ai_co_discuss 0.3.97 → 0.3.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runner/discuss.py CHANGED
@@ -121,6 +121,14 @@ def parallel_groups(names, joined):
121
121
  return groups
122
122
 
123
123
 
124
+ def repeat_gate(value):
125
+ """`--repeat AFTER=START`: a loop's checkpoint after AFTER's step whose next pass starts at START's step."""
126
+ after, separator, start = value.partition("=")
127
+ if not separator:
128
+ raise argparse.ArgumentTypeError("须写成 检查点成员=再来一圈的起点成员,如 批评家=写手")
129
+ return {"after": member_handle(after), "repeat_from": member_handle(start)}
130
+
131
+
124
132
  def positive_int(value):
125
133
  """Parse a positive page or resource identifier."""
126
134
  try:
@@ -312,6 +320,10 @@ def message_commands(subcommands, registry):
312
320
  help="自由房间主持人:--to 点名的多位成员按点名顺序依次接力,走完一圈算一轮,最后一位做完交回你")
313
321
  post.add_argument("--loop", action="store_true",
314
322
  help="与 --sequential 同用:交回成为检查点,用 sequence continue 开始下一圈,不继续即直接验收")
323
+ post.add_argument("--checkpoint", type=member_handle, action="append", default=[], metavar="MEMBER",
324
+ help="与 --sequential 同用:该成员所在那一棒结束后交回你(检查点),你用 sequence continue 往下一棒走,直接回答即结束接力;可重复")
325
+ post.add_argument("--repeat", type=repeat_gate, action="append", default=[], metavar="MEMBER=START",
326
+ help="与 --sequential 同用:MEMBER 所在那一棒结束后为循环检查点,sequence continue 从 START 所在那一棒再来一圈,加 --proceed 往下一棒走;START 不能早于前一个检查点;可重复")
315
327
  post.add_argument("--detach", type=member_handle, action="append", default=[], metavar="MEMBER",
316
328
  help="自由房间主持人:与 --to 同用,点名一位与这次派活同时开工、但不等它的成员(不计入交回或下一棒的等待);它失败时程序通知你;可重复")
