@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
@@ -1,4 +1,4 @@
1
- import { dispatchAgentForTask } from "./agent-dispatch.js";
1
+ import { dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
2
2
  import { asyncRoute } from "./express-async.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
4
  import { bodyObject, sendRouteError, text } from "./server-request.js";
@@ -7,6 +7,8 @@ import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId
7
7
  import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
8
8
  import { recordIterationPromptForTask } from "./iteration-log.js";
9
9
  import { resolveSystemAiContext } from "./session-ai-context.js";
10
+ import { memberAgents } from "./ai-team-types.js";
11
+ import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
10
12
  import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
11
13
  import { isAutoNameableBoardTask, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
12
14
  import { isSessionProvider } from "./session-provider.js";
@@ -38,6 +40,48 @@ export function writeTaskBoardLastAgent(storage, agent) {
38
40
  kind: agent.kind,
39
41
  });
40
42
  }
43
+ function parseExecutionSubject(storage, value) {
44
+ if (value === null)
45
+ return null;
46
+ const body = bodyObject(value);
47
+ const type = text(body.type);
48
+ const id = text(body.id);
49
+ if (!id)
50
+ throw new Error("请选择执行对象。");
51
+ if (type === "employee") {
52
+ const employee = storage.getSiliconEmployee(id);
53
+ if (!employee || employee.archivedAt)
54
+ throw new Error("硅基员工不存在或已归档。");
55
+ return { type, id };
56
+ }
57
+ if (type === "team") {
58
+ if (!storage.getAiTeam(id))
59
+ throw new Error("AI 团队不存在。");
60
+ return { type, id };
61
+ }
62
+ if (type === "cli") {
63
+ if (!isSessionProvider(id))
64
+ throw new Error("请选择有效的 CLI 工具。");
65
+ return { type, id };
66
+ }
67
+ throw new Error("执行对象类型无效。");
68
+ }
69
+ function preferredAgentForSubject(storage, subject) {
70
+ if (subject.type === "employee")
71
+ return storage.getSiliconEmployee(subject.id)?.agents[0] ?? null;
72
+ if (subject.type === "team") {
73
+ const team = storage.getAiTeam(subject.id);
74
+ const member = team?.members.find((entry) => entry.isLeader) ?? team?.members[0];
75
+ return member ? memberAgents(member)[0] ?? null : null;
76
+ }
77
+ return null;
78
+ }
79
+ function cliAgentForSubject(subject, current) {
80
+ return parseTaskAgent({
81
+ ...(current?.provider === subject.id ? current : defaultTaskBoardAgent()),
82
+ provider: subject.id,
83
+ });
84
+ }
41
85
  function dateValue(value) {
42
86
  if (value === undefined)
43
87
  return undefined;
@@ -178,6 +222,9 @@ function taskSessionSummary(session) {
178
222
  thinkingEffort: session.thinkingEffort || "off",
179
223
  // 会话实际跑的执行模式;旧会话没有该字段时留空,前端回落到任务上的配置。
180
224
  mode: session.mode || "",
225
+ employeeId: session.employeeId,
226
+ employeeName: session.employeeName,
227
+ employeeAvatar: session.employeeAvatar,
181
228
  };
182
229
  }
183
230
  /**
@@ -191,7 +238,7 @@ function scheduleWandTaskTitleGeneration(storage, taskId, source, signature, opt
191
238
  const cwd = options.cwd || options.config?.defaultCwd || process.cwd();
192
239
  const run = async () => {
193
240
  try {
194
- const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config));
241
+ const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config, storage));
195
242
  const current = storage.getWandTask(taskId);
196
243
  // 用户已经自己写了标题(原生端 / 面板编辑)就不要覆盖。
197
244
  if (!current || current.titleSource !== "auto")
@@ -249,7 +296,7 @@ export function refreshAutoBoardTaskTitles(storage, options = {}) {
249
296
  }
250
297
  }
251
298
  /** 自动生成标题没有会话上下文,按「默认 provider + 默认模型」解析,与提示词优化一致。 */
