@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.
- package/README.md +18 -6
- package/dist/agent-dispatch.d.ts +3 -0
- package/dist/agent-dispatch.js +5 -0
- package/dist/ai-team-runner.js +4 -2
- package/dist/ai-team-types.d.ts +34 -0
- package/dist/ai-team-types.js +12 -1
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +4 -3
- package/dist/config.js +25 -0
- package/dist/git-quick-commit.d.ts +9 -8
- package/dist/git-quick-commit.js +123 -8
- package/dist/models.d.ts +3 -0
- package/dist/models.js +24 -2
- package/dist/process-manager.js +16 -3
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/provider-cli-updater.js +12 -1
- package/dist/server-ai-team-routes.js +4 -1
- package/dist/server-employee-routes.d.ts +22 -0
- package/dist/server-employee-routes.js +277 -0
- package/dist/server-session-routes.js +38 -13
- package/dist/server-settings-routes.js +5 -0
- package/dist/server-task-routes.d.ts +2 -0
- package/dist/server-task-routes.js +174 -22
- package/dist/server-update-routes.js +1 -1
- package/dist/server-workspace-routes.js +3 -0
- package/dist/server.js +16 -3
- package/dist/session-ai-context.d.ts +7 -2
- package/dist/session-ai-context.js +36 -9
- package/dist/session-provider.d.ts +6 -1
- package/dist/session-provider.js +27 -1
- package/dist/session-transport.js +4 -0
- package/dist/silicon-employee-dispatch.d.ts +9 -0
- package/dist/silicon-employee-dispatch.js +13 -0
- package/dist/silicon-employee-draft.d.ts +36 -0
- package/dist/silicon-employee-draft.js +146 -0
- package/dist/storage.d.ts +19 -2
- package/dist/storage.js +158 -7
- package/dist/structured-gemini-adapter.d.ts +40 -0
- package/dist/structured-gemini-adapter.js +198 -0
- package/dist/structured-provider-common.d.ts +2 -1
- package/dist/structured-provider-common.js +7 -2
- package/dist/structured-session-manager.d.ts +10 -0
- package/dist/structured-session-manager.js +106 -14
- package/dist/system-ai.js +1 -1
- package/dist/system-employee.d.ts +35 -0
- package/dist/system-employee.js +81 -0
- package/dist/task-types.d.ts +8 -1
- package/dist/task-types.js +7 -0
- package/dist/types.d.ts +23 -4
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +64 -64
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +3 -0
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +2 -1
- package/dist/web-ui/provider-identity.js +10 -1
- package/dist/web-ui/session-activity.js +1 -1
- 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,
|
package/dist/process-manager.js
CHANGED
|
@@ -1046,10 +1046,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1046
1046
|
? getProviderCommandSessionId(provider, processedCommand)
|
|
1047
1047
|
?? getProviderCommandSessionId(provider, command)
|
|
1048
1048
|
: null;
|
|
1049
|
-
// Grok
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
461
|
-
|
|
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:
|
|
477
|
-
|
|
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
|
}
|