@co0ontty/wand 4.79.0 → 4.80.0

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.
Files changed (58) hide show
  1. package/README.md +18 -6
  2. package/dist/agent-dispatch.d.ts +3 -0
  3. package/dist/agent-dispatch.js +5 -0
  4. package/dist/ai-team-runner.js +4 -2
  5. package/dist/ai-team-types.d.ts +34 -0
  6. package/dist/ai-team-types.js +12 -1
  7. package/dist/build-info.json +3 -3
  8. package/dist/config.d.ts +4 -3
  9. package/dist/config.js +25 -0
  10. package/dist/git-quick-commit.d.ts +9 -8
  11. package/dist/git-quick-commit.js +123 -8
  12. package/dist/models.d.ts +3 -0
  13. package/dist/models.js +24 -2
  14. package/dist/process-manager.js +16 -3
  15. package/dist/provider-cli-updater.d.ts +1 -1
  16. package/dist/provider-cli-updater.js +12 -1
  17. package/dist/server-ai-team-routes.js +4 -1
  18. package/dist/server-employee-routes.d.ts +22 -0
  19. package/dist/server-employee-routes.js +277 -0
  20. package/dist/server-session-routes.js +38 -13
  21. package/dist/server-settings-routes.js +5 -0
  22. package/dist/server-task-routes.d.ts +2 -0
  23. package/dist/server-task-routes.js +174 -22
  24. package/dist/server-update-routes.js +1 -1
  25. package/dist/server-workspace-routes.js +3 -0
  26. package/dist/server.js +16 -3
  27. package/dist/session-ai-context.d.ts +7 -2
  28. package/dist/session-ai-context.js +36 -9
  29. package/dist/session-provider.d.ts +6 -1
  30. package/dist/session-provider.js +27 -1
  31. package/dist/session-transport.js +4 -0
  32. package/dist/silicon-employee-dispatch.d.ts +9 -0
  33. package/dist/silicon-employee-dispatch.js +13 -0
  34. package/dist/silicon-employee-draft.d.ts +36 -0
  35. package/dist/silicon-employee-draft.js +146 -0
  36. package/dist/storage.d.ts +19 -2
  37. package/dist/storage.js +158 -7
  38. package/dist/structured-gemini-adapter.d.ts +40 -0
  39. package/dist/structured-gemini-adapter.js +198 -0
  40. package/dist/structured-provider-common.d.ts +2 -1
  41. package/dist/structured-provider-common.js +7 -2
  42. package/dist/structured-session-manager.d.ts +10 -0
  43. package/dist/structured-session-manager.js +106 -14
  44. package/dist/system-ai.js +1 -1
  45. package/dist/system-employee.d.ts +35 -0
  46. package/dist/system-employee.js +81 -0
  47. package/dist/task-types.d.ts +8 -1
  48. package/dist/task-types.js +7 -0
  49. package/dist/types.d.ts +23 -4
  50. package/dist/web-ui/content/ai-teams.js +13 -13
  51. package/dist/web-ui/content/scripts.js +64 -64
  52. package/dist/web-ui/content/styles.css +1 -1
  53. package/dist/web-ui/content/tailwind.css +3 -0
  54. package/dist/web-ui/embedded-assets.js +3 -3
  55. package/dist/web-ui/provider-identity.d.ts +2 -1
  56. package/dist/web-ui/provider-identity.js +10 -1
  57. package/dist/web-ui/session-activity.js +1 -1
  58. package/package.json +1 -1