252
- function taskTitleAiOptions(config) {
299
+ function taskTitleAiOptions(config, storage) {
253
300
  if (!config)
254
301
  return {};
255
302
  const provider = config.defaultProvider ?? "claude";
@@ -261,11 +308,11 @@ function taskTitleAiOptions(config) {
261
308
  selectedModel: null,
262
309
  thinkingEffort: config.defaultThinkingEffort,
263
310
  };
264
- // resolveSystemAiContext 会在直连 API 可用且已启用时优先走 API,否则回落到 CLI。
265
- return resolveSystemAiContext(defaultSession, config);
311
+ // resolveSystemAiContext 会在直连 API 可用且已启用时优先走 API,否则走系统运维员工的 CLI 候选链。
312
+ return resolveSystemAiContext(defaultSession, config, storage?.getSystemSiliconEmployee() ?? null);
266
313
  }
267
314
  export function registerTaskRoutes(app, deps) {
268
- const { storage, sessions, structured, processes, config } = deps;
315
+ const { storage, sessions, structured, processes, config, aiTeams } = deps;
269
316
  const dto = (task) => taskDto(deps, task);
270
317
  app.get("/api/wand-tasks", (req, res) => {
271
318
  try {
@@ -392,8 +439,18 @@ export function registerTaskRoutes(app, deps) {
392
439
  const status = STATUSES.has(body.status) ? body.status : "todo";
393
440
  const priority = PRIORITIES.has(body.priority) ? body.priority : DEFAULT_WAND_TASK_PRIORITY;
394
441
  const agent = body.agent === undefined ? null : parseTaskAgent(body.agent);
395
- if (agent)
396
- writeTaskBoardLastAgent(storage, agent);
442
+ const executionSubject = body.executionSubject === undefined
443
+ ? agent ? { type: "cli", id: agent.provider } : null
444
+ : parseExecutionSubject(storage, body.executionSubject);
445
+ if (executionSubject?.type === "cli" && agent && agent.provider !== executionSubject.id) {
446
+ throw new Error("CLI 执行对象与工具配置不一致。");
447
+ }
448
+ if (executionSubject?.type !== "cli" && executionSubject && agent?.kind === "pty") {
449
+ throw new Error("PTY 只能选择 CLI 工具。");
450
+ }
451
+ const assignedAgent = executionSubject && executionSubject.type !== "cli"
452
+ ? preferredAgentForSubject(storage, executionSubject)
453
+ : executionSubject?.type === "cli" ? agent ?? cliAgentForSubject(executionSubject) : agent;
397
454
  const labels = labelsFrom(body.labels);
398
455
  const dueDate = dateValue(body.dueDate) ?? null;
399
456
  // 没选迭代就落到默认迭代:每个任务都属于一个迭代。
@@ -411,8 +468,11 @@ export function registerTaskRoutes(app, deps) {
411
468
  labels,
412
469
  dueDate,
413
470
  milestoneId,
414
- agent,
471
+ agent: assignedAgent,
472
+ executionSubject,
415
473
  });
474
+ if (agent)
475
+ writeTaskBoardLastAgent(storage, agent);
416
476
  if (titleSource === "auto") {
417
477
  refreshAutoBoardTaskTitles(storage, {
418
478
  cwd: workspaceId ? storage.getWorkspace(workspaceId)?.cwd : undefined,
@@ -484,8 +544,24 @@ export function registerTaskRoutes(app, deps) {
484
544
  if (body.agent !== undefined) {
485
545
  // 老客户端不传 mode / kind:沿用任务当前值,而不是复位成标准 / 结构化。
486
546
  patch.agent = parseTaskAgent(body.agent, currentTask.agent?.mode, currentTask.agent?.kind);
487
- if (patch.agent)
488
- writeTaskBoardLastAgent(storage, patch.agent);
547
+ if (body.executionSubject === undefined && patch.agent) {
548
+ patch.executionSubject = { type: "cli", id: patch.agent.provider };
549
+ }
550
+ }
551
+ if (body.executionSubject !== undefined) {
552
+ patch.executionSubject = parseExecutionSubject(storage, body.executionSubject);
553
+ if (patch.executionSubject?.type === "cli" && patch.agent
554
+ && patch.agent.provider !== patch.executionSubject.id) {
555
+ throw new Error("CLI 执行对象与工具配置不一致。");
556
+ }
557
+ if (patch.executionSubject?.type === "cli" && !patch.agent) {
558
+ patch.agent = cliAgentForSubject(patch.executionSubject, currentTask.agent);
559
+ }
560
+ if (patch.executionSubject && patch.executionSubject.type !== "cli") {
561
+ if (patch.agent?.kind === "pty")
562
+ throw new Error("PTY 只能选择 CLI 工具。");
563
+ patch.agent = preferredAgentForSubject(storage, patch.executionSubject);
564
+ }
489
565
  }
490
566
  if (body.sortOrder !== undefined && Number.isFinite(Number(body.sortOrder))) {
491
567
  patch.sortOrder = Math.floor(Number(body.sortOrder));
@@ -495,6 +571,8 @@ export function registerTaskRoutes(app, deps) {
495
571
  res.status(404).json({ error: "未找到该任务。" });
496
572
  return;
497
573
  }
574
+ if (body.agent !== undefined && patch.agent)
575
+ writeTaskBoardLastAgent(storage, patch.agent);
498
576
  res.json(dto(storage.getWandTask(task.id) ?? task));
499
577
  }
500
578
  catch (error) {
@@ -556,16 +634,81 @@ export function registerTaskRoutes(app, deps) {
556
634
  if (!config)
557
635
  throw new Error("当前服务未启用派发,无法派发 Agent。");
558
636
  const body = bodyObject(req.body);
559
- const agent = body.agent === undefined
560
- ? task.agent ?? parseWandTaskAgent(JSON.stringify(body))
561
- : parseTaskAgent(body.agent, task.agent?.mode, task.agent?.kind);
637
+ const subject = body.subject === undefined
638
+ ? body.agent !== undefined ? null : task.executionSubject ?? null
639
+ : parseExecutionSubject(storage, body.subject);
640
+ if ((subject?.type === "employee" || subject?.type === "team")
641
+ && (body.kind === "pty" || (body.agent && bodyObject(body.agent).kind === "pty"))) {
642
+ throw new Error("PTY 只能选择 CLI 工具。");
643
+ }
644
+ const requestedPrompt = text(body.prompt);
645
+ const existingSessions = storage.listWandTaskSessionIds(task.id).length;
646
+ if (existingSessions > 0 && !requestedPrompt)
647
+ throw new Error("请输入提示词。");
648
+ const prompt = requestedPrompt || task.description.trim() || task.title || "执行此任务";
649
+ if (subject?.type === "team") {
650
+ if (!aiTeams)
651
+ throw new Error("当前服务未启用 AI 团队。");
652
+ const team = storage.getAiTeam(subject.id);
653
+ if (!team)
654
+ throw new Error("AI 团队不存在。");
655
+ if (body.workspaceId !== undefined) {
656
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
657
+ if (workspaceId && !storage.getWorkspace(workspaceId))
658
+ throw new Error("项目不存在。");
659
+ parentTaskIdFrom(storage, task.parentTaskId, workspaceId, task.id, task.parentTaskId);
660
+ if (workspaceId !== task.workspaceId)
661
+ assertTaskWorkspaceMove(storage, task.id, workspaceId);
662
+ task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
663
+ }
664
+ const target = resolveTaskDispatchTarget({ storage, config }, task);
665
+ const detail = await aiTeams.start({ teamId: team.id, taskId: task.id, note: prompt });
666
+ const preferredMember = team.members.find((member) => member.isLeader) ?? team.members[0];
667
+ const preferredAgent = preferredMember ? memberAgents(preferredMember)[0] ?? null : null;
668
+ storage.updateWandTask(task.id, {
669
+ executionSubject: subject,
670
+ agent: preferredAgent,
671
+ status: task.status === "todo" ? "doing" : task.status,
672
+ });
673
+ recordIterationPromptForTask(storage, {
674
+ sessionId: detail.run.chatSessionId ?? undefined,
675
+ cwd: target.cwd,
676
+ workspaceId: target.workspaceId,
677
+ milestoneId: task.milestoneId,
678
+ taskId: task.id,
679
+ title: task.title,
680
+ detail: task.description || prompt,
681
+ source: "dispatch",
682
+ });
683
+ res.status(202).json({
684
+ ok: true,
685
+ taskId: task.id,
686
+ subject,
687
+ teamRun: detail,
688
+ session: detail.run.chatSessionId ? { id: detail.run.chatSessionId, sessionKind: "structured" } : null,
689
+ });
690
+ return;
691
+ }
692
+ const employee = subject?.type === "employee" ? storage.getSiliconEmployee(subject.id) : null;
693
+ if (subject?.type === "employee" && (!employee || employee.archivedAt)) {
694
+ throw new Error("硅基员工不存在或已归档。");
695
+ }
696
+ const selected = employee ? selectEmployeeCandidate(employee) : null;
697
+ const cliDefault = subject?.type === "cli"
698
+ ? parseTaskAgent({ ...cliAgentForSubject(subject, task.agent), ...(body.kind === "pty" ? { kind: "pty" } : {}) })
699
+ : null;
700
+ const agent = selected?.agent ?? (body.agent === undefined
701
+ ? cliDefault ?? task.agent ?? parseWandTaskAgent(JSON.stringify(body))
702
+ : parseTaskAgent(body.agent, task.agent?.mode, task.agent?.kind));
562
703
  if (!agent)
563
704
  throw new Error("请先为该任务选择 CLI 工具。");
705
+ if (subject?.type === "cli" && agent.provider !== subject.id) {
706
+ throw new Error("CLI 执行对象与工具配置不一致。");
707
+ }
564
708
  if (agent.kind === "structured" && !structured)
565
709
  throw new Error("当前服务未启用结构化会话,无法派发 Agent。");
566
710
  if (agent.kind === "pty" && !processes)
567
711
  throw new Error("当前服务未启用终端会话,无法派发 Agent。");
568
- writeTaskBoardLastAgent(storage, agent);
569
712
  if (body.workspaceId !== undefined) {
570
713
  const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
571
714
  if (workspaceId && !storage.getWorkspace(workspaceId))
@@ -575,14 +718,18 @@ export function registerTaskRoutes(app, deps) {
575
718
  assertTaskWorkspaceMove(storage, task.id, workspaceId);
576
719
  task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
577
720
  }
578
- const requestedPrompt = text(body.prompt);
579
- const existingSessions = storage.listWandTaskSessionIds(task.id).length;
580
- if (existingSessions > 0 && !requestedPrompt)
581
- throw new Error("请输入提示词。");
582
- const prompt = requestedPrompt || task.description.trim() || task.title || "执行此任务";
583
- const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}` });
721
+ const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}`,
722
+ systemPrompt: employee?.prompt, employee: employee ?? undefined,
723
+ employeeCandidateIndex: selected?.index });
584
724
  task = storage.getWandTask(task.id);
585
- storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
725
+ const usedSubject = subject ?? { type: "cli", id: agent.provider };
726
+ storage.updateWandTask(task.id, {
727
+ agent,
728
+ executionSubject: usedSubject,
729
+ status: task.status === "todo" ? "doing" : task.status,
730
+ });
731
+ if (!employee)
732
+ writeTaskBoardLastAgent(storage, agent);
586
733
  // 派发也算一轮迭代里的改动意图:直接把任务的标题 / 描述记进迭代记录。
587
734
  recordIterationPromptForTask(storage, {
588
735
  sessionId: session.id,
@@ -597,6 +744,7 @@ export function registerTaskRoutes(app, deps) {
597
744
  res.status(202).json({
598
745
  ok: true,
599
746
  taskId: task.id,
747
+ subject: usedSubject,
600
748
  session: {
601
749
  id: session.id,
602
750
  provider: session.provider,
@@ -605,6 +753,10 @@ export function registerTaskRoutes(app, deps) {
605
753
  thinkingEffort: session.thinkingEffort,
606
754
  mode: session.mode,
607
755
  cwd: session.cwd,
756
+ employeeId: session.employeeId,
757
+ employeeName: session.employeeName,
758
+ employeeAvatar: session.employeeAvatar,
759
+ employeeCandidateIndex: session.employeeCandidateIndex,
608
760
  },
609
761
  });
610
762
  }
@@ -405,7 +405,7 @@ export function registerAdminUpdateRoutes(app, deps) {
405
405
  return;
406
406
  }
407
407
  const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
408
- const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "grok" || value === "qoder" || value === "pi");
408
+ const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "grok" || value === "qoder" || value === "pi" || value === "gemini");
409
409
  state.providerCliUpdateInFlight = true;
410
410
  try {
411
411
  const before = await refreshProviderCliUpdateState(state, config);
@@ -40,6 +40,9 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
40
40
  inFlight: live.structuredState?.inFlight === true,
41
41
  cwd: live.cwd,
42
42
  startedAt: live.startedAt,
43
+ employeeId: live.employeeId,
44
+ employeeName: live.employeeName,
45
+ employeeAvatar: live.employeeAvatar,
43
46
  ...(team.chat ? { teamChat: team.chat } : {}),
44
47
  ...(team.step ? { teamStep: team.step } : {}),
45
48
  };
package/dist/server.js CHANGED
@@ -33,6 +33,7 @@ import { registerMissionRoutes } from "./server-mission-routes.js";
33
33
  import { Missions } from "./missions.js";
34
34
  import { createAiTeamRunner } from "./ai-team-runner.js";
35
35
  import { registerAiTeamRoutes } from "./server-ai-team-routes.js";
36
+ import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
36
37
  import { registerAttentionRoutes } from "./server-attention-routes.js";
37
38
  import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
38
39
  import { parseSessionCreationOrigin, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
@@ -298,6 +299,7 @@ export async function startServer(config, configPath, options = {}) {
298
299
  grok: storage.getPreference("pref:defaultGrokModel", config.defaultGrokModel ?? ""),
299
300
  qoder: storage.getPreference("pref:defaultQoderModel", config.defaultQoderModel ?? ""),
300
301
  pi: storage.getPreference("pref:defaultPiModel", config.defaultPiModel ?? ""),
302
+ gemini: storage.getPreference("pref:defaultGeminiModel", config.defaultGeminiModel ?? ""),
301
303
  });
302
304
  const getModelRefreshOptions = () => {
303
305
  const injected = options.modelRefreshOptions?.() ?? {};
@@ -361,6 +363,7 @@ export async function startServer(config, configPath, options = {}) {
361
363
  grok: "grokModels",
362
364
  qoder: "qoderModels",
363
365
  pi: "piModels",
366
+ gemini: "geminiModels",
364
367
  }[provider];
365
368
  return cache[key].map((entry) => entry.id);
366
369
  };
@@ -707,12 +710,17 @@ export async function startServer(config, configPath, options = {}) {
707
710
  });
708
711
  registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
709
712
  // 任务管理与 Missions 一样只需登录:原生 connected-app 也要能列/建/派发。
710
- registerTaskRoutes(app, { storage, sessions: sessionRegistry, structured: structuredSessions, processes, config });
713
+ registerTaskRoutes(app, { storage, sessions: sessionRegistry, structured: structuredSessions, processes, config, aiTeams });
711
714
  registerAiTeamRoutes(app, {
712
715
  storage,
713
716
  runner: aiTeams,
714
717
  notifyTeamChanged: (teamId) => notifyAiTeamRun({ kind: "ai-team-definition", teamId }),
715
718
  });
719
+ registerSiliconEmployeeRoutes(app, {
720
+ storage,
721
+ config,
722
+ notifyEmployeeChanged: (employeeId) => notifyAiTeamRun({ kind: "silicon-employee-definition", employeeId }),
723
+ });
716
724
  registerAttentionRoutes(app, { sessions: sessionRegistry, runner: aiTeams });
717
725
  registerAdminUpdateRoutes(app, {
718
726
  storage,
@@ -755,7 +763,7 @@ export async function startServer(config, configPath, options = {}) {
755
763
  if (snapshot?.cwd)
756
764
  cwd = snapshot.cwd;
757
765
  if (snapshot)
758
- ai = resolveSystemAiContext(snapshot, config);
766
+ ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
759
767
  }
760
768
  if (!ai) {
761
769
  const defaultSession = {
@@ -766,7 +774,7 @@ export async function startServer(config, configPath, options = {}) {
766
774
  selectedModel: null,
767
775
  thinkingEffort: config.defaultThinkingEffort,
768
776
  };
769
- ai = resolveSystemAiContext(defaultSession, config);
777
+ ai = resolveSystemAiContext(defaultSession, config, storage.getSystemSiliconEmployee());
770
778
  }
771
779
  try {
772
780
  const optimized = await optimizePrompt(text, config.language ?? "", cwd, ai);
@@ -785,6 +793,11 @@ export async function startServer(config, configPath, options = {}) {
785
793
  // ── Session control ──
786
794
  app.post("/api/commands", asyncRoute(async (req, res) => {
787
795
  const body = req.body;
796
+ if (body.employeeId !== undefined || body.teamId !== undefined
797
+ || (body.subject !== undefined && body.subject?.type !== "cli")) {
798
+ res.status(400).json({ error: "PTY 只能选择 CLI 工具。" });
799
+ return;
800
+ }
788
801
  const interactiveShell = body.shell === true;
789
802
  if (!interactiveShell && !body.command?.trim()) {
790
803
  res.status(400).json({ error: "请输入要执行的命令。" });
@@ -1,4 +1,5 @@
1
1
  import { discoverCliSystemAiConfigs } from "./system-ai.js";
2
+ import type { SiliconEmployee } from "./ai-team-types.js";
2
3
  import type { SessionProvider, SessionSnapshot, SystemAiConfig, WandConfig } from "./types.js";
3
4
  export interface SessionAiContext {
4
5
  provider: SessionProvider;
@@ -6,6 +7,10 @@ export interface SessionAiContext {
6
7
  thinkingEffort: SessionSnapshot["thinkingEffort"];
7
8
  inheritEnv?: boolean;
8
9
  systemAi?: SystemAiConfig;
10
+ /** 内置「系统运维」员工的人设;作为系统提示前缀注入 Wand 自有 AI 调用。 */
11
+ opsPersona?: string;
12
+ /** CLI 降级链(按顺序)。为空时只用 provider/model 这一次调用。 */
13
+ cliCandidates?: import("./types.js").AiCliCandidate[];
9
14
  }
10
15
  /**
11
16
  * Resolve the provider from every representation used by current and legacy
@@ -16,6 +21,6 @@ export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "
16
21
  /** Build the provider-specific settings used by session-adjacent AI actions. */
17
22
  export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
18
23
  /** Build the source order for Wand-owned AI features such as titles. */
19
- export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0], config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAi" | "systemAiCli" | "systemAiModel">): SessionAiContext;
24
+ export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0], config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAi" | "systemAiCli" | "systemAiModel">, systemEmployee?: SiliconEmployee | null): SessionAiContext;
20
25
  /** Build the AI context for quick-commit actions from their global preferences. */
21
- export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv" | "commitAiSource" | "systemAi">, discoverApis?: typeof discoverCliSystemAiConfigs): SessionAiContext;
26
+ export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv" | "commitAiSource" | "systemAi" | "systemAiCli" | "systemAiModel">, discoverApis?: typeof discoverCliSystemAiConfigs, systemEmployee?: SiliconEmployee | null): SessionAiContext;
@@ -1,6 +1,8 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
2
  import { discoverCliSystemAiConfigs, mergeSystemAiConfigs, systemAiProfiles, } from "./system-ai.js";
3
- import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider } from "./session-provider.js";
3
+ import { providerCliInstalled, isSessionProvider } from "./session-provider.js";
4
+ import { isSystemSiliconEmployee, systemEmployeeCliCandidates } from "./system-employee.js";
5
+ import { inferProviderFromCommand, inferProviderFromRunner } from "./session-provider.js";
4
6
  /**
5
7
  * Resolve the provider from every representation used by current and legacy
6
8
  * sessions. Older persisted sessions may not have the top-level provider, but
@@ -37,18 +39,39 @@ export function resolveSessionAiContext(snapshot, config) {
37
39
  };
38
40
  }
39
41
  /** Build the source order for Wand-owned AI features such as titles. */
40
- export function resolveSystemAiContext(snapshot, config) {
42
+ export function resolveSystemAiContext(snapshot, config, systemEmployee) {
41
43
  const sessionContext = resolveSessionAiContext(snapshot, config);
44
+ // 候选里的「跟随默认模型」在这里就换成具体的 Wand 默认模型:降级到下一个 provider
45
+ // 时不能拿上一个 provider 的模型,也不能把决定权交给 CLI 自己的默认值。
46
+ const chain = systemEmployeeCliCandidates(systemEmployee).map((candidate) => ({
47
+ ...candidate,
48
+ model: candidate.model ?? normalizeModel(getDefaultModelForProvider(config, candidate.provider)),
49
+ }));
50
+ const owned = {
51
+ ...sessionContext,
52
+ ...(chain.length ? { cliCandidates: chain } : {}),
53
+ ...(isSystemSiliconEmployee(systemEmployee) ? { opsPersona: systemEmployee.prompt } : {}),
54
+ };
55
+ // 内置员工可用时,Wand 自有调用一律按它的候选链:provider/model 取首个已安装的
56
+ // 候选(两条路都一致),整条链留给运行期降级。
57
+ if (chain.length) {
58
+ const preferred = chain.find((candidate) => providerCliInstalled(candidate.provider)) ?? chain[0];
59
+ owned.provider = preferred.provider;
60
+ owned.model = preferred.model ?? normalizeModel(getDefaultModelForProvider(config, preferred.provider));
61
+ owned.thinkingEffort = preferred.thinkingEffort ?? config.defaultThinkingEffort;
62
+ }
42
63
  if (config.systemAi?.enabled) {
43
64
  const directApi = usableSystemAi(config.systemAi);
44
- return directApi ? { ...sessionContext, systemAi: directApi } : sessionContext;
65
+ return directApi ? { ...owned, systemAi: directApi } : owned;
45
66
  }
67
+ if (chain.length)
68
+ return owned;
46
69
  // Older installations without a system AI CLI preference still follow the
47
70
  // current session. A chosen CLI must never inherit another provider's model.
48
71
  if (!isSessionProvider(config.systemAiCli))
49
- return sessionContext;
72
+ return owned;
50
73
  return {
51
- ...sessionContext,
74
+ ...owned,
52
75
  provider: config.systemAiCli,
53
76
  model: normalizeModel(config.systemAiModel)
54
77
  ?? normalizeModel(getDefaultModelForProvider(config, config.systemAiCli)),
@@ -56,10 +79,14 @@ export function resolveSystemAiContext(snapshot, config) {
56
79
  };
57
80
  }
58
81
  /** Build the AI context for quick-commit actions from their global preferences. */
59
- export function resolveCommitAiContext(snapshot, config, discoverApis = discoverCliSystemAiConfigs) {
60
- const sessionContext = resolveSessionAiContext(snapshot, config);
61
- if (config.commitAiSource !== "api")
62
- return sessionContext;
82
+ export function resolveCommitAiContext(snapshot, config, discoverApis = discoverCliSystemAiConfigs, systemEmployee) {
83
+ const sessionContext = resolveSystemAiContext(snapshot, config, systemEmployee);
84
+ if (config.commitAiSource !== "api") {
85
+ // 快捷提交的直连来源只看 commitAiSource;系统 AI 的 enabled 不能顺手把它打开。
86
+ const cliContext = { ...sessionContext };
87
+ delete cliContext.systemAi;
88
+ return cliContext;
89
+ }
63
90
  const directApi = mergeSystemAiConfigs(config.systemAi, discoverApis(sessionContext.provider));
64
91
  return {
65
92
  ...sessionContext,
@@ -1,10 +1,15 @@
1
1
  import type { SessionProvider } from "./types.js";
2
2
  /** 全部受支持的会话 provider;顺序与 UI / 派发选择器一致。 */
3
- export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi"];
3
+ export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
4
4
  /** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
5
5
  export declare function isSessionProvider(value: unknown): value is SessionProvider;
6
6
  /** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
7
7
  export declare function providerCliCommand(provider: SessionProvider): string;
8
+ /**
9
+ * 不启动 shell 也能判断某个 provider 的 CLI 是否在 PATH 上(与子进程 PATH 一致)。
10
+ * 用于多候选降级时跳过没安装的工具,避免白等一次 spawn 失败。
11
+ */
12
+ export declare function providerCliInstalled(provider: SessionProvider, pathValue?: string): boolean;
8
13
  /** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
9
14
  export declare function inferProviderFromRunner(runner: unknown): SessionProvider | undefined;
10
15
  /**
@@ -1,5 +1,7 @@
1
+ import { constants, accessSync, statSync } from "node:fs";
2
+ import path from "node:path";
1
3
  /** 全部受支持的会话 provider;顺序与 UI / 派发选择器一致。 */
2
- export const SESSION_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi"];
4
+ export const SESSION_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
3
5
  const SESSION_PROVIDER_SET = new Set(SESSION_PROVIDERS);
4
6
  /** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
5
7
  export function isSessionProvider(value) {
@@ -9,6 +11,25 @@ export function isSessionProvider(value) {
9
11
  export function providerCliCommand(provider) {
10
12
  return provider === "qoder" ? "qodercli" : provider;
11
13
  }
14
+ /**
15
+ * 不启动 shell 也能判断某个 provider 的 CLI 是否在 PATH 上(与子进程 PATH 一致)。
16
+ * 用于多候选降级时跳过没安装的工具,避免白等一次 spawn 失败。
17
+ */
18
+ export function providerCliInstalled(provider, pathValue = process.env.PATH ?? "") {
19
+ const command = providerCliCommand(provider);
20
+ for (const directory of pathValue.split(path.delimiter)) {
21
+ if (!directory)
22
+ continue;
23
+ try {
24
+ const executable = path.join(directory, command);
25
+ accessSync(executable, constants.X_OK);
26
+ if (statSync(executable).isFile())
27
+ return true;
28
+ }
29
+ catch { /* 检查下一个 PATH 目录 */ }
30
+ }
31
+ return false;
32
+ }
12
33
  /** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
13
34
  export function inferProviderFromRunner(runner) {
14
35
  if (runner === "claude-cli" || runner === "claude-cli-print" || runner === "claude-sdk")
@@ -23,6 +44,8 @@ export function inferProviderFromRunner(runner) {
23
44
  return "qoder";
24
45
  if (runner === "pi-cli-json")
25
46
  return "pi";
47
+ if (runner === "gemini-cli-json")
48
+ return "gemini";
26
49
  return undefined;
27
50
  }
28
51
  /**
@@ -45,5 +68,8 @@ export function inferProviderFromCommand(command) {
45
68
  return "qoder";
46
69
  if (/^pi\b/i.test(value))
47
70
  return "pi";
71
+ // Gemini CLI 的可执行文件就叫 gemini;-p / --prompt 等参数不影响首 token 识别。
72
+ if (/^gemini\b/i.test(value))
73
+ return "gemini";
48
74
  return undefined;
49
75
  }
@@ -26,6 +26,10 @@ function sessionBase(snapshot) {
26
26
  id: snapshot.id,
27
27
  sessionSource: snapshot.sessionSource,
28
28
  automationId: snapshot.automationId,
29
+ employeeId: snapshot.employeeId,
30
+ employeeName: snapshot.employeeName,
31
+ employeeAvatar: snapshot.employeeAvatar,
32
+ employeeCandidateIndex: snapshot.employeeCandidateIndex,
29
33
  sessionKind: snapshot.sessionKind,
30
34
  provider: snapshot.provider,
31
35
  providerCliActive: snapshot.providerCliActive,
@@ -0,0 +1,9 @@
1
+ import type { SiliconEmployee } from "./ai-team-types.js";
2
+ import type { WandTaskAgent } from "./task-types.js";
3
+ /** Match child-process PATH without starting a shell or trusting a client availability claim. */
4
+ export declare function employeeCliAvailable(agent: WandTaskAgent, pathValue?: string): boolean;
5
+ /** First installed candidate wins; when none is installed retain the preferred candidate for a recoverable failure. */
6
+ export declare function selectEmployeeCandidate(employee: Pick<SiliconEmployee, "agents">, available?: (agent: WandTaskAgent) => boolean): {
7
+ agent: WandTaskAgent;
8
+ index: number;
9
+ };
@@ -0,0 +1,13 @@
1
+ import { providerCliInstalled } from "./session-provider.js";
2
+ /** Match child-process PATH without starting a shell or trusting a client availability claim. */
3
+ export function employeeCliAvailable(agent, pathValue = process.env.PATH ?? "") {
4
+ return providerCliInstalled(agent.provider, pathValue);
5
+ }
6
+ /** First installed candidate wins; when none is installed retain the preferred candidate for a recoverable failure. */
7
+ export function selectEmployeeCandidate(employee, available = employeeCliAvailable) {
8
+ if (!employee.agents.length || employee.agents.some((agent) => agent.kind !== "structured")) {
9
+ throw new Error("硅基员工没有可用的结构化执行候选。");
10
+ }
11
+ const index = employee.agents.findIndex((agent) => available(agent));
12
+ return { agent: employee.agents[index < 0 ? 0 : index], index: index < 0 ? 0 : index };
13
+ }
@@ -0,0 +1,36 @@
1
+ import { type QuickCommitAiOptions } from "./git-quick-commit.js";
2
+ import type { SiliconEmployeeDraft } from "./ai-team-types.js";
3
+ import type { AiTextRequest } from "./system-ai.js";
4
+ import type { WandTaskAgent } from "./task-types.js";
5
+ import type { SessionProvider } from "./types.js";
6
+ export declare class SiliconEmployeeDraftError extends Error {
7
+ readonly code: string;
8
+ constructor(message: string, code: string);
9
+ }
10
+ /** 硅基员工只能跑结构化会话;候选沿用看板的「跟随服务端默认」。 */
11
+ export declare function employeeAgentFor(provider: SessionProvider): WandTaskAgent;
12
+ /**
13
+ * 已安装的 CLI:默认 provider 优先,其余按全局顺序;
14
+ * 都没装时返回空数组,由调用方决定退回到哪一个首选。
15
+ */
16
+ export declare function availableEmployeeProviders(preferred?: SessionProvider, isAvailable?: (agent: WandTaskAgent) => boolean): SessionProvider[];
17
+ /** 从模型输出里取第一个 JSON 对象,容忍前后夹带解释文字。 */
18
+ export declare function extractDraftJson(raw: string): Record<string, unknown>;
19
+ /**
20
+ * 校验并收敛模型输出。名字 / 角色设定缺失即视为不可用;
21
+ * provider 不在已安装清单里时按候选顺序兜底,不让员工带着跑不起来的 CLI 落库。
22
+ */
23
+ export declare function parseSiliconEmployeeDraft(raw: string, options?: {
24
+ allowedProviders?: SessionProvider[];
25
+ fallbackProvider?: SessionProvider;
26
+ }): SiliconEmployeeDraft;
27
+ /** 规则走系统提示,用户的期望走用户消息;一次性调用沿用系统 AI 通道。 */
28
+ export declare function buildEmployeeDraftPrompt(expectation: string, providers: SessionProvider[], existingNames?: string[]): AiTextRequest;
29
+ /** 按自然语言期望生成员工配置;调用方负责随后落库。 */
30
+ export declare function generateSiliconEmployeeDraft(expectation: string, ai?: QuickCommitAiOptions, options?: {
31
+ cwd?: string;
32
+ language?: string;
33
+ existingNames?: string[];
34
+ preferredProvider?: SessionProvider;
35
+ isProviderAvailable?: (agent: WandTaskAgent) => boolean;
36
+ }): Promise<SiliconEmployeeDraft>;