317
329
  post.add_argument("--with", dest="to", type=member_handle, action=JoinPreviousStep, metavar="MEMBER",
@@ -475,8 +487,9 @@ def sequence_commands(subcommands, registry):
475
487
  """Read a host's one-after-another relay and run a loop's next pass from its checkpoint."""
476
488
  view = add_leaf(subcommands, registry, "sequence view", "房主或主持人读取依次接力的顺序、当前圈与本圈各棒原文;此读取不派发任务", True)
477
489
  view.add_argument("id", type=positive_int, help="依次接力编号(任务文件 requests[].sequence.id)")
478
- advance = add_leaf(subcommands, registry, "sequence continue", "主持人在循环检查点开始下一圈,只结束这条检查点交接;不继续则直接验收回答", True)
479
- advance.add_argument("--request", type=positive_int, required=True, help="检查点交接请求编号(purpose=handoff 且 sequence.phase=checkpoint)")
490
+ advance = add_leaf(subcommands, registry, "sequence continue", "主持人在检查点让接力继续:循环检查点再来一圈(加 --proceed 往下一棒),普通检查点往下一棒;只结束这条检查点交接,不继续则直接回答即结束接力", True)
491
+ advance.add_argument("--request", type=positive_int, required=True, help="检查点交接请求编号(purpose=handoff 且 sequence.phase 为 checkpoint 或 gate)")
492
+ advance.add_argument("--proceed", action="store_true", help="循环检查点不再来一圈,往下一棒走;最后一棒之后没有下一棒")
480
493
 
481
494
 
482
495
  def build_parser(prog="discuss"):
@@ -720,6 +733,11 @@ def plan_message(args):
720
733
  payload["parallel"] = parallel_groups(args.to, joined)
721
734
  if args.loop:
722
735
  payload["loop"] = True
736
+ gates = [{"after": name} for name in args.checkpoint] + args.repeat
737
+ if gates:
738
+ if not args.sequential:
739
+ raise CliError("--checkpoint 与 --repeat 须与 --sequential 一起使用")
740
+ payload["gates"] = gates
723
741
  for field in ("next_review", "review_scope", "review_roster"):
724
742
  if getattr(args, field, None) is not None:
725
743
  payload[field] = getattr(args, field)
@@ -933,7 +951,8 @@ def plan_sequence(args):
933
951
  """Read a relay, or continue a loop bound to its one checkpoint request."""
934
952
  if args.action == "view":
935
953
  return RequestPlan(f"/api/rooms/{args.room}/sequences/{args.id}")
936
- return RequestPlan(f"/api/rooms/{args.room}/sequences/continue", "POST", {"actor": require_actor(args), "request_id": args.request})
954
+ return RequestPlan(f"/api/rooms/{args.room}/sequences/continue", "POST", {"actor": require_actor(args), "request_id": args.request,
955
+ **({"proceed": True} if args.proceed else {})})
937
956
 
938
957
 
939
958
  def build_plan(args):
@@ -0,0 +1,212 @@
1
+ /**
2
+ * A workflow's canvas (`flow.json`): member cards and host checkpoints joined by arrows, compiled into the host's
3
+ * one-after-another dispatch: steps (a card, or cards working at once), and gates where the relay hands back to the host,
4
+ * optionally looping over part of it. Plain JavaScript without Node modules: the web editor and the runner share it.
5
+ *
6
+ * Shape the program runs: from the start node, each layer is one step; cards in one layer work at once and must all come
7
+ * from the same step and go to the same next one. A host node is a checkpoint after the step before it. An arrow back to an
8
+ * earlier card makes a loop: the step it leaves (or the host node it leaves) becomes a checkpoint whose next pass starts at
9
+ * that card, never reaching back past the checkpoint before it. No branching on conditions: put a host node there.
10
+ */
11
+
12
+ export const FLOW_NODE_TYPES = ['start', 'card', 'host'];
13
+ export const MAX_FLOW_NODES = 60;
14
+ export const MAX_FLOW_EDGES = 120;
15
+ const NODE_ID = /^[A-Za-z0-9_-]{1,40}$/;
16
+
17
+ /** A blank canvas: only the start node. */
18
+ export function empty_flow() {
19
+ return { nodes: [{ id: 'start', type: 'start', x: 0, y: 0 }], edges: [] };
20
+ }
21
+
22
+ /** Whether a flow holds anything beyond its start node. */
23
+ export function flow_in_use(flow) {
24
+ return Boolean(flow?.nodes?.some(node => node.type !== 'start'));
25
+ }
26
+
27
+ /** Check the stored shape of a flow and return a clean copy; throws in Chinese on anything malformed. */
28
+ export function normalize_flow(flow) {
29
+ if (!flow || typeof flow !== 'object' || !Array.isArray(flow.nodes) || !Array.isArray(flow.edges)) throw new Error('flow 须为 { nodes, edges }');
30
+ if (flow.nodes.length > MAX_FLOW_NODES || flow.edges.length > MAX_FLOW_EDGES) throw new Error(`画布最多 ${MAX_FLOW_NODES} 个节点、${MAX_FLOW_EDGES} 条连线`);
31
+ const ids = new Set();
32
+ const nodes = flow.nodes.map(node => {
33
+ if (!node || !NODE_ID.test(node.id) || ids.has(node.id) || !FLOW_NODE_TYPES.includes(node.type)) throw new Error('画布节点须有不重复的 id 与类型 start / card / host');
34
+ if (node.type === 'card' && (typeof node.card !== 'string' || !node.card)) throw new Error(`节点 ${node.id} 没有指明卡片`);
35
+ ids.add(node.id);
36
+ const x = Number.isFinite(node.x) ? Math.round(node.x) : 0, y = Number.isFinite(node.y) ? Math.round(node.y) : 0;
37
+ return { id: node.id, type: node.type, ...(node.type === 'card' ? { card: node.card } : {}), x, y };
38
+ });
39
+ const seen = new Set();
40
+ const edges = flow.edges.map(edge => {
41
+ if (!edge || !ids.has(edge.source) || !ids.has(edge.target)) throw new Error('连线须连接画布上已有的节点');
42
+ const key = `${edge.source}>${edge.target}`;
43
+ if (seen.has(key)) throw new Error('两个节点之间只能有一条同向连线');
44
+ seen.add(key);
45
+ return { source: edge.source, target: edge.target };
46
+ });
47
+ return { nodes, edges };
48
+ }
49
+
50
+ /** Top to bottom, then left to right: the order members of one step are named in. */
51
+ function by_position(left, right) {
52
+ return left.y - right.y || left.x - right.x;
53
+ }
54
+
55
+ /** Walk from the start: an arrow to a node still on the walk's path goes back (a loop); every other arrow runs forward. */
56
+ function walk_edges(nodes, outgoing, start_id) {
57
+ const back = [], forward = [], state = new Map();
58
+ const walk = id => {
59
+ state.set(id, 'open');
60
+ for (const target of [...outgoing.get(id)].sort((left, right) => by_position(nodes.get(left), nodes.get(right)))) {
61
+ if (state.get(target) === 'open') back.push({ source: id, target });
62
+ else {
63
+ forward.push({ source: id, target });
64
+ if (!state.has(target)) walk(target);
65
+ }
66
+ }
67
+ state.set(id, 'done');
68
+ };
69
+ walk(start_id);
70
+ return { back, forward, state };
71
+ }
72
+
73
+ /**
74
+ * Arrows of a canvas as the relay reads them, for drawing it: `loops` holds `source>target` of each arrow going back, and
75
+ * `layers` gives each node reached from the start its column (the longest forward path to it).
76
+ */
77
+ export function flow_shape(flow) {
78
+ try {
79
+ const shape = normalize_flow(flow);
80
+ const nodes = new Map(shape.nodes.map(node => [node.id, node]));
81
+ const start = shape.nodes.find(node => node.type === 'start');
82
+ if (!start) return { loops: new Set(), layers: new Map() };
83
+ const outgoing = new Map(shape.nodes.map(node => [node.id, []]));
84
+ for (const edge of shape.edges) if (edge.source !== edge.target && edge.target !== start.id) outgoing.get(edge.source).push(edge.target);
85
+ const { back, forward } = walk_edges(nodes, outgoing, start.id);
86
+ const layers = new Map([[start.id, 0]]);
87
+ // Forward arrows have no cycle, so relaxing them as many times as there are nodes settles every column.
88
+ for (let round = 0; round < shape.nodes.length; round++) {
89
+ for (const edge of forward) if (layers.has(edge.source)) layers.set(edge.target, Math.max(layers.get(edge.target) ?? 0, layers.get(edge.source) + 1));
90
+ }
91
+ return { loops: new Set(back.map(edge => `${edge.source}>${edge.target}`)), layers };
92
+ } catch { return { loops: new Set(), layers: new Map() }; }
93
+ }
94
+
95
+ /**
96
+ * Compile a flow over the given card IDs (`labels` names them in errors): `{ steps: [[card…]…], gates: [{after, repeat_from}], errors: [text…], empty }`.
97
+ * Step and gate positions are step indexes. Any error leaves steps and gates out.
98
+ */
99
+ export function compile_flow(flow, cards = [], labels = {}) {
100
+ let shape;
101
+ try { shape = normalize_flow(flow || empty_flow()); }
102
+ catch (error) { return { steps: [], gates: [], errors: [error.message], empty: false }; }
103
+ const errors = [];
104
+ const nodes = new Map(shape.nodes.map(node => [node.id, node]));
105
+ const starts = shape.nodes.filter(node => node.type === 'start');
106
+ if (starts.length !== 1) return { steps: [], gates: [], errors: ['画布须有且只有一个开始节点'], empty: false };
107
+ const start = starts[0];
108
+ const used = shape.nodes.filter(node => node.type !== 'start');
109
+ // Cards laid out with no arrow from the start yet are no relay at all, not a broken one.
110
+ if (!used.length || !shape.edges.some(edge => edge.source === start.id)) return { steps: [], gates: [], errors: [], empty: true };
111
+ const known = new Set(cards);
112
+ const label = node => node.type === 'host' ? '主持人检查点' : node.type === 'start' ? '开始' : `卡片 ${labels[node.card] || node.card}`;
113
+ const placed = new Set();
114
+ for (const node of used) {
115
+ if (node.type !== 'card') continue;
116
+ if (!known.has(node.card)) errors.push(`画布引用了不存在的卡片 ${node.card}`);
117
+ if (placed.has(node.card)) errors.push(`卡片 ${node.card} 在画布上出现了多次`);
118
+ placed.add(node.card);
119
+ }
120
+ const outgoing = new Map(shape.nodes.map(node => [node.id, []]));
121
+ for (const edge of shape.edges) {
122
+ if (edge.source === edge.target) errors.push(`${label(nodes.get(edge.source))} 连向了自己`);
123
+ else if (edge.target === start.id) errors.push('不能连回开始节点');
124
+ else outgoing.get(edge.source).push(edge.target);
125
+ }
126
+ if (errors.length) return { steps: [], gates: [], errors, empty: false };
127
+
128
+ const { back, forward, state } = walk_edges(nodes, outgoing, start.id);
129
+ const unreached = used.filter(node => !state.has(node.id));
130
+ if (unreached.length) return { steps: [], gates: [], errors: unreached.map(node => `${label(node)} 没有从开始节点连过来`), empty: false };
131
+
132
+ const successors = id => new Set(forward.filter(edge => edge.source === id).map(edge => edge.target));
133
+ const predecessors = id => new Set(forward.filter(edge => edge.target === id).map(edge => edge.source));
134
+ const same = (left, right) => left.size === right.size && [...left].every(item => right.has(item));
135
+ const steps = [], gates = [], step_of = new Map(), gate_node = new Map();
136
+ let layer = [start.id];
137
+ while (true) {
138
+ const next = new Set(layer.flatMap(id => [...successors(id)]));
139
+ if (!next.size) break;
140
+ const current = new Set(layer);
141
+ if (layer.some(id => !same(successors(id), next))) {
142
+ errors.push(`${layer.map(id => label(nodes.get(id))).join('、')} 须一起交给同一棒`);
143
+ break;
144
+ }
145
+ const members = [...next].map(id => nodes.get(id)).sort(by_position);
146
+ if (members.some(node => !same(predecessors(node.id), current))) {
147
+ errors.push(`${members.map(label).join('、')} 须由同一棒交来`);
148
+ break;
149
+ }
150
+ const hosts = members.filter(node => node.type === 'host');
151
+ if (hosts.length) {
152
+ if (members.length > 1) { errors.push('主持人检查点须单独一棒,不能与卡片并列'); break; }
153
+ if (!steps.length) { errors.push('开始节点之后须先接卡片,主持人本来就在开始处派活'); break; }
154
+ if (gate_node.has(steps.length - 1)) { errors.push('两个主持人检查点不能相连'); break; }
155
+ gate_node.set(steps.length - 1, hosts[0].id);
156
+ } else {
157
+ members.forEach(node => step_of.set(node.id, steps.length));
158
+ steps.push(members.map(node => node.card));
159
+ }
160
+ layer = members.map(node => node.id);
161
+ }
162
+ if (errors.length) return { steps: [], gates: [], errors, empty: false };
163
+ if (steps.flat().length < 2) return { steps: [], gates: [], errors: ['流程至少要有两位成员接力'], empty: false };
164
+
165
+ const gate_of_node = new Map([...gate_node].map(([after, id]) => [id, after]));
166
+ const loops = new Map();
167
+ for (const edge of back) {
168
+ const target = nodes.get(edge.target);
169
+ if (target.type !== 'card') { errors.push('循环的回连须指向一张卡片'); continue; }
170
+ const after = step_of.has(edge.source) ? step_of.get(edge.source) : gate_of_node.get(edge.source);
171
+ const from = step_of.get(edge.target);
172
+ if (loops.has(after) && loops.get(after) !== from) errors.push(`第 ${after + 1} 棒之后只能回到一个起点`);
173
+ loops.set(after, from);
174
+ }
175
+ for (const after of new Set([...gate_node.keys(), ...loops.keys()])) {
176
+ // A plain checkpoint after the last step changes nothing: the relay hands back there anyway.
177
+ if (!loops.has(after) && after === steps.length - 1) continue;
178
+ gates.push({ after, repeat_from: loops.has(after) ? loops.get(after) : null });
179
+ }
180
+ gates.sort((left, right) => left.after - right.after);
181
+ gates.forEach((gate, index) => {
182
+ const floor = index ? gates[index - 1].after + 1 : 0;
183
+ if (gate.repeat_from !== null && gate.repeat_from < floor) errors.push(`第 ${gate.after + 1} 棒之后的循环跨过了前一个检查点`);
184
+ });
185
+ return errors.length ? { steps: [], gates: [], errors, empty: false } : { steps, gates, errors: [], empty: false };
186
+ }
187
+
188
+ /** Quote a member handle for a shell command line when it needs it. */
189
+ function quoted(value) {
190
+ return /^[\w\u4e00-\u9fff.-]+$/.test(value) ? value : `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
191
+ }
192
+
193
+ /**
194
+ * The dispatch a compiled flow becomes for the room's members: `{ to, parallel, gates }` for the API and the CLI flags.
195
+ * `member_of` maps a card ID to the member handle the host names it by; a card without a member is listed in `missing`.
196
+ */
197
+ export function flow_dispatch(compiled, member_of) {
198
+ const missing = compiled.steps.flat().filter(card => !member_of[card]);
199
+ if (compiled.errors.length || compiled.empty || missing.length) return { missing };
200
+ const steps = compiled.steps.map(step => step.map(card => member_of[card]));
201
+ const to = steps.flat();
202
+ const parallel = steps.filter(step => step.length > 1);
203
+ const gates = compiled.gates.map(gate => ({ after: steps[gate.after][0], ...(gate.repeat_from === null ? {} : { repeat_from: steps[gate.repeat_from][0] }) }));
204
+ const flags = [...steps.flatMap(step => step.map((name, index) => `${index ? '--with' : '--to'} ${quoted(name)}`)), '--sequential',
205
+ ...gates.map(gate => gate.repeat_from ? `--repeat ${quoted(`${gate.after}=${gate.repeat_from}`)}` : `--checkpoint ${quoted(gate.after)}`)];
206
+ return { missing, to, ...(parallel.length ? { parallel } : {}), ...(gates.length ? { gates } : {}), cli: flags.join(' '),
207
+ order: steps.map((step, index) => {
208
+ const gate = gates.find(item => item.after === step[0]);
209
+ const tail = !gate ? '' : gate.repeat_from ? `(之后检查点:可从 ${gate.repeat_from} 再来一圈)` : '(之后交回主持人)';
210
+ return `${index + 1}. ${step.join(' + ')}${tail}`;
211
+ }) };
212
+ }
@@ -6,6 +6,7 @@ import { createHash, randomUUID } from 'node:crypto';
6
6
  import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { DatabaseSync } from 'node:sqlite';
9
+ import { compile_flow, flow_dispatch, flow_in_use, normalize_flow } from './workflow_flow.mjs';
9
10
 
10
11
  // A workflow ID follows the Agent Skills naming rule: lowercase letters and digits joined by single hyphens.
11
12
  export const WORKFLOW_ID_PATTERN = /^(?=.{1,40}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -34,6 +35,8 @@ const RESOURCE_SEGMENT = /^[^./\\:*?"<>|\x00-\x1f][^/\\:*?"<>|\x00-\x1f]{0,99}$/
34
35
  const utf8 = new TextDecoder('utf-8', { fatal: true });
35
36
  // Commands an API member may run, registered beside SKILL.md so they travel with the workflow.
36
37
  export const COMMANDS_FILE = 'commands.json';
38
+ // The canvas of cards and arrows the host's relay follows; a field of its own, not one of the folder's other files.
39
+ export const FLOW_FILE = 'flow.json';
37
40
  const COMMAND_ID_PATTERN = /^(?=.{1,40}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
38
41
  const COMMAND_FIELDS = ['id', 'description', 'run', 'path', 'access', 'timeout_seconds'];
39
42
  const MAX_COMMANDS = 20;
@@ -151,16 +154,31 @@ function card_from_markdown(card, text) {
151
154
  return value;
152
155
  }
153
156
 
154
- /** Turn SKILL.md, card files and the folder's other files into one workflow object, unchecked. */
157
+ /** Turn SKILL.md, card files and the folder's other files into one workflow object, unchecked; flow.json becomes `flow`. */
155
158
  function workflow_from_files(skill_text, cards, resources = {}) {
156
159
  const { fields, body } = parse_markdown(skill_text, 'SKILL.md');
160
+ const { [FLOW_FILE]: flow_text, ...rest } = resources;
161
+ let flow;
162
+ if (typeof flow_text === 'string') {
163
+ try { flow = JSON.parse(flow_text); } catch (error) { throw new Error(`${FLOW_FILE} 不是合法 JSON:${error.message}`); }
164
+ }
157
165
  return { id: fields.name || '', label: fields.label || '', description: fields.description || '', skill: body,
158
- agents: Object.entries(cards).map(([card, text]) => card_from_markdown(card, text)), resources };
166
+ agents: Object.entries(cards).map(([card, text]) => card_from_markdown(card, text)), resources: rest, ...(flow ? { flow } : {}) };
167
+ }
168
+
169
+ /** A checked canvas over the workflow's cards, or null for none; an arrangement the relay cannot follow is refused. */
170
+ function validate_flow(flow, cards) {
171
+ if (flow === undefined || flow === null) return flow;
172
+ const normalized = normalize_flow(flow);
173
+ if (!flow_in_use(normalized)) return null;
174
+ const compiled = compile_flow(normalized, cards);
175
+ if (compiled.errors.length) throw new Error(`流程画布:${compiled.errors.join(';')}`);
176
+ return normalized;
159
177
  }
160
178
 
161
179
  /** A path, relative to the workflow folder, that a resource may take: inside it, not SKILL.md and not under agents/. */
162
180
  function resource_path_allowed(path) {
163
- return path !== 'SKILL.md' && !path.startsWith('agents/') && path.split('/').every(segment => RESOURCE_SEGMENT.test(segment));
181
+ return path !== 'SKILL.md' && path !== FLOW_FILE && !path.startsWith('agents/') && path.split('/').every(segment => RESOURCE_SEGMENT.test(segment));
164
182
  }
165
183
 
166
184
  /** Check resource files `{ "relative/path": text }`. */
@@ -185,6 +203,8 @@ function read_resource_folder(directory, prefix, resources, ignored) {
185
203
  for (const name of readdirSync(directory).sort()) {
186
204
  const relative = `${prefix}${name}`;
187
205
  if (relative === 'SKILL.md' || relative === 'agents') continue;
206
+ // The canvas is read as a resource here and taken out as `flow` with the rest of the folder.
207
+ if (relative === FLOW_FILE) { resources[relative] = readFileSync(join(directory, name), 'utf8'); continue; }
188
208
  const path = join(directory, name);
189
209
  if (lstatSync(path).isDirectory()) { read_resource_folder(path, `${relative}/`, resources, ignored); continue; }
190
210
  let text = null;
@@ -245,8 +265,11 @@ export function validate_workflow(input, validate_card) {
245
265
  return { card, ...kept };
246
266
  });
247
267
  // Absent resources mean "keep the saved folder's other files" (a host draft or the editor carries none); `{}` clears them.
268
+ // Likewise an absent flow keeps the saved canvas, and null removes it.
269
+ const flow = validate_flow(input.flow, normalized.map(agent => agent.card));
248
270
  return { id, label, description, skill, agents: normalized,
249
- ...(input.resources === undefined ? {} : { resources: validate_resources(input.resources) }) };
271
+ ...(input.resources === undefined ? {} : { resources: validate_resources(input.resources) }),
272
+ ...(flow === undefined ? {} : { flow }) };
250
273
  }
251
274
 
252
275
  /** Refuse links anywhere in a folder, so a shared workflow cannot point outside itself. */
@@ -315,7 +338,7 @@ export function read_workflow_files(files) {
315
338
  if (path === 'SKILL.md') continue;
316
339
  const match = CARD_PATH.exec(path);
317
340
  if (match && typeof files[path] === 'string') cards[match[1]] = files[path];
318
- else if (typeof files[path] === 'string' && resource_path_allowed(path)) resources[path] = files[path];
341
+ else if (typeof files[path] === 'string' && (path === FLOW_FILE || resource_path_allowed(path))) resources[path] = files[path];
319
342
  else ignored.push(path);
320
343
  }
321
344
  return { workflow: workflow_from_files(files['SKILL.md'], cards, resources), ignored };
@@ -378,6 +401,9 @@ export function save_workflow(home, input, validate_card) {
378
401
  }
379
402
  if (workflow.resources) write_resources(staging, workflow.resources);
380
403
  else if (existsSync(directory)) keep_resources(directory, staging);
404
+ if (workflow.flow) writeFileSync(join(staging, FLOW_FILE), `${JSON.stringify(workflow.flow, null, 2)}
405
+ `);
406
+ else if (workflow.flow === null || workflow.resources) rmSync(join(staging, FLOW_FILE), { force: true });
381
407
  });
382
408
  return read_saved_workflow(home, workflow.id);
383
409
  });
@@ -468,9 +494,12 @@ export function load_room_workflow(config) {
468
494
  const workflow = get_workflow(config.api_home, binding.id);
469
495
  const { directory } = workflow;
470
496
  const cards = Object.fromEntries(workflow.agents.map(agent => [agent.card, agent]));
497
+ // A canvas whose start has no arrow yet asks for no relay.
498
+ const flow = workflow.flow ? compile_flow(workflow.flow, Object.keys(cards)) : null;
471
499
  return { id: binding.id, label: workflow.label || binding.id, description: workflow.description || '', directory, skill_path: join(directory, 'SKILL.md'), skill: workflow.skill,
472
500
  revision: createHash('sha256').update(workflow.skill).digest('hex').slice(0, REVISION_LENGTH),
473
- members: binding.members || {}, cards, commands: workflow.commands };
501
+ members: binding.members || {}, cards, commands: workflow.commands,
502
+ ...(flow && !flow.empty ? { flow } : {}) };
474
503
  } catch (error) { return { id: binding.id, error: error.message }; }
475
504
  }
476
505
 
@@ -501,7 +530,19 @@ export function workflow_task_field(workflow, room_members, participants) {
501
530
  .map(card => ({ card: card.card, label: card.label || '', description: card.description || '' }));
502
531
  // The host reads SKILL.md only when a request fits the description and its revision is one it has not read.
503
532
  return { id: workflow.id, label: workflow.label, description: workflow.description || '', revision: workflow.revision || '', members,
504
- ...(missing.length ? { missing } : {}) };
533
+ ...(missing.length ? { missing } : {}), ...(workflow.flow ? { flow: flow_task_field(workflow.flow, members) } : {}) };
534
+ }
535
+
536
+ /**
537
+ * The relay the canvas asks for, ready for the host to dispatch: the order in words, the CLI flags and the API fields.
538
+ * Cards whose member is away or removed are named instead, since the relay cannot run without them.
539
+ */
540
+ function flow_task_field(compiled, members) {
541
+ if (compiled.errors.length) return { errors: compiled.errors };
542
+ const member_of = Object.fromEntries(members.filter(item => item.status === 'active').map(item => [item.card, item.member]));
543
+ const { missing, order, cli, to, parallel, gates } = flow_dispatch(compiled, member_of);
544
+ if (missing.length) return { missing_cards: missing };
545
+ return { order, cli, api: { to, sequential: true, ...(parallel ? { parallel } : {}), ...(gates ? { gates } : {}) } };
505
546
  }
506
547
 
507
548
  /**
package/web/app.js CHANGED
@@ -87,7 +87,7 @@ const PLATFORM_IDENTITIES = {
87
87
  Antigravity: { label: "Gemini", initials: "GM", style: "gemini", icon: "/icons/gemini.svg", description: "AI 身份" },
88
88
  DeepSeek: { initials: "DS", style: "deepseek", icon: "/icons/deepseek.svg", description: "AI 身份" },
89
89
  Qwen: { initials: "QW", style: "qwen", icon: "/icons/qwen.svg", description: "AI 身份" },
90
- Grok: { initials: "GK", style: "grok", description: "AI 身份" },
90
+ Grok: { initials: "GK", style: "grok", icon: "/icons/grok.svg", description: "AI 身份" },
91
91
  OpenAI: { initials: "OA", style: "codex", icon: "/icons/openai.svg", description: "AI 身份" },
92
92
  Custom: { label: "自定义 API", initials: "API", style: "custom", api_label: "自定义 API", description: "AI 身份" },
93
93
  };
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Azure</title><path d="M7.242 1.613A1.11 1.11 0 018.295.857h6.977L8.03 22.316a1.11 1.11 0 01-1.052.755h-5.43a1.11 1.11 0 01-1.053-1.466L7.242 1.613z" fill="url(#lobe-icons-azure-0-_R_0_)"></path><path d="M18.397 15.296H7.4a.51.51 0 00-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226l-2.706-7.775z" fill="#0078D4"></path><path d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998L15.272.857z" fill="url(#lobe-icons-azure-1-_R_0_)"></path><path d="M17.193 1.613a1.11 1.11 0 00-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 01-1.052 1.466h-.12 7.895a1.11 1.11 0 001.052-1.466L17.193 1.613z" fill="url(#lobe-icons-azure-2-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-0-_R_0_" x1="8.247" x2="1.002" y1="1.626" y2="23.03"><stop stop-color="#114A8B"></stop><stop offset="1" stop-color="#0669BC"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-1-_R_0_" x1="14.042" x2="12.324" y1="15.302" y2="15.888"><stop stop-opacity=".3"></stop><stop offset=".071" stop-opacity=".2"></stop><stop offset=".321" stop-opacity=".1"></stop><stop offset=".623" stop-opacity=".05"></stop><stop offset="1" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-2-_R_0_" x1="12.841" x2="20.793" y1="1.626" y2="22.814"><stop stop-color="#3CCBF4"></stop><stop offset="1" stop-color="#2892DF"></stop></linearGradient></defs></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Baichuan</title><defs><linearGradient id="lobe-icons-baichuan-_R_0_" x1="17.764%" x2="100%" y1="8.678%" y2="91.322%"><stop offset="0%" stop-color="#FEC13E"></stop><stop offset="100%" stop-color="#FF6933"></stop></linearGradient></defs><path d="M7.333 2h-3.2l-2 4.333V17.8L0 22h5.2l2.028-4.2L7.333 2zm7.334 0h-5.2v20h5.2V2zM16.8 7.733H22V22h-5.2V7.733zM22 2h-5.2v4.133H22V2z" fill="url(#lobe-icons-baichuan-_R_0_)" fill-rule="nonzero"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cohere</title><path clip-rule="evenodd" d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z" fill="#39594D" fill-rule="evenodd"></path><path clip-rule="evenodd" d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z" fill="#D18EE2" fill-rule="evenodd"></path><path d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z" fill="#FF7759"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Doubao</title><path d="M5.31 15.756c.172-3.75 1.883-5.999 2.549-6.739-3.26 2.058-5.425 5.658-6.358 8.308v1.12C1.501 21.513 4.226 24 7.59 24a6.59 6.59 0 002.2-.375c.353-.12.7-.248 1.039-.378.913-.899 1.65-1.91 2.243-2.992-4.877 2.431-7.974.072-7.763-4.5l.002.001z" fill="#1E37FC"></path><path d="M22.57 10.283c-1.212-.901-4.109-2.404-7.397-2.8.295 3.792.093 8.766-2.1 12.773a12.782 12.782 0 01-2.244 2.992c3.764-1.448 6.746-3.457 8.596-5.219 2.82-2.683 3.353-5.178 3.361-6.66a2.737 2.737 0 00-.216-1.084v-.002z" fill="#37E1BE"></path><path d="M14.303 1.867C12.955.7 11.248 0 9.39 0 7.532 0 5.883.677 4.545 1.807 2.791 3.29 1.627 5.557 1.5 8.125v9.201c.932-2.65 3.097-6.25 6.357-8.307.5-.318 1.025-.595 1.569-.829 1.883-.801 3.878-.932 5.746-.706-.222-2.83-.718-5.002-.87-5.617h.001z" fill="#A569FF"></path><path d="M17.305 4.961a199.47 199.47 0 01-1.08-1.094c-.202-.213-.398-.419-.586-.622l-1.333-1.378c.151.615.648 2.786.869 5.617 3.288.395 6.185 1.898 7.396 2.8-1.306-1.275-3.475-3.487-5.266-5.323z" fill="#1E37FC"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GithubCopilot</title><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Hunyuan</title><circle cx="12" cy="12" fill="#0055E9" r="12"></circle><path d="M12 0c.518 0 1.028.033 1.528.096A6.188 6.188 0 0112.12 12.28l-.12.001c-2.99 0-5.242 2.179-5.554 5.11-.223 2.086.353 4.412 2.242 6.146C3.672 22.1 0 17.479 0 12 0 5.373 5.373 0 12 0z" fill="#A8DFF5"></path><path d="M5.286 5a2.438 2.438 0 01.682 3.38c-3.962 5.966-3.215 10.743 2.648 15.136C3.636 22.056 0 17.452 0 12c0-1.787.39-3.482 1.09-5.006.253-.435.525-.872.817-1.311A2.438 2.438 0 015.286 5z" fill="#0055E9"></path><path d="M12.98.04c.272.021.543.053.81.093.583.106 1.117.254 1.538.44 6.638 2.927 8.07 10.052 1.748 15.642a4.125 4.125 0 01-5.822-.358c-1.51-1.706-1.3-4.184.357-5.822.858-.848 3.108-1.223 4.045-2.441 1.257-1.634 2.122-6.009-2.523-7.506L12.98.039z" fill="#00BCFF"></path><path d="M13.528.096A6.187 6.187 0 0112 12.281a5.75 5.75 0 00-1.71.255c.147-.905.595-1.784 1.321-2.501.858-.848 3.108-1.223 4.045-2.441 1.27-1.651 2.14-6.104-2.676-7.554.184.014.367.033.548.056z" fill="#ECECEE"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z" fill="#1783FF"></path><path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z" fill="currentColor"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Meta</title><path d="M6.897 4h-.024l-.031 2.615h.022c1.715 0 3.046 1.357 5.94 6.246l.175.297.012.02 1.62-2.438-.012-.019a48.763 48.763 0 00-1.098-1.716 28.01 28.01 0 00-1.175-1.629C10.413 4.932 8.812 4 6.896 4z" fill="url(#lobe-icons-meta-0-_R_0_)"></path><path d="M6.873 4C4.95 4.01 3.247 5.258 2.02 7.17a4.352 4.352 0 00-.01.017l2.254 1.231.011-.017c.718-1.083 1.61-1.774 2.568-1.785h.021L6.896 4h-.023z" fill="url(#lobe-icons-meta-1-_R_0_)"></path><path d="M2.019 7.17l-.011.017C1.2 8.447.598 9.995.274 11.664l-.005.022 2.534.6.004-.022c.27-1.467.786-2.828 1.456-3.845l.011-.017L2.02 7.17z" fill="url(#lobe-icons-meta-2-_R_0_)"></path><path d="M2.807 12.264l-2.533-.6-.005.022c-.177.918-.267 1.851-.269 2.786v.023l2.598.233v-.023a12.591 12.591 0 01.21-2.44z" fill="url(#lobe-icons-meta-3-_R_0_)"></path><path d="M2.677 15.537a5.462 5.462 0 01-.079-.813v-.022L0 14.468v.024a8.89 8.89 0 00.146 1.652l2.535-.585a4.106 4.106 0 01-.004-.022z" fill="url(#lobe-icons-meta-4-_R_0_)"></path><path d="M3.27 16.89c-.284-.31-.484-.756-.589-1.328l-.004-.021-2.535.585.004.021c.192 1.01.568 1.85 1.106 2.487l.014.017 2.018-1.745a2.106 2.106 0 01-.015-.016z" fill="url(#lobe-icons-meta-5-_R_0_)"></path><path d="M10.78 9.654c-1.528 2.35-2.454 3.825-2.454 3.825-2.035 3.2-2.739 3.917-3.871 3.917a1.545 1.545 0 01-1.186-.508l-2.017 1.744.014.017C2.01 19.518 3.058 20 4.356 20c1.963 0 3.374-.928 5.884-5.33l1.766-3.13a41.283 41.283 0 00-1.227-1.886z" fill="#0082FB"></path><path d="M13.502 5.946l-.016.016c-.4.43-.786.908-1.16 1.416.378.483.768 1.024 1.175 1.63.48-.743.928-1.345 1.367-1.807l.016-.016-1.382-1.24z" fill="url(#lobe-icons-meta-6-_R_0_)"></path><path d="M20.918 5.713C19.853 4.633 18.583 4 17.225 4c-1.432 0-2.637.787-3.723 1.944l-.016.016 1.382 1.24.016-.017c.715-.747 1.408-1.12 2.176-1.12.826 0 1.6.39 2.27 1.075l.015.016 1.589-1.425-.016-.016z" fill="#0082FB"></path><path d="M23.998 14.125c-.06-3.467-1.27-6.566-3.064-8.396l-.016-.016-1.588 1.424.015.016c1.35 1.392 2.277 3.98 2.361 6.971v.023h2.292v-.022z" fill="url(#lobe-icons-meta-7-_R_0_)"></path><path d="M23.998 14.15v-.023h-2.292v.022c.004.14.006.282.006.424 0 .815-.121 1.474-.368 1.95l-.011.022 1.708 1.782.013-.02c.62-.96.946-2.293.946-3.91 0-.083 0-.165-.002-.247z" fill="url(#lobe-icons-meta-8-_R_0_)"></path><path d="M21.344 16.52l-.011.02c-.214.402-.519.67-.917.787l.778 2.462a3.493 3.493 0 00.438-.182 3.558 3.558 0 001.366-1.218l.044-.065.012-.02-1.71-1.784z" fill="url(#lobe-icons-meta-9-_R_0_)"></path><path d="M19.92 17.393c-.262 0-.492-.039-.718-.14l-.798 2.522c.449.153.927.222 1.46.222.492 0 .943-.073 1.352-.215l-.78-2.462c-.167.05-.341.075-.517.073z" fill="url(#lobe-icons-meta-10-_R_0_)"></path><path d="M18.323 16.534l-.014-.017-1.836 1.914.016.017c.637.682 1.246 1.105 1.937 1.337l.797-2.52c-.291-.125-.573-.353-.9-.731z" fill="url(#lobe-icons-meta-11-_R_0_)"></path><path d="M18.309 16.515c-.55-.642-1.232-1.712-2.303-3.44l-1.396-2.336-.011-.02-1.62 2.438.012.02.989 1.668c.959 1.61 1.74 2.774 2.493 3.585l.016.016 1.834-1.914a2.353 2.353 0 01-.014-.017z" fill="url(#lobe-icons-meta-12-_R_0_)"></path><defs><linearGradient id="lobe-icons-meta-0-_R_0_" x1="75.897%" x2="26.312%" y1="89.199%" y2="12.194%"><stop offset=".06%" stop-color="#0867DF"></stop><stop offset="45.39%" stop-color="#0668E1"></stop><stop offset="85.91%" stop-color="#0064E0"></stop></linearGradient><linearGradient id="lobe-icons-meta-1-_R_0_" x1="21.67%" x2="97.068%" y1="75.874%" y2="23.985%"><stop offset="13.23%" stop-color="#0064DF"></stop><stop offset="99.88%" stop-color="#0064E0"></stop></linearGradient><linearGradient id="lobe-icons-meta-2-_R_0_" x1="38.263%" x2="60.895%" y1="89.127%" y2="16.131%"><stop offset="1.47%" stop-color="#0072EC"></stop><stop offset="68.81%" stop-color="#0064DF"></stop></linearGradient><linearGradient id="lobe-icons-meta-3-_R_0_" x1="47.032%" x2="52.15%" y1="90.19%" y2="15.745%"><stop offset="7.31%" stop-color="#007CF6"></stop><stop offset="99.43%" stop-color="#0072EC"></stop></linearGradient><linearGradient id="lobe-icons-meta-4-_R_0_" x1="52.155%" x2="47.591%" y1="58.301%" y2="37.004%"><stop offset="7.31%" stop-color="#007FF9"></stop><stop offset="100%" stop-color="#007CF6"></stop></linearGradient><linearGradient id="lobe-icons-meta-5-_R_0_" x1="37.689%" x2="61.961%" y1="12.502%" y2="63.624%"><stop offset="7.31%" stop-color="#007FF9"></stop><stop offset="100%" stop-color="#0082FB"></stop></linearGradient><linearGradient id="lobe-icons-meta-6-_R_0_" x1="34.808%" x2="62.313%" y1="68.859%" y2="23.174%"><stop offset="27.99%" stop-color="#007FF8"></stop><stop offset="91.41%" stop-color="#0082FB"></stop></linearGradient><linearGradient id="lobe-icons-meta-7-_R_0_" x1="43.762%" x2="57.602%" y1="6.235%" y2="98.514%"><stop offset="0%" stop-color="#0082FB"></stop><stop offset="99.95%" stop-color="#0081FA"></stop></linearGradient><linearGradient id="lobe-icons-meta-8-_R_0_" x1="60.055%" x2="39.88%" y1="4.661%" y2="69.077%"><stop offset="6.19%" stop-color="#0081FA"></stop><stop offset="100%" stop-color="#0080F9"></stop></linearGradient><linearGradient id="lobe-icons-meta-9-_R_0_" x1="30.282%" x2="61.081%" y1="59.32%" y2="33.244%"><stop offset="0%" stop-color="#027AF3"></stop><stop offset="100%" stop-color="#0080F9"></stop></linearGradient><linearGradient id="lobe-icons-meta-10-_R_0_" x1="20.433%" x2="82.112%" y1="50.001%" y2="50.001%"><stop offset="0%" stop-color="#0377EF"></stop><stop offset="99.94%" stop-color="#0279F1"></stop></linearGradient><linearGradient id="lobe-icons-meta-11-_R_0_" x1="40.303%" x2="72.394%" y1="35.298%" y2="57.811%"><stop offset=".19%" stop-color="#0471E9"></stop><stop offset="100%" stop-color="#0377EF"></stop></linearGradient><linearGradient id="lobe-icons-meta-12-_R_0_" x1="32.254%" x2="68.003%" y1="19.719%" y2="84.908%"><stop offset="27.65%" stop-color="#0867DF"></stop><stop offset="100%" stop-color="#0471E9"></stop></linearGradient></defs></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-_R_0_" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-_R_0_)" fill-rule="nonzero"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Mistral</title><path d="M3.428 3.4h3.429v3.428H3.428V3.4zm13.714 0h3.43v3.428h-3.43V3.4z" fill="gold"></path><path d="M3.428 6.828h6.857v3.429H3.429V6.828zm10.286 0h6.857v3.429h-6.857V6.828z" fill="#FFAF00"></path><path d="M3.428 10.258h17.144v3.428H3.428v-3.428z" fill="#FF8205"></path><path d="M3.428 13.686h3.429v3.428H3.428v-3.428zm6.858 0h3.429v3.428h-3.429v-3.428zm6.856 0h3.43v3.428h-3.43v-3.428z" fill="#FA500F"></path><path d="M0 17.114h10.286v3.429H0v-3.429zm13.714 0H24v3.429H13.714v-3.429z" fill="#E10500"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title><path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M18.654 3.87a5.087 5.087 0 110 10.174L23.7 19.09c.64.641.187 1.737-.72 1.737H8.48a8.479 8.479 0 010-16.958h10.175zM8.479 7.26a5.087 5.087 0 100 10.176 5.087 5.087 0 000-10.175z" fill="#C8FF00"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Perplexity</title><path d="M19.785 0v7.272H22.5V17.62h-2.935V24l-7.037-6.194v6.145h-1.091v-6.152L4.392 24v-6.465H1.5V7.188h2.884V0l7.053 6.494V.19h1.09v6.49L19.786 0zm-7.257 9.044v7.319l5.946 5.234V14.44l-5.946-5.397zm-1.099-.08l-5.946 5.398v7.235l5.946-5.234V8.965zm8.136 7.58h1.844V8.349H13.46l6.105 5.54v2.655zm-8.982-8.28H2.59v8.195h1.8v-2.576l6.192-5.62zM5.475 2.476v4.71h5.115l-5.115-4.71zm13.219 0l-5.115 4.71h5.115v-4.71z" fill="#22B8CD" fill-rule="nonzero"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Windsurf</title><path clip-rule="evenodd" d="M23.78 5.004h-.228a2.187 2.187 0 00-2.18 2.196v4.912c0 .98-.804 1.775-1.76 1.775a1.818 1.818 0 01-1.472-.773L13.168 5.95a2.197 2.197 0 00-1.81-.95c-1.134 0-2.154.972-2.154 2.173v4.94c0 .98-.797 1.775-1.76 1.775-.57 0-1.136-.289-1.472-.773L.408 5.098C.282 4.918 0 5.007 0 5.228v4.284c0 .216.066.426.188.604l5.475 7.889c.324.466.8.812 1.351.938 1.377.316 2.645-.754 2.645-2.117V11.89c0-.98.787-1.775 1.76-1.775h.002c.586 0 1.135.288 1.472.773l4.972 7.163a2.15 2.15 0 001.81.95c1.158 0 2.151-.973 2.151-2.173v-4.939c0-.98.787-1.775 1.76-1.775h.194c.122 0 .22-.1.22-.222V5.225a.221.221 0 00-.22-.222z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Zhipu</title><path d="M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z" fill="#3859FF" fill-rule="nonzero"></path></svg>