package/dist/models.js CHANGED
@@ -57,6 +57,18 @@ const QODER_FALLBACK_MODELS = [
57
57
  const PI_FALLBACK_MODELS = [
58
58
  { id: "default", label: "跟随 Pi 默认", alias: true },
59
59
  ];
60
+ /**
61
+ * Gemini CLI 没有列模型的子命令,只能给出稳定别名(`packages/core/src/config/models.ts`
62
+ * 里的 GEMINI_MODEL_ALIAS_*)。具体版本名随发布滚动,不适合写死在这里;
63
+ * 设置页的模型输入框是自由文本,用户可以自己填完整模型名。
64
+ */
65
+ const GEMINI_FALLBACK_MODELS = [
66
+ { id: "default", label: "跟随 Gemini 默认", alias: true },
67
+ { id: "auto", label: "Auto(按任务自动选模型)" },
68
+ { id: "pro", label: "Pro" },
69
+ { id: "flash", label: "Flash" },
70
+ { id: "flash-lite", label: "Flash Lite" },
71
+ ];
60
72
  const EFFORT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
61
73
  function effortLevels(ids) {
62
74
  return ids.map((effort) => ({ effort }));
@@ -68,6 +80,7 @@ export const FALLBACK_PROVIDER_THINKING = {
68
80
  grok: effortLevels(["low", "medium", "high", "xhigh"]),
69
81
  qoder: effortLevels(["auto", "none", "low", "medium", "high", "xhigh", "max", "ultracode"]),
70
82
  pi: effortLevels(["minimal", "low", "medium", "high", "xhigh", "max"]),
83
+ gemini: effortLevels(["off"]),
71
84
  };
72
85
  function cloneThinkingEfforts(efforts) {
73
86
  const copy = (levels) => levels.map((level) => ({ ...level }));
@@ -77,6 +90,7 @@ function cloneThinkingEfforts(efforts) {
77
90
  grok: copy(efforts.grok),
78
91
  qoder: copy(efforts.qoder),
79
92
  pi: copy(efforts.pi),
93
+ gemini: copy(efforts.gemini),
80
94
  };
81
95
  }
82
96
  /** Immutable-looking snapshot returned to API clients. */
@@ -88,6 +102,7 @@ const PROVIDER_MODEL_LIST_FIELDS = {
88
102
  grok: "grokModels",
89
103
  qoder: "qoderModels",
90
104
  pi: "piModels",
105
+ gemini: "geminiModels",
91
106
  };
92
107
  /**
93
108
  * 目录里 `default` 项(「不传 --model」那一项)的标签补上服务端配置的默认模型名:
@@ -130,6 +145,7 @@ function cloneCache(cache) {
130
145
  grokModels: cloneModels(cache.grokModels),
131
146
  qoderModels: cloneModels(cache.qoderModels),
132
147
  piModels: cloneModels(cache.piModels),
148
+ geminiModels: cloneModels(cache.geminiModels),
133
149
  thinkingEfforts: cloneThinkingEfforts(cache.thinkingEfforts),
134
150
  claudeVersion: cache.claudeVersion,
135
151
  opencodeVersion: cache.opencodeVersion,
@@ -297,6 +313,7 @@ function createInitialCache(options) {
297
313
  grokModels: cloneModels(GROK_FALLBACK_MODELS),
298
314
  qoderModels: cloneModels(QODER_FALLBACK_MODELS),
299
315
  piModels: cloneModels(PI_FALLBACK_MODELS),
316
+ geminiModels: cloneModels(GEMINI_FALLBACK_MODELS),
300
317
  thinkingEfforts: cloneThinkingEfforts(FALLBACK_PROVIDER_THINKING),
301
318
  claudeVersion: null,
302
319
  opencodeVersion: null,
@@ -831,6 +848,7 @@ function catalogRevision(cache) {
831
848
  grokModels: cache.grokModels,
832
849
  qoderModels: cache.qoderModels,
833
850
  piModels: cache.piModels,
851
+ geminiModels: cache.geminiModels,
834
852
  thinkingEfforts: cache.thinkingEfforts,
835
853
  claudeVersion: cache.claudeVersion,
836
854
  opencodeVersion: cache.opencodeVersion,
@@ -907,7 +925,7 @@ function parsePersistedThinkingEfforts(value) {
907
925
  const fallback = cloneThinkingEfforts(FALLBACK_PROVIDER_THINKING);
908
926
  if (!isRecord(value))
909
927
  return fallback;
910
- const providers = ["claude", "opencode", "grok", "qoder", "pi"];
928
+ const providers = ["claude", "opencode", "grok", "qoder", "pi", "gemini"];
911
929
  for (const provider of providers) {
912
930
  const levels = parsePersistedEffortLevels(value[provider]);
913
931
  if (levels)
@@ -942,8 +960,9 @@ function parsePersistedModelCatalog(value) {
942
960
  const grokModels = parsePersistedModelList(catalog.grokModels);
943
961
  const qoderModels = parsePersistedModelList(catalog.qoderModels);
944
962
  const piModels = parsePersistedModelList(catalog.piModels);
963
+ const geminiModels = parsePersistedModelList(catalog.geminiModels);
945
964
  const refreshedAt = safePersistedString(catalog.refreshedAt, 64);
946
- if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels || !piModels
965
+ if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels || !piModels || !geminiModels
947
966
  || !refreshedAt || Number.isNaN(Date.parse(refreshedAt))) {
948
967
  return null;
949
968
  }
@@ -959,6 +978,7 @@ function parsePersistedModelCatalog(value) {
959
978
  grokModels,
960
979
  qoderModels,
961
980
  piModels,
981
+ geminiModels,
962
982
  thinkingEfforts: parsePersistedThinkingEfforts(catalog.thinkingEfforts),
963
983
  claudeVersion,
964
984
  opencodeVersion,
@@ -1043,6 +1063,7 @@ async function discoverModelCache(options, previous) {
1043
1063
  grokModels: grokProbe.ok ? grokProbe.value : cloneModels(previous.grokModels),
1044
1064
  qoderModels: qoderProbe.ok ? qoderProbe.value : cloneModels(previous.qoderModels),
1045
1065
  piModels: piProbe.ok ? piProbe.value : cloneModels(previous.piModels),
1066
+ geminiModels: cloneModels(previous.geminiModels),
1046
1067
  thinkingEfforts: {
1047
1068
  claude: claudeEffortProbe.ok ? claudeEffortProbe.value : cloneThinkingEfforts(previous.thinkingEfforts).claude,
1048
1069
  opencode: opencodeProbe.models.ok && unionModelEfforts(opencodeProbe.models.value).length
@@ -1055,6 +1076,7 @@ async function discoverModelCache(options, previous) {
1055
1076
  : piEffortProbe.ok
1056
1077
  ? piEffortProbe.value
1057
1078
  : cloneThinkingEfforts(previous.thinkingEfforts).pi,
1079
+ gemini: cloneThinkingEfforts(previous.thinkingEfforts).gemini,
1058
1080
  },
1059
1081
  claudeVersion,
1060
1082
  opencodeVersion: opencodeProbe.version.ok ? opencodeProbe.version.value : previous.opencodeVersion,
@@ -1046,10 +1046,10 @@ export class ProcessManager extends EventEmitter {
1046
1046
  ? getProviderCommandSessionId(provider, processedCommand)
1047
1047
  ?? getProviderCommandSessionId(provider, command)
1048
1048
  : null;
1049
- // Grok and Qoder accept caller-selected IDs for new conversations. Assigning
1049
+ // Grok / Qoder / Gemini accept caller-selected IDs for new conversations. Assigning
1050
1050
  // one up front avoids depending on provider-specific TUI rendering to learn
1051
1051
  // the durable ID later.
1052
- const assignedProviderSessionId = !existingProviderSessionId && (provider === "grok" || provider === "qoder")
1052
+ const assignedProviderSessionId = !existingProviderSessionId && (provider === "grok" || provider === "qoder" || provider === "gemini")
1053
1053
  ? randomUUID()
1054
1054
  : null;
1055
1055
  if (assignedProviderSessionId) {
@@ -2026,7 +2026,7 @@ export class ProcessManager extends EventEmitter {
2026
2026
  input: prompt,
2027
2027
  cwd: record.cwd,
2028
2028
  language: this.config.language,
2029
- ai: resolveSystemAiContext(record, this.config),
2029
+ ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
2030
2030
  onGenerating: (generating) => {
2031
2031
  if (!this.disposed)
2032
2032
  this.setSessionTopicGenerating(id, generating);
@@ -2528,6 +2528,19 @@ export class ProcessManager extends EventEmitter {
2528
2528
  result += ` --thinking '${level}'`;
2529
2529
  return result;
2530
2530
  }
2531
+ if (provider === "gemini") {
2532
+ let result = command;
2533
+ const trimmedModel = model?.trim();
2534
+ if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
2535
+ result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2536
+ }
2537
+ // Gemini CLI 没有 thinking 档位开关。approval mode 与其它 provider 的
2538
+ // auto-approve 语义对齐,default 下让 TUI 自己弹确认。
2539
+ if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--approval-mode(?:\s|=)/.test(result) && !/(?:^|\s)-y(?:\s|$)/.test(result)) {
2540
+ result += " --approval-mode yolo";
2541
+ }
2542
+ return result;
2543
+ }
2531
2544
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2532
2545
  if (!isClaudeCmd)
2533
2546
  return command;
@@ -1,4 +1,4 @@
1
- export type ProviderCliId = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
1
+ export type ProviderCliId = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi" | "gemini";
2
2
  export interface ProviderCliUpdateStatus {
3
3
  id: ProviderCliId;
4
4
  label: string;
@@ -63,6 +63,14 @@ const PROVIDER_CLI_SPECS = [
63
63
  versionArgs: ["--version"],
64
64
  updateArgs: ["update", "self"],
65
65
  },
66
+ {
67
+ id: "gemini",
68
+ label: "Gemini CLI",
69
+ command: "gemini",
70
+ npmPackage: "@google/gemini-cli",
71
+ versionArgs: ["--version"],
72
+ // 没有 `gemini update` 子命令,只能走 npm 发布渠道。
73
+ },
66
74
  ];
67
75
  async function runCommand(command, args, timeout, options) {
68
76
  const { stdout, stderr } = await execFileAsync(command, args, {
@@ -327,7 +335,10 @@ export async function updateProviderClis(statuses, ids, options = {}) {
327
335
  }
328
336
  options.onLog?.(`[CLI Update] ${spec.label}: ${status.currentVersion} -> ${status.latestVersion}`);
329
337
  try {
330
- const output = await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
338
+ // 没有自更新子命令的 CLI(Gemini)走它的 npm 发布渠道。
339
+ const output = spec.updateArgs
340
+ ? await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options)
341
+ : await runCommand(options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm"), ["install", "-g", `${spec.npmPackage}@latest`], options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
331
342
  const combined = trimOutput([output.stdout, output.stderr].filter(Boolean).join("\n"));
332
343
  results.push({
333
344
  id: spec.id,
@@ -221,6 +221,7 @@ export function registerAiTeamRoutes(app, deps) {
221
221
  labels: [TEAM_DIRECT_LABEL],
222
222
  milestoneId: scopedMilestoneId(storage, milestoneId, workspace.id) ?? milestoneId,
223
223
  agent,
224
+ executionSubject: { type: "team", id: team.id },
224
225
  });
225
226
  try {
226
227
  const detail = await runner.start({ teamId: team.id, taskId: task.id, note });
@@ -255,7 +256,9 @@ export function registerAiTeamRoutes(app, deps) {
255
256
  const teamId = text(body.teamId);
256
257
  if (!teamId)
257
258
  throw new Error("请选择团队。");
258
- res.status(202).json(await runner.start({ teamId, taskId: req.params.id, note: text(body.note) }));
259
+ const detail = await runner.start({ teamId, taskId: req.params.id, note: text(body.note) });
260
+ storage.updateWandTask(req.params.id, { executionSubject: { type: "team", id: teamId } });
261
+ res.status(202).json(detail);
259
262
  }
260
263
  catch (error) {
261
264
  sendTeamError(res, error);
@@ -0,0 +1,22 @@
1
+ import type { Express } from "express";
2
+ import { type SiliconEmployee, type SiliconEmployeeDraft } from "./ai-team-types.js";
3
+ import type { WandStorage } from "./storage.js";
4
+ import type { WandTaskAgent } from "./task-types.js";
5
+ import type { WandConfig } from "./types.js";
6
+ export interface EmployeeDraftRequest {
7
+ expectation: string;
8
+ existingNames: string[];
9
+ }
10
+ /**
11
+ * 内置员工只接受 agents;其余字段即使前端遗漏地发上来也必须与服务端一致,
12
+ * 不一致直接拒绝(静默忽略会让客户端以为改成功了)。
13
+ */
14
+ export declare function parseSystemEmployeeAgents(value: unknown, existing: SiliconEmployee): WandTaskAgent[];
15
+ export declare function parseSiliconEmployeeInput(value: unknown, existing: SiliconEmployee | null, now: string): SiliconEmployee;
16
+ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
17
+ storage: WandStorage;
18
+ notifyEmployeeChanged?: (employeeId: string) => void;
19
+ config?: WandConfig;
20
+ /** 测试注入点:默认按系统 AI 配置真实调用模型。 */
21
+ generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
22
+ }): void;
@@ -0,0 +1,277 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, } from "./ai-team-types.js";
3
+ import { getErrorMessage } from "./error-utils.js";
4
+ import { asyncRoute } from "./express-async.js";
5
+ import { bodyObject, sendRouteError, text } from "./server-request.js";
6
+ import { parseTaskAgent } from "./server-task-routes.js";
7
+ import { resolveSystemAiContext } from "./session-ai-context.js";
8
+ import { generateSiliconEmployeeDraft } from "./silicon-employee-draft.js";
9
+ import { isSystemSiliconEmployee } from "./system-employee.js";
10
+ /** 员工起草没有会话上下文,按「默认 provider」解析,与任务标题 / 提示词优化一致。 */
11
+ function employeeDraftAiOptions(storage, config) {
12
+ if (!config)
13
+ return {};
14
+ const provider = config.defaultProvider ?? "claude";
15
+ return resolveSystemAiContext({
16
+ provider,
17
+ structuredState: undefined,
18
+ runner: undefined,
19
+ command: provider === "qoder" ? "qodercli" : provider,
20
+ selectedModel: null,
21
+ thinkingEffort: config.defaultThinkingEffort,
22
+ }, config, storage.getSystemSiliconEmployee());
23
+ }
24
+ function sendEmployeeError(res, error) {
25
+ const message = getErrorMessage(error, "硅基员工操作失败。");
26
+ const status = /不存在/.test(message) ? 404 : 400;
27
+ sendRouteError(res, error, "硅基员工操作失败。", status);
28
+ }
29
+ /** 内置员工是 Wand 自己的执行者,不能被归档或删除。 */
30
+ function rejectSystemEmployeeMutation(existing, action) {
31
+ if (!isSystemSiliconEmployee(existing))
32
+ return;
33
+ throw new Error(`「${existing.name}」是 Wand 内置的系统运维员工,不能${action}。`);
34
+ }
35
+ function boundedText(value, label, min, max) {
36
+ const result = text(value);
37
+ if (result.length < min)
38
+ throw new Error(`${label}不能为空。`);
39
+ if (result.length > max)
40
+ throw new Error(`${label}不能超过 ${max} 个字符。`);
41
+ return result;
42
+ }
43
+ function parseAvatar(value, name) {
44
+ const avatar = text(value);
45
+ if (!avatar || /^cat:\d{1,2}$/.test(avatar))
46
+ return avatar;
47
+ if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar)) {
48
+ throw new Error(`员工「${name}」的头像格式无效。`);
49
+ }
50
+ if (avatar.length > SILICON_EMPLOYEE_AVATAR_MAX_CHARS) {
51
+ throw new Error(`员工「${name}」的头像太大。`);
52
+ }
53
+ return avatar;
54
+ }
55
+ /** 内置员工的锁定字段:名字 / 职责 / 人设 / 头像写死在服务端,只有候选可改。 */
56
+ const SYSTEM_EMPLOYEE_LOCKED_FIELDS = [
57
+ { key: "name", label: "名字" },
58
+ { key: "duty", label: "职责" },
59
+ { key: "prompt", label: "角色设定" },
60
+ { key: "avatar", label: "头像" },
61
+ ];
62
+ /**
63
+ * 内置员工只接受 agents;其余字段即使前端遗漏地发上来也必须与服务端一致,
64
+ * 不一致直接拒绝(静默忽略会让客户端以为改成功了)。
65
+ */
66
+ export function parseSystemEmployeeAgents(value, existing) {
67
+ const body = bodyObject(value);
68
+ for (const field of SYSTEM_EMPLOYEE_LOCKED_FIELDS) {
69
+ const incoming = body[field.key];
70
+ if (incoming === undefined || incoming === null)
71
+ continue;
72
+ const current = existing[field.key];
73
+ const isSame = typeof incoming === "string" && incoming === current;
74
+ if (!isSame) {
75
+ throw new Error(`系统运维员工是内置的,${field.label}不可修改。`);
76
+ }
77
+ }
78
+ if (!Array.isArray(body.agents))
79
+ throw new Error("请提供执行候选数组。");
80
+ if (body.agents.length === 0)
81
+ throw new Error("至少需要一个执行候选。");
82
+ if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
83
+ throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
84
+ }
85
+ const agents = body.agents.map((rawAgent) => {
86
+ const parsed = parseTaskAgent(rawAgent ?? {});
87
+ if (!parsed)
88
+ throw new Error("请选择有效的 CLI 工具。");
89
+ if (parsed.kind !== "structured")
90
+ throw new Error("硅基员工只支持结构化会话。");
91
+ return parsed;
92
+ });
93
+ if (new Set(agents.map(agentKey)).size !== agents.length) {
94
+ throw new Error("执行候选存在重复。");
95
+ }
96
+ return agents;
97
+ }
98
+ export function parseSiliconEmployeeInput(value, existing, now) {
99
+ const body = bodyObject(value);
100
+ const name = boundedText(body.name, "员工名字", 1, 40);
101
+ const duty = text(body.duty);
102
+ if (duty.length > 2000)
103
+ throw new Error("员工职责不能超过 2000 个字符。");
104
+ const prompt = text(body.prompt);
105
+ if (prompt.length > 20_000)
106
+ throw new Error("员工设定 Prompt 不能超过 20000 个字符。");
107
+ let agents;
108
+ if (Array.isArray(body.agents)) {
109
+ if (body.agents.length === 0)
110
+ throw new Error("至少需要一个执行候选。");
111
+ if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
112
+ throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
113
+ }
114
+ agents = body.agents.map((rawAgent) => {
115
+ const parsed = parseTaskAgent(rawAgent ?? {});
116
+ if (!parsed)
117
+ throw new Error("请选择有效的 CLI 工具。");
118
+ if (parsed.kind !== "structured")
119
+ throw new Error("硅基员工只支持结构化会话。");
120
+ return parsed;
121
+ });
122
+ }
123
+ else if (body.agents !== undefined && body.agents !== null) {
124
+ throw new Error("候选执行配置必须是数组。");
125
+ }
126
+ else if (body.agent !== undefined && body.agent !== null) {
127
+ const singleAgent = parseTaskAgent(body.agent);
128
+ if (!singleAgent)
129
+ throw new Error("请选择有效的 CLI 工具。");
130
+ if (singleAgent.kind !== "structured")
131
+ throw new Error("硅基员工只支持结构化会话。");
132
+ agents = [singleAgent];
133
+ }
134
+ else {
135
+ throw new Error("至少需要一个执行候选。");
136
+ }
137
+ const seenKeys = new Set(agents.map(agentKey));
138
+ if (seenKeys.size !== agents.length) {
139
+ throw new Error("执行候选存在重复。");
140
+ }
141
+ const avatar = parseAvatar(body.avatar, name);
142
+ const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
143
+ return {
144
+ id,
145
+ name,
146
+ duty,
147
+ prompt,
148
+ avatar,
149
+ agents,
150
+ archivedAt: existing?.archivedAt,
151
+ createdAt: existing ? existing.createdAt : now,
152
+ updatedAt: now,
153
+ };
154
+ }
155
+ export function registerSiliconEmployeeRoutes(app, deps) {
156
+ const { storage, notifyEmployeeChanged, config } = deps;
157
+ const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config), {
158
+ cwd: config?.defaultCwd || process.cwd(),
159
+ language: config?.language ?? "",
160
+ existingNames: request.existingNames,
161
+ preferredProvider: config?.defaultProvider ?? "claude",
162
+ }));
163
+ const notifyEmployee = (employeeId) => {
164
+ try {
165
+ notifyEmployeeChanged?.(employeeId);
166
+ }
167
+ catch (error) {
168
+ console.error("[SiliconEmployee] definition notification failed:", getErrorMessage(error));
169
+ }
170
+ };
171
+ app.get("/api/silicon-employees", (req, res) => {
172
+ try {
173
+ const includeArchived = req.query.includeArchived === "true" || req.query.includeArchived === "1";
174
+ const employees = storage.listSiliconEmployees({ includeArchived });
175
+ res.json({ employees });
176
+ }
177
+ catch (error) {
178
+ sendEmployeeError(res, error);
179
+ }
180
+ });
181
+ app.post("/api/silicon-employees/draft", asyncRoute(async (req, res) => {
182
+ try {
183
+ const body = bodyObject(req.body);
184
+ const expectation = text(body.expectation);
185
+ const existingNames = storage.listSiliconEmployees({ includeArchived: true })
186
+ .map((employee) => employee.name);
187
+ const draft = await generateDraft({ expectation, existingNames });
188
+ res.json({ draft });
189
+ }
190
+ catch (error) {
191
+ sendEmployeeError(res, error);
192
+ }
193
+ }));
194
+ app.post("/api/silicon-employees", (req, res) => {
195
+ try {
196
+ const now = new Date().toISOString();
197
+ const employee = parseSiliconEmployeeInput(req.body, null, now);
198
+ storage.saveSiliconEmployee(employee);
199
+ notifyEmployee(employee.id);
200
+ res.status(201).json(employee);
201
+ }
202
+ catch (error) {
203
+ sendEmployeeError(res, error);
204
+ }
205
+ });
206
+ app.get("/api/silicon-employees/:id", (req, res) => {
207
+ try {
208
+ const employee = storage.getSiliconEmployee(req.params.id);
209
+ if (!employee)
210
+ throw new Error(`硅基员工「${req.params.id}」不存在。`);
211
+ res.json(employee);
212
+ }
213
+ catch (error) {
214
+ sendEmployeeError(res, error);
215
+ }
216
+ });
217
+ app.put("/api/silicon-employees/:id", (req, res) => {
218
+ try {
219
+ const existing = storage.getSiliconEmployee(req.params.id);
220
+ if (!existing)
221
+ throw new Error(`硅基员工「${req.params.id}」不存在。`);
222
+ const now = new Date().toISOString();
223
+ const employee = isSystemSiliconEmployee(existing)
224
+ ? { ...existing, agents: parseSystemEmployeeAgents(req.body, existing), updatedAt: now }
225
+ : parseSiliconEmployeeInput(req.body, existing, now);
226
+ storage.saveSiliconEmployee(employee);
227
+ notifyEmployee(employee.id);
228
+ res.json(employee);
229
+ }
230
+ catch (error) {
231
+ sendEmployeeError(res, error);
232
+ }
233
+ });
234
+ app.post("/api/silicon-employees/:id/archive", (req, res) => {
235
+ try {
236
+ const existing = storage.getSiliconEmployee(req.params.id);
237
+ if (!existing)
238
+ throw new Error(`硅基员工「${req.params.id}」不存在。`);
239
+ rejectSystemEmployeeMutation(existing, "归档");
240
+ const now = new Date().toISOString();
241
+ storage.archiveSiliconEmployee(req.params.id, now);
242
+ notifyEmployee(req.params.id);
243
+ res.json({ ok: true, archivedAt: now });
244
+ }
245
+ catch (error) {
246
+ sendEmployeeError(res, error);
247
+ }
248
+ });
249
+ app.post("/api/silicon-employees/:id/unarchive", (req, res) => {
250
+ try {
251
+ const existing = storage.getSiliconEmployee(req.params.id);
252
+ if (!existing)
253
+ throw new Error(`硅基员工「${req.params.id}」不存在。`);
254
+ rejectSystemEmployeeMutation(existing, "恢复");
255
+ storage.unarchiveSiliconEmployee(req.params.id);
256
+ notifyEmployee(req.params.id);
257
+ res.json({ ok: true });
258
+ }
259
+ catch (error) {
260
+ sendEmployeeError(res, error);
261
+ }
262
+ });
263
+ app.delete("/api/silicon-employees/:id", (req, res) => {
264
+ try {
265
+ const existing = storage.getSiliconEmployee(req.params.id);
266
+ if (!existing)
267
+ throw new Error(`硅基员工「${req.params.id}」不存在。`);
268
+ rejectSystemEmployeeMutation(existing, "删除");
269
+ storage.deleteSiliconEmployee(req.params.id);
270
+ notifyEmployee(req.params.id);
271
+ res.status(204).end();
272
+ }
273
+ catch (error) {
274
+ sendEmployeeError(res, error);
275
+ }
276
+ });
277
+ }
@@ -11,6 +11,7 @@ import { getErrorMessage } from "./error-utils.js";
11
11
  import { archiveCommitTasks } from "./commit-task-archive.js";
