@co0ontty/wand 4.82.0 → 4.83.1
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 +47 -9
- package/dist/agent-dispatch.d.ts +3 -2
- package/dist/agent-dispatch.js +19 -3
- package/dist/ai-team-delivery-types.d.ts +40 -0
- package/dist/ai-team-delivery-types.js +1 -0
- package/dist/ai-team-delivery.d.ts +13 -0
- package/dist/ai-team-delivery.js +89 -0
- package/dist/ai-team-employee-binding.d.ts +16 -0
- package/dist/ai-team-employee-binding.js +77 -0
- package/dist/ai-team-prompts.js +5 -0
- package/dist/ai-team-runner.d.ts +9 -2
- package/dist/ai-team-runner.js +52 -7
- package/dist/ai-team-types.d.ts +17 -3
- package/dist/ai-team-types.js +43 -3
- package/dist/build-info.json +3 -3
- package/dist/cli.js +36 -0
- package/dist/config.d.ts +18 -10
- package/dist/config.js +63 -105
- package/dist/decision-client.d.ts +4 -0
- package/dist/decision-client.js +91 -0
- package/dist/decision-runner.d.ts +9 -0
- package/dist/decision-runner.js +36 -0
- package/dist/decision-service.d.ts +46 -0
- package/dist/decision-service.js +230 -0
- package/dist/decision-skill.d.ts +6 -0
- package/dist/decision-skill.js +70 -0
- package/dist/decision-tool.d.ts +21 -0
- package/dist/decision-tool.js +346 -0
- package/dist/decision-types.d.ts +36 -0
- package/dist/decision-types.js +123 -0
- package/dist/decision-worker.py +86 -0
- package/dist/default-employee.d.ts +4 -1
- package/dist/default-employee.js +11 -1
- package/dist/env-utils.js +3 -0
- package/dist/git-quick-commit.js +7 -1
- package/dist/message-truncator.d.ts +1 -1
- package/dist/message-truncator.js +29 -22
- package/dist/models.d.ts +1 -0
- package/dist/models.js +31 -4
- package/dist/process-manager.d.ts +39 -1
- package/dist/process-manager.js +112 -8
- package/dist/provider-catalog.d.ts +47 -0
- package/dist/provider-catalog.js +138 -0
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/pty-foreground.d.ts +37 -0
- package/dist/pty-foreground.js +56 -0
- package/dist/pty-turn-activity.d.ts +6 -5
- package/dist/pty-turn-activity.js +6 -5
- package/dist/retention.d.ts +29 -0
- package/dist/retention.js +142 -0
- package/dist/server-ai-team-routes.d.ts +2 -1
- package/dist/server-ai-team-routes.js +27 -9
- package/dist/server-decision-routes.d.ts +10 -0
- package/dist/server-decision-routes.js +52 -0
- package/dist/server-employee-routes.js +9 -1
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +97 -1
- package/dist/server-settings-routes.js +10 -36
- package/dist/server-update-routes.js +2 -1
- package/dist/server-workspace-routes.js +8 -0
- package/dist/server.js +37 -5
- package/dist/session-completion-state.d.ts +26 -0
- package/dist/session-completion-state.js +30 -0
- package/dist/session-completion.d.ts +12 -0
- package/dist/session-completion.js +62 -0
- package/dist/session-provider.d.ts +1 -15
- package/dist/session-provider.js +4 -54
- package/dist/session-registry.d.ts +5 -0
- package/dist/session-registry.js +19 -1
- package/dist/session-transport.js +2 -0
- package/dist/storage.d.ts +12 -0
- package/dist/storage.js +119 -16
- package/dist/structured-client-protocol.js +36 -1
- package/dist/structured-provider-common.js +3 -3
- package/dist/structured-session-manager.d.ts +13 -1
- package/dist/structured-session-manager.js +196 -27
- package/dist/system-employee.js +1 -0
- package/dist/task-types.d.ts +4 -2
- package/dist/tool-preview.d.ts +8 -0
- package/dist/tool-preview.js +158 -0
- package/dist/types.d.ts +35 -2
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +66 -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 +7 -13
- package/dist/web-ui/provider-identity.js +8 -71
- package/package.json +3 -2
- package/skills/wand-decision/SKILL.md +61 -0
- package/skills/wand-decision/scripts/decide.mjs +16 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, } from "./ai-team-types.js";
|
|
2
|
+
import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
4
|
import { asyncRoute } from "./express-async.js";
|
|
5
5
|
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
@@ -75,6 +75,12 @@ export function parseSystemEmployeeAgents(value, existing) {
|
|
|
75
75
|
throw new Error(`该员工是内置的,${field.label}不可修改。`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
if (body.tags !== undefined && JSON.stringify(body.tags) !== JSON.stringify(siliconEmployeeTags(existing))) {
|
|
79
|
+
throw new Error("该员工是内置的,标签不可修改。");
|
|
80
|
+
}
|
|
81
|
+
if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
|
|
82
|
+
throw new Error("该员工是内置的,身份不可修改。");
|
|
83
|
+
}
|
|
78
84
|
if (!Array.isArray(body.agents))
|
|
79
85
|
throw new Error("请提供执行候选数组。");
|
|
80
86
|
if (body.agents.length === 0)
|
|
@@ -139,6 +145,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
139
145
|
throw new Error("执行候选存在重复。");
|
|
140
146
|
}
|
|
141
147
|
const avatar = parseAvatar(body.avatar, name);
|
|
148
|
+
const tags = parseSiliconEmployeeTags(body.tags === undefined ? existing?.tags ?? [] : body.tags);
|
|
142
149
|
const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
|
|
143
150
|
return {
|
|
144
151
|
id,
|
|
@@ -147,6 +154,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
147
154
|
prompt,
|
|
148
155
|
avatar,
|
|
149
156
|
agents,
|
|
157
|
+
tags,
|
|
150
158
|
archivedAt: existing?.archivedAt,
|
|
151
159
|
createdAt: existing ? existing.createdAt : now,
|
|
152
160
|
updatedAt: now,
|
|
@@ -2,7 +2,7 @@ import type { Express } from "express";
|
|
|
2
2
|
import { ProcessManager } from "./process-manager.js";
|
|
3
3
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
|
-
import { ExecutionMode, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
5
|
+
import { ExecutionMode, ProcessEvent, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
6
6
|
import { toSessionListItemDTO } from "./session-transport.js";
|
|
7
7
|
import { SessionRegistry } from "./session-registry.js";
|
|
8
8
|
import { type SessionDirectoryNode } from "./session-directory-tree.js";
|
|
@@ -45,6 +45,6 @@ export interface SessionDirectoryTreeResponse {
|
|
|
45
45
|
}
|
|
46
46
|
export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
|
|
47
47
|
export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
|
|
48
|
-
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
|
|
48
|
+
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void): void;
|
|
49
49
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|
|
50
50
|
export {};
|
|
@@ -181,6 +181,8 @@ function sessionListRevision(entries, directoryNames = []) {
|
|
|
181
181
|
entry.sortTimestamp,
|
|
182
182
|
entry.session.title,
|
|
183
183
|
entry.session.status,
|
|
184
|
+
entry.session.completionRevision ?? 0,
|
|
185
|
+
entry.session.viewedCompletionRevision ?? 0,
|
|
184
186
|
entry.session.ptyBusy === true,
|
|
185
187
|
entry.session.structuredState?.inFlight === true,
|
|
186
188
|
]);
|
|
@@ -347,7 +349,7 @@ function canMergeSession(snapshot) {
|
|
|
347
349
|
function isMergeActionAllowed(snapshot) {
|
|
348
350
|
return snapshot.status !== "running";
|
|
349
351
|
}
|
|
350
|
-
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
|
|
352
|
+
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed) {
|
|
351
353
|
const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
|
|
352
354
|
const sessionResponseDTO = (snapshot, req) => {
|
|
353
355
|
const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
@@ -528,6 +530,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
528
530
|
sendRouteError(res, error, "无法启动结构化会话。");
|
|
529
531
|
}
|
|
530
532
|
}));
|
|
533
|
+
app.post("/api/sessions/:id/provider", (req, res) => {
|
|
534
|
+
const provider = req.body?.provider;
|
|
535
|
+
if (!isSessionProvider(provider)) {
|
|
536
|
+
res.status(400).json({ error: "请选择有效的 CLI 工具。" });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const id = req.params.id;
|
|
540
|
+
const owner = sessions.ownerOf(id);
|
|
541
|
+
if (!owner) {
|
|
542
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (owner !== "structured") {
|
|
546
|
+
res.status(400).json({ error: "只有新建空白结构化对话可以更换工具。" });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
res.json(sessionResponseDTO(structured.setSessionProvider(id, provider), req));
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
sendRouteError(res, error, "切换工具失败。");
|
|
554
|
+
}
|
|
555
|
+
});
|
|
531
556
|
app.post("/api/sessions/:id/model", (req, res) => {
|
|
532
557
|
const body = req.body;
|
|
533
558
|
const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
|
|
@@ -1129,6 +1154,24 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1129
1154
|
}
|
|
1130
1155
|
res.json(page);
|
|
1131
1156
|
}));
|
|
1157
|
+
// Explicit display acknowledgement: reads/prefetch/resync never consume completion state.
|
|
1158
|
+
app.post("/api/sessions/:id/completion/view", (req, res) => {
|
|
1159
|
+
const snapshot = sessions.get(req.params.id);
|
|
1160
|
+
if (!snapshot) {
|
|
1161
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const revision = req.body?.completionRevision;
|
|
1165
|
+
if (!Number.isSafeInteger(revision) || revision <= 0) {
|
|
1166
|
+
res.status(400).json({ error: "完成代次无效。" });
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const changed = storage.markSessionCompletionViewed(snapshot.id, revision);
|
|
1170
|
+
const completion = storage.getSessionCompletion(snapshot.id);
|
|
1171
|
+
if (changed)
|
|
1172
|
+
onCompletionViewed?.({ type: "status", sessionId: snapshot.id, data: completion });
|
|
1173
|
+
res.json({ ok: true, ...completion });
|
|
1174
|
+
});
|
|
1132
1175
|
app.get("/api/sessions/:id", (req, res) => {
|
|
1133
1176
|
const snapshot = sessions.get(req.params.id);
|
|
1134
1177
|
if (!snapshot) {
|
|
@@ -1494,6 +1537,59 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1494
1537
|
sendRouteError(res, error, "无法停止会话。");
|
|
1495
1538
|
}
|
|
1496
1539
|
});
|
|
1540
|
+
// 归档 / 取消归档会话:只写标记,不杀终端、不删历史;保留期扫描再按 archivedAt 清理。
|
|
1541
|
+
app.post("/api/sessions/:id/archive", (req, res) => {
|
|
1542
|
+
try {
|
|
1543
|
+
const updated = sessions.setArchived(req.params.id, true);
|
|
1544
|
+
if (!updated) {
|
|
1545
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
res.json(sessionResponseDTO(updated, req));
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
sendRouteError(res, error, "无法归档会话。");
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
app.post("/api/sessions/:id/unarchive", (req, res) => {
|
|
1555
|
+
try {
|
|
1556
|
+
const updated = sessions.setArchived(req.params.id, false);
|
|
1557
|
+
if (!updated) {
|
|
1558
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
res.json(sessionResponseDTO(updated, req));
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
sendRouteError(res, error, "无法恢复会话。");
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
// 批量归档 / 恢复:与 /api/sessions/batch-delete 对称,返回失败项让客户端保留选择。
|
|
1568
|
+
app.post("/api/sessions/batch-archive", (req, res) => {
|
|
1569
|
+
const body = req.body;
|
|
1570
|
+
const ids = Array.isArray(body?.sessionIds)
|
|
1571
|
+
? body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1572
|
+
: [];
|
|
1573
|
+
if (ids.length === 0) {
|
|
1574
|
+
res.status(400).json({ error: "至少提供一个会话 ID。" });
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const archived = body?.archived !== false;
|
|
1578
|
+
const failed = [];
|
|
1579
|
+
let changed = 0;
|
|
1580
|
+
for (const id of ids) {
|
|
1581
|
+
try {
|
|
1582
|
+
if (sessions.setArchived(id, archived))
|
|
1583
|
+
changed += 1;
|
|
1584
|
+
else
|
|
1585
|
+
failed.push(id);
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
failed.push(id);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
res.json({ ok: true, archived: changed, ...(failed.length > 0 ? { failed } : {}) });
|
|
1592
|
+
});
|
|
1497
1593
|
app.delete("/api/sessions/:id", (req, res) => {
|
|
1498
1594
|
try {
|
|
1499
1595
|
sessions.deleteWithProviderHistory(req.params.id);
|
|
@@ -4,7 +4,8 @@ import { buildInfoPayload } from "./build-info.js";
|
|
|
4
4
|
import { buildChildEnv } from "./env-utils.js";
|
|
5
5
|
import { asyncRoute } from "./express-async.js";
|
|
6
6
|
import { sendRouteError } from "./server-request.js";
|
|
7
|
-
import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
7
|
+
import { getProviderDefaultModels, PREFERENCE_KEYS, PROVIDER_MODEL_PREFERENCE_FIELDS, providerModelFields, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
8
|
+
import { SESSION_PROVIDERS } from "./session-provider.js";
|
|
8
9
|
import { withConfiguredDefaultModelLabels } from "./models.js";
|
|
9
10
|
import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
|
|
10
11
|
function publicConfig(config) {
|
|
@@ -12,13 +13,7 @@ function publicConfig(config) {
|
|
|
12
13
|
const defaultModels = getProviderDefaultModels(config);
|
|
13
14
|
return {
|
|
14
15
|
...safe,
|
|
15
|
-
|
|
16
|
-
defaultCodexModel: defaultModels.codex,
|
|
17
|
-
defaultOpenCodeModel: defaultModels.opencode,
|
|
18
|
-
defaultGrokModel: defaultModels.grok,
|
|
19
|
-
defaultQoderModel: defaultModels.qoder,
|
|
20
|
-
defaultPiModel: defaultModels.pi,
|
|
21
|
-
defaultGeminiModel: defaultModels.gemini,
|
|
16
|
+
...providerModelFields(defaultModels),
|
|
22
17
|
defaultModels,
|
|
23
18
|
};
|
|
24
19
|
}
|
|
@@ -160,20 +155,11 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
160
155
|
if (!body.defaultModels || typeof body.defaultModels !== "object" || Array.isArray(body.defaultModels)) {
|
|
161
156
|
throw new Error("defaultModels 必须是对象。");
|
|
162
157
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
stagePreference("defaultOpenCodeModel", body.defaultModels.opencode);
|
|
169
|
-
if (Object.hasOwn(body.defaultModels, "grok"))
|
|
170
|
-
stagePreference("defaultGrokModel", body.defaultModels.grok);
|
|
171
|
-
if (Object.hasOwn(body.defaultModels, "qoder"))
|
|
172
|
-
stagePreference("defaultQoderModel", body.defaultModels.qoder);
|
|
173
|
-
if (Object.hasOwn(body.defaultModels, "pi"))
|
|
174
|
-
stagePreference("defaultPiModel", body.defaultModels.pi);
|
|
175
|
-
if (Object.hasOwn(body.defaultModels, "gemini"))
|
|
176
|
-
stagePreference("defaultGeminiModel", body.defaultModels.gemini);
|
|
158
|
+
for (const provider of SESSION_PROVIDERS) {
|
|
159
|
+
if (Object.hasOwn(body.defaultModels, provider)) {
|
|
160
|
+
stagePreference(PROVIDER_MODEL_PREFERENCE_FIELDS[provider], body.defaultModels[provider]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
177
163
|
}
|
|
178
164
|
for (const field of PREFERENCE_KEYS) {
|
|
179
165
|
const value = body[field];
|
|
@@ -229,13 +215,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
229
215
|
const defaults = getProviderDefaultModels(config);
|
|
230
216
|
res.json({
|
|
231
217
|
...withConfiguredDefaultModelLabels(cached, defaults),
|
|
232
|
-
|
|
233
|
-
defaultCodexModel: defaults.codex,
|
|
234
|
-
defaultOpenCodeModel: defaults.opencode,
|
|
235
|
-
defaultGrokModel: defaults.grok,
|
|
236
|
-
defaultQoderModel: defaults.qoder,
|
|
237
|
-
defaultPiModel: defaults.pi,
|
|
238
|
-
defaultGeminiModel: defaults.gemini,
|
|
218
|
+
...providerModelFields(defaults),
|
|
239
219
|
defaultModels: defaults,
|
|
240
220
|
});
|
|
241
221
|
});
|
|
@@ -245,13 +225,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
245
225
|
const defaults = getProviderDefaultModels(config);
|
|
246
226
|
res.json({
|
|
247
227
|
...withConfiguredDefaultModelLabels(refreshed, defaults),
|
|
248
|
-
|
|
249
|
-
defaultCodexModel: defaults.codex,
|
|
250
|
-
defaultOpenCodeModel: defaults.opencode,
|
|
251
|
-
defaultGrokModel: defaults.grok,
|
|
252
|
-
defaultQoderModel: defaults.qoder,
|
|
253
|
-
defaultPiModel: defaults.pi,
|
|
254
|
-
defaultGeminiModel: defaults.gemini,
|
|
228
|
+
...providerModelFields(defaults),
|
|
255
229
|
defaultModels: defaults,
|
|
256
230
|
});
|
|
257
231
|
}
|
|
@@ -4,6 +4,7 @@ import { buildInfoPayload } from "./build-info.js";
|
|
|
4
4
|
import { getErrorMessage } from "./error-utils.js";
|
|
5
5
|
import { asyncRoute } from "./express-async.js";
|
|
6
6
|
import { sendRouteError, text } from "./server-request.js";
|
|
7
|
+
import { isSessionProvider } from "./session-provider.js";
|
|
7
8
|
import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
|
|
8
9
|
import { absoluteUrl, buildIosInstallPage, buildIosOtaManifest, buildItmsServicesUrl, collectIosOtaBlockers, formatIosAssetSize, inspectIpa, normalizePublicOrigin, publicOriginFromRequest, } from "./ios-ota.js";
|
|
9
10
|
import { streamFileWithRange } from "./server-file-routes.js";
|
|
@@ -405,7 +406,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
405
406
|
return;
|
|
406
407
|
}
|
|
407
408
|
const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
|
|
408
|
-
const ids = rawIds.filter((value) => value
|
|
409
|
+
const ids = rawIds.filter((value) => isSessionProvider(value));
|
|
409
410
|
state.providerCliUpdateInFlight = true;
|
|
410
411
|
try {
|
|
411
412
|
const before = await refreshProviderCliUpdateState(state, config);
|
|
@@ -35,8 +35,13 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
|
|
|
35
35
|
command: live.command,
|
|
36
36
|
title: team.chat?.chatTitle ?? workspaceSessionTitle(live, names),
|
|
37
37
|
status: live.status,
|
|
38
|
+
completionRevision: live.completionRevision ?? 0,
|
|
39
|
+
viewedCompletionRevision: live.viewedCompletionRevision ?? 0,
|
|
38
40
|
ptyBusy: live.ptyBusy === true,
|
|
41
|
+
archived: live.archived === true,
|
|
42
|
+
archivedAt: live.archivedAt ?? null,
|
|
39
43
|
providerCliActive: live.providerCliActive,
|
|
44
|
+
providerCliExitCode: live.providerCliExitCode,
|
|
40
45
|
inFlight: live.structuredState?.inFlight === true,
|
|
41
46
|
cwd: live.cwd,
|
|
42
47
|
startedAt: live.startedAt,
|
|
@@ -72,6 +77,9 @@ function cheapTasksRevision(storage, registry) {
|
|
|
72
77
|
...(registry?.listSlim() ?? []).map((session) => [
|
|
73
78
|
session.id,
|
|
74
79
|
session.status,
|
|
80
|
+
session.completionRevision ?? 0,
|
|
81
|
+
session.viewedCompletionRevision ?? 0,
|
|
82
|
+
session.archived === true ? 1 : 0,
|
|
75
83
|
session.ptyBusy === true ? 1 : 0,
|
|
76
84
|
session.providerCliActive ? 1 : 0,
|
|
77
85
|
session.structuredState?.inFlight === true ? 1 : 0,
|
package/dist/server.js
CHANGED
|
@@ -16,8 +16,11 @@ import { ModelCatalogService } from "./models.js";
|
|
|
16
16
|
import { ProcessManager } from "./process-manager.js";
|
|
17
17
|
import { SessionLogger } from "./session-logger.js";
|
|
18
18
|
import { SessionRegistry } from "./session-registry.js";
|
|
19
|
+
import { SessionCompletionTracker } from "./session-completion.js";
|
|
19
20
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
20
21
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
22
|
+
import { DecisionService } from "./decision-service.js";
|
|
23
|
+
import { registerDecisionRoutes } from "./server-decision-routes.js";
|
|
21
24
|
import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
|
|
22
25
|
import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
|
|
23
26
|
import { registerSettingsRoutes } from "./server-settings-routes.js";
|
|
@@ -47,6 +50,7 @@ import { asyncRoute, jsonErrorHandler } from "./express-async.js";
|
|
|
47
50
|
import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
|
|
48
51
|
import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
|
|
49
52
|
import { computeRelaunch } from "./relaunch.js";
|
|
53
|
+
import { startRetentionTimer } from "./retention.js";
|
|
50
54
|
import { RuntimeConfigState } from "./runtime-config.js";
|
|
51
55
|
import { safeServiceInstalled } from "./tui/runtime-utils.js";
|
|
52
56
|
import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
|
|
@@ -296,6 +300,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
296
300
|
const storage = new WandStorage(resolveDatabasePath(configPath));
|
|
297
301
|
const runtimeConfig = new RuntimeConfigState(config);
|
|
298
302
|
const authService = new AuthService(storage);
|
|
303
|
+
const decisions = new DecisionService(config.localDecision);
|
|
299
304
|
// 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
|
|
300
305
|
const getCurrentDefaultModels = () => ({
|
|
301
306
|
claude: storage.getPreference("pref:defaultModel", config.defaultModel ?? ""),
|
|
@@ -313,6 +318,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
313
318
|
storage,
|
|
314
319
|
inheritEnv: config.inheritEnv !== false,
|
|
315
320
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
321
|
+
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
316
322
|
...injected,
|
|
317
323
|
piEndpointDiscovery: injected.piEndpointDiscovery ?? { enabled: !testMode },
|
|
318
324
|
configuredClaudeModels: [
|
|
@@ -352,8 +358,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
352
358
|
const legacyStructuredHost = ptyHosts.legacyHost
|
|
353
359
|
?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : null);
|
|
354
360
|
const structuredHosts = await createUpgradeAwareStructuredHost(configPath, legacyStructuredHost, config.structured?.processHost);
|
|
355
|
-
|
|
361
|
+
let decisionRuntime = null;
|
|
362
|
+
const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host, () => decisionRuntime);
|
|
356
363
|
const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
|
|
364
|
+
const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id));
|
|
357
365
|
const missions = new Missions(storage, structuredSessions, sessionRegistry);
|
|
358
366
|
// wsManager 在后面才建:团队运行的变更通知经由这个转发口,接好之前静默丢弃。
|
|
359
367
|
let notifyAiTeamRun = (_data) => { };
|
|
@@ -399,6 +407,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
399
407
|
registerLocalPreviewRoutes(app, { requireAuth, requireFiles });
|
|
400
408
|
// Route-specific parsers must run before the global parser. Once body-parser
|
|
401
409
|
// has consumed a request, a later express.json() cannot tighten or widen it.
|
|
410
|
+
app.use("/api/decisions", express.json({ limit: "32kb" }));
|
|
402
411
|
app.use("/api/optimize-prompt", express.json({ limit: "256kb" }));
|
|
403
412
|
app.use("/api/file-write", express.json({ limit: "2mb" }));
|
|
404
413
|
app.use(express.json({ limit: "1mb" }));
|
|
@@ -593,6 +602,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
593
602
|
app.get("/api/session-check", (req, res) => {
|
|
594
603
|
res.json({ authed: authService.validateSession(readSessionCookie(req, useHttps)) });
|
|
595
604
|
});
|
|
605
|
+
registerDecisionRoutes(app, { storage, decisions, requireAuth, requireSessions });
|
|
596
606
|
app.use("/api", requireAuth);
|
|
597
607
|
// Connected apps receive only the route families used by native clients and
|
|
598
608
|
// the browser extension. Browser-admin sessions implicitly satisfy all scopes.
|
|
@@ -754,7 +764,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
754
764
|
}
|
|
755
765
|
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, sessionRegistry, (cwd) => {
|
|
756
766
|
recordRecentPath(storage, cwd);
|
|
757
|
-
});
|
|
767
|
+
}, (event) => wsManager.emitEvent(event));
|
|
758
768
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
759
769
|
registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
|
|
760
770
|
registerMissionRoutes(app, missions);
|
|
@@ -833,6 +843,9 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
833
843
|
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
834
844
|
const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
835
845
|
const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
|
|
846
|
+
const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
|
|
847
|
+
&& (isSessionProvider(body.provider) || inferProviderFromCommand(command))
|
|
848
|
+
? defaultRoleForCli(storage, provider) : null;
|
|
836
849
|
const snapshot = await (interactiveShell
|
|
837
850
|
? processes.startShell(sessionCwd, body.mode ?? "default", {
|
|
838
851
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
@@ -849,9 +862,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
849
862
|
cols: reqCols,
|
|
850
863
|
rows: reqRows,
|
|
851
864
|
thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
|
|
852
|
-
systemPrompt: body.systemPrompt?.trim() ||
|
|
853
|
-
|
|
854
|
-
|
|
865
|
+
systemPrompt: body.systemPrompt?.trim() || role?.prompt,
|
|
866
|
+
employeeId: role?.id,
|
|
867
|
+
employeeName: role?.name,
|
|
868
|
+
employeeAvatar: role?.avatar,
|
|
855
869
|
workspaceId,
|
|
856
870
|
workspaceTaskId: body.workspaceTaskId,
|
|
857
871
|
...origin,
|
|
@@ -937,11 +951,13 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
937
951
|
});
|
|
938
952
|
// Wire process events to WebSocket broadcast
|
|
939
953
|
processes.on("process", (event) => {
|
|
954
|
+
sessionCompletions.ingest(event);
|
|
940
955
|
missions.ingest(event);
|
|
941
956
|
aiTeams.ingest(event);
|
|
942
957
|
wsManager.emitEvent(event);
|
|
943
958
|
});
|
|
944
959
|
structuredSessions.setEventEmitter((event) => {
|
|
960
|
+
sessionCompletions.ingest(event);
|
|
945
961
|
missions.ingest(event);
|
|
946
962
|
aiTeams.ingest(event);
|
|
947
963
|
wsManager.emitEvent(event);
|
|
@@ -1002,6 +1018,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1002
1018
|
await new Promise((resolve, reject) => {
|
|
1003
1019
|
const cleanupFailedListen = () => {
|
|
1004
1020
|
shuttingDown = true;
|
|
1021
|
+
decisions.dispose();
|
|
1005
1022
|
try {
|
|
1006
1023
|
processes.dispose();
|
|
1007
1024
|
}
|
|
@@ -1052,6 +1069,11 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1052
1069
|
const address = server.address();
|
|
1053
1070
|
const actualPort = typeof address === "object" && address ? address.port : config.port;
|
|
1054
1071
|
bindAddr = `${config.host}:${actualPort}`;
|
|
1072
|
+
if (config.localDecision?.enabled) {
|
|
1073
|
+
const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
|
|
1074
|
+
decisionRuntime = { url: `${protocol}://${localHost}:${actualPort}`,
|
|
1075
|
+
...(activeSslCertPath ? { caPath: activeSslCertPath } : {}) };
|
|
1076
|
+
}
|
|
1055
1077
|
const scheme = useHttps ? "HTTPS" : "HTTP";
|
|
1056
1078
|
// 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
|
|
1057
1079
|
collectedUrls.push({ url: `${protocol}://127.0.0.1:${actualPort}`, scheme });
|
|
@@ -1095,6 +1117,11 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1095
1117
|
}, MODEL_CATALOG_AUTO_REFRESH_INTERVAL_MS);
|
|
1096
1118
|
modelCatalogRefreshTimer.unref();
|
|
1097
1119
|
}
|
|
1120
|
+
// 保留期扫描:7 天没活动的会话/任务自动归档,归档 7 天后自动清理。
|
|
1121
|
+
let retentionTimer = null;
|
|
1122
|
+
if (!testMode) {
|
|
1123
|
+
retentionTimer = startRetentionTimer({ storage, sessions: sessionRegistry });
|
|
1124
|
+
}
|
|
1098
1125
|
// Express 4 does not forward rejected route promises automatically. Every
|
|
1099
1126
|
// async route above is wrapped with asyncRoute, and this final middleware
|
|
1100
1127
|
// keeps parser, synchronous middleware, and async failures JSON-shaped.
|
|
@@ -1241,6 +1268,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1241
1268
|
return closePromise;
|
|
1242
1269
|
closePromise = (async () => {
|
|
1243
1270
|
shuttingDown = true;
|
|
1271
|
+
decisions.dispose();
|
|
1244
1272
|
await userMemory.dispose();
|
|
1245
1273
|
if (updateCheckTimer) {
|
|
1246
1274
|
clearInterval(updateCheckTimer);
|
|
@@ -1254,6 +1282,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1254
1282
|
clearInterval(modelCatalogRefreshTimer);
|
|
1255
1283
|
modelCatalogRefreshTimer = null;
|
|
1256
1284
|
}
|
|
1285
|
+
if (retentionTimer) {
|
|
1286
|
+
clearInterval(retentionTimer);
|
|
1287
|
+
retentionTimer = null;
|
|
1288
|
+
}
|
|
1257
1289
|
// Stop accepting requests first. Existing requests get a short grace
|
|
1258
1290
|
// period while managers flush and active runners are cancelled.
|
|
1259
1291
|
const serverClosed = new Promise((resolve) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SessionCompletionState {
|
|
2
|
+
/** Monotonic successful-completion generation, owned by storage rather than runner checkpoints. */
|
|
3
|
+
completionRevision?: number;
|
|
4
|
+
viewedCompletionRevision?: number;
|
|
5
|
+
}
|
|
6
|
+
/** Explicit navigation, not a list refresh choosing its preferred/default session. */
|
|
7
|
+
export declare function createSessionCompletionViewIntent(): {
|
|
8
|
+
open(id: string): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
isOpen(id: string): boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
|
|
13
|
+
export declare function mergeSessionCompletionState(current: SessionCompletionState | null | undefined, incoming: SessionCompletionState): SessionCompletionState;
|
|
14
|
+
/** A presentation state only: never change the runner's lifecycle or input eligibility. */
|
|
15
|
+
export declare function isSessionJustCompleted(session: SessionCompletionState & {
|
|
16
|
+
status?: string;
|
|
17
|
+
archived?: boolean;
|
|
18
|
+
ptyBusy?: boolean;
|
|
19
|
+
inFlight?: boolean;
|
|
20
|
+
structuredState?: {
|
|
21
|
+
inFlight?: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
permissionBlocked?: boolean;
|
|
24
|
+
pendingEscalation?: unknown;
|
|
25
|
+
providerCliExitCode?: number | null;
|
|
26
|
+
}): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Explicit navigation, not a list refresh choosing its preferred/default session. */
|
|
2
|
+
export function createSessionCompletionViewIntent() {
|
|
3
|
+
let openedId = null;
|
|
4
|
+
return {
|
|
5
|
+
open(id) { openedId = id; },
|
|
6
|
+
clear() { openedId = null; },
|
|
7
|
+
isOpen(id) { return openedId === id; },
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
|
|
11
|
+
export function mergeSessionCompletionState(current, incoming) {
|
|
12
|
+
if (incoming.completionRevision === undefined && incoming.viewedCompletionRevision === undefined)
|
|
13
|
+
return {};
|
|
14
|
+
return {
|
|
15
|
+
completionRevision: Math.max(current?.completionRevision ?? 0, incoming.completionRevision ?? 0),
|
|
16
|
+
viewedCompletionRevision: Math.max(current?.viewedCompletionRevision ?? 0, incoming.viewedCompletionRevision ?? 0),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** A presentation state only: never change the runner's lifecycle or input eligibility. */
|
|
20
|
+
export function isSessionJustCompleted(session) {
|
|
21
|
+
return (session.completionRevision ?? 0) > (session.viewedCompletionRevision ?? 0)
|
|
22
|
+
&& !session.archived
|
|
23
|
+
&& !session.ptyBusy
|
|
24
|
+
&& !session.inFlight
|
|
25
|
+
&& !session.structuredState?.inFlight
|
|
26
|
+
&& !session.permissionBlocked
|
|
27
|
+
&& !session.pendingEscalation
|
|
28
|
+
&& !(typeof session.providerCliExitCode === "number" && session.providerCliExitCode !== 0)
|
|
29
|
+
&& ["idle", "running", "exited"].includes(session.status ?? "");
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WandStorage } from "./storage.js";
|
|
2
|
+
import type { ProcessEvent, SessionSnapshot } from "./types.js";
|
|
3
|
+
/** Observe completion facts without combining PTY and structured execution implementations. */
|
|
4
|
+
export declare class SessionCompletionTracker {
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private readonly getSession;
|
|
7
|
+
private readonly busyPty;
|
|
8
|
+
private readonly completedPty;
|
|
9
|
+
private readonly ended;
|
|
10
|
+
constructor(storage: WandStorage, getSession: (id: string) => SessionSnapshot | null);
|
|
11
|
+
ingest(event: ProcessEvent): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Observe completion facts without combining PTY and structured execution implementations. */
|
|
2
|
+
export class SessionCompletionTracker {
|
|
3
|
+
storage;
|
|
4
|
+
getSession;
|
|
5
|
+
busyPty = new Set();
|
|
6
|
+
completedPty = new Set();
|
|
7
|
+
ended = new Map();
|
|
8
|
+
constructor(storage, getSession) {
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
this.getSession = getSession;
|
|
11
|
+
}
|
|
12
|
+
ingest(event) {
|
|
13
|
+
if (event.type === "started") {
|
|
14
|
+
this.busyPty.delete(event.sessionId);
|
|
15
|
+
this.completedPty.delete(event.sessionId);
|
|
16
|
+
this.ended.delete(event.sessionId);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (event.type !== "status" && event.type !== "ended")
|
|
20
|
+
return;
|
|
21
|
+
const data = event.data;
|
|
22
|
+
if (!data)
|
|
23
|
+
return;
|
|
24
|
+
const snapshot = this.getSession(event.sessionId);
|
|
25
|
+
if (!snapshot)
|
|
26
|
+
return;
|
|
27
|
+
let completed = false;
|
|
28
|
+
if ((snapshot.sessionKind ?? "pty") === "structured") {
|
|
29
|
+
if (event.type === "ended" && snapshot.status === "idle" && snapshot.exitCode === 0
|
|
30
|
+
&& !snapshot.structuredState?.inFlight && !snapshot.structuredState?.lastError) {
|
|
31
|
+
const key = snapshot.endedAt ?? "";
|
|
32
|
+
completed = !!key && this.ended.get(snapshot.id) !== key;
|
|
33
|
+
if (completed)
|
|
34
|
+
this.ended.set(snapshot.id, key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (data.ptyBusy === true) {
|
|
39
|
+
this.busyPty.add(snapshot.id);
|
|
40
|
+
this.completedPty.delete(snapshot.id);
|
|
41
|
+
}
|
|
42
|
+
else if (data.ptyBusy === false && this.busyPty.delete(snapshot.id)) {
|
|
43
|
+
completed = !snapshot.permissionBlocked && !snapshot.pendingEscalation
|
|
44
|
+
&& snapshot.status === "running"
|
|
45
|
+
&& !(typeof snapshot.providerCliExitCode === "number" && snapshot.providerCliExitCode !== 0);
|
|
46
|
+
}
|
|
47
|
+
if ((data.providerCliActive === false && data.providerCliExitCode === 0)
|
|
48
|
+
|| (event.type === "ended" && snapshot.status === "exited" && snapshot.exitCode === 0)) {
|
|
49
|
+
completed ||= !this.completedPty.has(snapshot.id);
|
|
50
|
+
}
|
|
51
|
+
if (completed)
|
|
52
|
+
this.completedPty.add(snapshot.id);
|
|
53
|
+
if (event.type === "ended")
|
|
54
|
+
this.busyPty.delete(snapshot.id);
|
|
55
|
+
}
|
|
56
|
+
if (completed) {
|
|
57
|
+
const completion = this.storage.recordSessionCompletion(snapshot.id);
|
|
58
|
+
if (completion)
|
|
59
|
+
Object.assign(data, completion);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { SessionProvider } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
|
|
4
|
-
/** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
|
|
5
|
-
export declare function isSessionProvider(value: unknown): value is SessionProvider;
|
|
6
|
-
/** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
|
|
7
|
-
export declare function providerCliCommand(provider: SessionProvider): string;
|
|
2
|
+
export { SESSION_PROVIDERS, isSessionProvider, providerCliCommand, providerDisplayName, inferProviderFromRunner, inferProviderFromCommand, normalizeProviderId, } from "./provider-catalog.js";
|
|
8
3
|
/**
|
|
9
4
|
* 不启动 shell 也能判断某个 provider 的 CLI 是否在 PATH 上(与子进程 PATH 一致)。
|
|
10
5
|
* 用于多候选降级时跳过没安装的工具,避免白等一次 spawn 失败。
|
|
11
6
|
*/
|
|
12
7
|
export declare function providerCliInstalled(provider: SessionProvider, pathValue?: string): boolean;
|
|
13
|
-
/** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
|
|
14
|
-
export declare function inferProviderFromRunner(runner: unknown): SessionProvider | undefined;
|
|
15
|
-
/**
|
|
16
|
-
* 从启动命令推断 provider;无法识别时返回 undefined,由调用方决定默认值。
|
|
17
|
-
*
|
|
18
|
-
* 识别的是命令行的第一个 token:`claude`、`npx claude`、任意路径下的 `claude`
|
|
19
|
-
* 都算 Claude,其余 provider 要求命令以其 CLI 名开头。
|
|
20
|
-
*/
|
|
21
|
-
export declare function inferProviderFromCommand(command: string): SessionProvider | undefined;
|