12
12
  import { buildIterationCommitContext, COMMIT_CONTEXT_MODE_PREF_KEY, isCommitContextMode, readCommitContextMode, resolveCommitContextInput, } from "./iteration-log.js";
13
13
  import { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
14
+ import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
14
15
  import { buildProviderResumeCommand, isProviderSessionId } from "./resume-policy.js";
15
16
  import { parseBoundedInteger } from "./request-limits.js";
16
17
  import { asyncRoute } from "./express-async.js";
@@ -457,30 +458,54 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
457
458
  app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
458
459
  const body = req.body;
459
460
  try {
460
- if (body.provider && !isSessionProvider(body.provider)) {
461
- res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder 或 Pi provider。" });
461
+ if (body.teamId !== undefined || body.subject?.type === "team") {
462
+ throw new Error("AI 团队请通过团队开工入口创建群聊。");
463
+ }
464
+ if (body.subject?.type === "employee")
465
+ throw new Error("员工会话请传 employeeId。");
466
+ const employeeId = body.employeeId === undefined ? "" : typeof body.employeeId === "string" ? body.employeeId.trim() : null;
467
+ if (employeeId === null || (body.employeeId !== undefined && !employeeId)) {
468
+ throw new Error("employeeId 必须是员工 ID。");
469
+ }
470
+ if (employeeId && (body.kind === "pty" || body.runner === "pty")) {
471
+ throw new Error("硅基员工只支持结构化会话。");
472
+ }
473
+ const employee = employeeId ? storage.getSiliconEmployee(employeeId) : null;
474
+ if (employeeId && (!employee || employee.archivedAt))
475
+ throw new Error("硅基员工不存在或已归档。");
476
+ const selectedEmployeeCandidate = employee ? selectEmployeeCandidate(employee) : null;
477
+ const employeeAgent = selectedEmployeeCandidate?.agent;
478
+ if (!employee && body.provider && !isSessionProvider(body.provider)) {
479
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder、Pi 或 Gemini provider。" });
462
480
  return;
463
481
  }
464
- const provider = isSessionProvider(body.provider) ? body.provider : "claude";
482
+ const provider = employeeAgent?.provider ?? (isSessionProvider(body.provider) ? body.provider : "claude");
465
483
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
466
484
  const origin = parseSessionCreationOrigin(body);
467
485
  const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
468
486
  const snapshot = structured.createSession({
469
487
  cwd,
470
- mode: parseExecutionMode(body.mode, defaultMode),
488
+ mode: employeeAgent?.mode ?? parseExecutionMode(body.mode, defaultMode),
471
489
  provider,
472
490
  // Omit runner to let StructuredSessionManager apply the configured
473
491
  // Claude default; explicit values are validated against the provider.
474
- runner: body.runner,
492
+ runner: employee ? undefined : body.runner,
475
493
  worktreeEnabled: body.worktreeEnabled === true,
476
- model: rawModel || getDefaultModelForProvider(config, provider) || undefined,
477
- thinkingEffort: typeof body.thinkingEffort === "string"
494
+ model: employeeAgent
495
+ ? employeeAgent.model === "default" ? getDefaultModelForProvider(config, provider) || undefined : employeeAgent.model
496
+ : rawModel || getDefaultModelForProvider(config, provider) || undefined,
497
+ thinkingEffort: employeeAgent?.thinkingEffort ?? (typeof body.thinkingEffort === "string"
478
498
  ? body.thinkingEffort
479
- : config.defaultThinkingEffort,
499
+ : config.defaultThinkingEffort),
480
500
  workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
481
501
  workspaceTaskId: body.workspaceTaskId,
482
502
  // 角色与规则走系统提示通道,不拼进 prompt(见 AGENTS.md「Session 输入契约」)。
483
- systemPrompt: body.systemPrompt,
503
+ systemPrompt: employee?.prompt ?? body.systemPrompt,
504
+ employeeId: employee?.id,
505
+ employeeName: employee?.name,
506
+ employeeAvatar: employee?.avatar,
507
+ employeeCandidates: employee?.agents,
508
+ employeeCandidateIndex: selectedEmployeeCandidate?.index,
484
509
  ...origin,
485
510
  });
486
511
  onSessionCreated?.(snapshot.cwd);
@@ -504,7 +529,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
504
529
  return;
505
530
  }
506
531
  const finished = await structured.sendMessage(snapshot.id, prompt);
507
- res.status(201).json(finished);
532
+ res.status(201).json(sessionResponseDTO(finished));
508
533
  return;
509
534
  }
510
535
  res.status(201).json(sessionResponseDTO(snapshot));
@@ -862,7 +887,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
862
887
  }
863
888
  const body = (req.body ?? {});
864
889
  try {
865
- const ai = resolveCommitAiContext(snapshot, config);
890
+ const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
866
891
  // 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
867
892
  const context = await iterationCommitInput(storage, snapshot, body);
868
893
  const result = await runQuickCommitWithFallback({
@@ -958,7 +983,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
958
983
  }
959
984
  const body = (req.body ?? {});
960
985
  try {
961
- const ai = resolveCommitAiContext(snapshot, config);
986
+ const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
962
987
  const context = await iterationCommitInput(storage, snapshot, body);
963
988
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
964
989
  ...ai,
@@ -995,7 +1020,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
995
1020
  }
996
1021
  const body = (req.body ?? {});
997
1022
  try {
998
- const ai = resolveCommitAiContext(snapshot, config);
1023
+ const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
999
1024
  const result = await runTagHead({
1000
1025
  cwd: snapshot.cwd,
1001
1026
  language: config.language ?? "",
@@ -29,6 +29,7 @@ function publicConfig(config) {
29
29
  defaultGrokModel: defaultModels.grok,
30
30
  defaultQoderModel: defaultModels.qoder,
31
31
  defaultPiModel: defaultModels.pi,
32
+ defaultGeminiModel: defaultModels.gemini,
32
33
  defaultModels,
33
34
  };
34
35
  }
@@ -292,6 +293,8 @@ export function registerSettingsRoutes(app, deps) {
292
293
  stagePreference("defaultQoderModel", body.defaultModels.qoder);
293
294
  if (Object.hasOwn(body.defaultModels, "pi"))
294
295
  stagePreference("defaultPiModel", body.defaultModels.pi);
296
+ if (Object.hasOwn(body.defaultModels, "gemini"))
297
+ stagePreference("defaultGeminiModel", body.defaultModels.gemini);
295
298
  }
296
299
  if (body.systemAi !== undefined) {
297
300
  if (!body.systemAi || typeof body.systemAi !== "object" || Array.isArray(body.systemAi)) {
@@ -407,6 +410,7 @@ export function registerSettingsRoutes(app, deps) {
407
410
  defaultGrokModel: defaults.grok,
408
411
  defaultQoderModel: defaults.qoder,
409
412
  defaultPiModel: defaults.pi,
413
+ defaultGeminiModel: defaults.gemini,
410
414
  defaultModels: defaults,
411
415
  });
412
416
  });
@@ -422,6 +426,7 @@ export function registerSettingsRoutes(app, deps) {
422
426
  defaultGrokModel: defaults.grok,
423
427
  defaultQoderModel: defaults.qoder,
424
428
  defaultPiModel: defaults.pi,
429
+ defaultGeminiModel: defaults.gemini,
425
430
  defaultModels: defaults,
426
431
  });
427
432
  }
@@ -4,6 +4,7 @@ import { generateWandTaskTitle } from "./task-title.js";
4
4
  import type { SessionRegistry } from "./session-registry.js";
5
5
  import type { StructuredSessionManager } from "./structured-session-manager.js";
6
6
  import type { ProcessManager } from "./process-manager.js";
7
+ import type { AiTeamRunner } from "./ai-team-runner.js";
7
8
  import type { WandTaskAgent, WandTaskAgentKind, WandTaskAgentMode } from "./task-types.js";
8
9
  import type { WandConfig } from "./types.js";
9
10
  /** 任务面板上次选用的 CLI 工具 / 模型 / 思考深度;从未保存过时回落到 Claude 默认。 */
@@ -16,6 +17,7 @@ export interface TaskRouteDependencies {
16
17
  /** PTY 会话创建入口;派发 `kind: "pty"` 的任务时使用。测试里可换成同步桩。 */
17
18
  processes?: ProcessManager;
18
19
  config?: WandConfig;
20
+ aiTeams?: AiTeamRunner;
19
21
  /** 可注入的任务标题生成器;测试里换成同步桩,避免真的起 CLI。 */
20
22
  generateTitle?: typeof generateWandTaskTitle;
21
23
  }