@co0ontty/wand 4.85.1 → 4.87.0-beta.gd2f0f8f

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 (108) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -3
  3. package/dist/ai-team-runner.js +1 -0
  4. package/dist/ai-team-types.d.ts +1 -0
  5. package/dist/ai-team-types.js +6 -2
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +12 -6
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +33 -0
  10. package/dist/core-runner.js +1 -1
  11. package/dist/daemon-maintenance-state.d.ts +6 -0
  12. package/dist/daemon-maintenance-state.js +9 -0
  13. package/dist/daemon-maintenance-targets.d.ts +20 -0
  14. package/dist/daemon-maintenance-targets.js +183 -0
  15. package/dist/daemon-maintenance.d.ts +53 -0
  16. package/dist/daemon-maintenance.js +164 -0
  17. package/dist/decision-expert-employee.d.ts +6 -0
  18. package/dist/decision-expert-employee.js +22 -0
  19. package/dist/decision-expert-identity.d.ts +7 -0
  20. package/dist/decision-expert-identity.js +7 -0
  21. package/dist/decision-hardware.d.ts +19 -0
  22. package/dist/decision-hardware.js +19 -0
  23. package/dist/decision-service.d.ts +6 -1
  24. package/dist/decision-service.js +60 -1
  25. package/dist/decision-types.d.ts +1 -0
  26. package/dist/decision-types.js +12 -0
  27. package/dist/git-quick-commit.js +1 -1
  28. package/dist/laya-model-manifest.d.ts +11 -0
  29. package/dist/laya-model-manifest.js +18 -0
  30. package/dist/local-model-setup.d.ts +46 -0
  31. package/dist/local-model-setup.js +248 -0
  32. package/dist/local-model-types.d.ts +34 -0
  33. package/dist/local-model-types.js +1 -0
  34. package/dist/message-truncator.d.ts +4 -2
  35. package/dist/message-truncator.js +20 -6
  36. package/dist/missions.js +6 -3
  37. package/dist/model-file-store.d.ts +13 -0
  38. package/dist/model-file-store.js +108 -0
  39. package/dist/model-groups.js +2 -2
  40. package/dist/model-setup-process.d.ts +2 -0
  41. package/dist/model-setup-process.js +80 -0
  42. package/dist/models.js +3 -3
  43. package/dist/process-manager.js +21 -8
  44. package/dist/provider-catalog.d.ts +13 -1
  45. package/dist/provider-catalog.js +14 -2
  46. package/dist/render-binary.d.ts +1 -1
  47. package/dist/render-binary.js +4 -4
  48. package/dist/render-daemon-client.d.ts +12 -1
  49. package/dist/render-daemon-client.js +72 -4
  50. package/dist/render-host.d.ts +1 -0
  51. package/dist/render-host.js +9 -56
  52. package/dist/render-structured-host.d.ts +1 -0
  53. package/dist/render-structured-host.js +2 -2
  54. package/dist/server-employee-routes.js +3 -3
  55. package/dist/server-local-model-routes.d.ts +8 -0
  56. package/dist/server-local-model-routes.js +62 -0
  57. package/dist/server-resume-routes.js +1 -1
  58. package/dist/server-session-routes.js +34 -8
  59. package/dist/server-settings-routes.js +2 -0
  60. package/dist/server-speech-routes.d.ts +8 -0
  61. package/dist/server-speech-routes.js +87 -0
  62. package/dist/server-task-routes.js +3 -1
  63. package/dist/server-workspace-routes.js +2 -0
  64. package/dist/server.js +63 -18
  65. package/dist/session-completion-state.d.ts +1 -0
  66. package/dist/session-completion-state.js +1 -0
  67. package/dist/session-transport.js +1 -0
  68. package/dist/silicon-employee-dispatch.js +5 -2
  69. package/dist/silicon-employee-draft.js +1 -1
  70. package/dist/speech-models.d.ts +24 -0
  71. package/dist/speech-models.js +16 -0
  72. package/dist/speech-service.d.ts +57 -0
  73. package/dist/speech-service.js +399 -0
  74. package/dist/speech-types.d.ts +47 -0
  75. package/dist/speech-types.js +3 -0
  76. package/dist/storage.d.ts +2 -0
  77. package/dist/storage.js +13 -0
  78. package/dist/structured-failure.js +1 -1
  79. package/dist/structured-session-manager.d.ts +15 -2
  80. package/dist/structured-session-manager.js +58 -19
  81. package/dist/terminal-daemon-build.d.ts +4 -0
  82. package/dist/terminal-daemon-build.js +54 -0
  83. package/dist/terminal-daemon-protocol.d.ts +1 -1
  84. package/dist/terminal-daemon-server.js +20 -1
  85. package/dist/terminal-host.d.ts +4 -0
  86. package/dist/terminal-host.js +7 -4
  87. package/dist/tui/commands.js +1 -1
  88. package/dist/types.d.ts +14 -2
  89. package/dist/web-ui/chat-history-window.d.ts +23 -0
  90. package/dist/web-ui/chat-history-window.js +29 -0
  91. package/dist/web-ui/content/ai-teams.js +12 -12
  92. package/dist/web-ui/content/scripts.js +316 -233
  93. package/dist/web-ui/content/styles.css +1 -1
  94. package/dist/web-ui/content/tailwind.css +1 -1
  95. package/dist/web-ui/embedded-assets.js +3 -3
  96. package/dist/web-ui/index.js +3 -3
  97. package/dist/web-ui/provider-identity.d.ts +28 -1
  98. package/dist/web-ui/provider-identity.js +31 -2
  99. package/dist/web-ui/session-activity.d.ts +1 -0
  100. package/dist/web-ui/session-activity.js +5 -2
  101. package/dist/web-ui/speech-audio.d.ts +2 -0
  102. package/dist/web-ui/speech-audio.js +41 -0
  103. package/dist/web-ui/vendor-loader.d.ts +2 -0
  104. package/dist/web-ui/vendor-loader.js +50 -0
  105. package/dist/ws-broadcast.js +10 -4
  106. package/package.json +7 -3
  107. package/scripts/install-laya-runtime.js +67 -0
  108. package/scripts/install-speech-runtime.js +82 -0
@@ -4,7 +4,7 @@ import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
4
4
  import { defaultRoleForCli } from "./default-employee.js";
5
5
  import { defaultModelGroupSelector } from "./model-groups.js";
6
6
  import { inspectPiExecution } from "./pi-execution.js";
7
- import { alignedBlockStart, blockWindowMessagesForTransport, compactToolMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
7
+ import { alignedBlockStart, blockWindowMessagesForTransport, compactToolMessagesForTransport, messageWindowByteBudget, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
8
8
  import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
9
9
  import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
10
10
  import { resolveSessionCwd } from "./session-cwd.js";
@@ -173,6 +173,7 @@ function sessionListRevision(entries, directoryNames = []) {
173
173
  entry.session.completionRevision ?? 0,
174
174
  entry.session.viewedCompletionRevision ?? 0,
175
175
  entry.session.ptyBusy === true,
176
+ entry.session.ptyCommandRunning === true,
176
177
  entry.session.structuredState?.inFlight === true,
177
178
  ]);
178
179
  const revisionState = directoryNames.length === 0
@@ -489,6 +490,21 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
489
490
  const provider = requestedProvider
490
491
  ?? employeeAgent?.provider
491
492
  ?? (isSessionProvider(body.provider) ? body.provider : config.defaultProvider ?? "claude");
493
+ // 引擎是第二个维度:Pi CLI(cli)与 Wand Agent(sdk)都是 provider=pi,但执行方式不同。
494
+ // 员工候选自己带引擎;只有直选 CLI(含员工面板里的「指定 CLI」)才接受请求里的 engine。
495
+ const rawEngine = typeof body.engine === "string" ? body.engine.trim() : "";
496
+ if (rawEngine && rawEngine !== "cli" && rawEngine !== "sdk") {
497
+ res.status(400).json({ error: "请选择有效的执行引擎。" });
498
+ return;
499
+ }
500
+ const requestedEngine = (employee && !requestedProvider) || !rawEngine
501
+ ? undefined
502
+ : rawEngine;
503
+ if (requestedEngine === "sdk" && provider !== "pi") {
504
+ res.status(400).json({ error: "Wand Agent 只支持 Pi 结构化会话。" });
505
+ return;
506
+ }
507
+ const engineDecision = requestedEngine ? structured.resolveNewSessionPiEngine(requestedEngine) : null;
492
508
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
493
509
  const origin = parseSessionCreationOrigin(body);
494
510
  // Explicit employees/custom role rules and internal automation retain their own role.
@@ -523,6 +539,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
523
539
  employeeAvatar: role?.avatar,
524
540
  employeeCandidates: employee?.agents,
525
541
  employeeCandidateIndex: requestedProvider ? undefined : selectedEmployeeCandidate?.index,
542
+ ...(engineDecision ? { engine: engineDecision.engine } : {}),
526
543
  ...origin,
527
544
  });
528
545
  onSessionCreated?.(snapshot.cwd);
@@ -582,8 +599,17 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
582
599
  });
583
600
  app.post("/api/sessions/:id/provider", (req, res) => {
584
601
  const provider = req.body?.provider;
602
+ const engine = req.body?.engine;
603
+ if (engine !== undefined && engine !== "cli" && engine !== "sdk") {
604
+ res.status(400).json({ error: "执行引擎必须是 cli 或 sdk。" });
605
+ return;
606
+ }
607
+ if (engine === "sdk" && provider !== "pi") {
608
+ res.status(400).json({ error: "Wand Agent 只支持 Pi 结构化会话。" });
609
+ return;
610
+ }
585
611
  if (!isSessionProvider(provider)) {
586
- res.status(400).json({ error: "请选择有效的 CLI 工具。" });
612
+ res.status(400).json({ error: "请选择有效的执行工具。" });
587
613
  return;
588
614
  }
589
615
  const id = req.params.id;
@@ -597,7 +623,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
597
623
  return;
598
624
  }
599
625
  try {
600
- res.json(sessionResponseDTO(structured.setSessionProvider(id, provider), req));
626
+ res.json(sessionResponseDTO(structured.setSessionProvider(id, provider, engine), req));
601
627
  }
602
628
  catch (error) {
603
629
  sendRouteError(res, error, "切换工具失败。");
@@ -614,7 +640,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
614
640
  const localDecisionAvailable = config.localDecision?.enabled === true;
615
641
  const inventory = await discoverPiResources(config, snapshot.cwd);
616
642
  const resourceCatalog = { ...inventory.catalog, supported: modifiable && !sdk && inventory.catalog.supported,
617
- reason: archived ? "请先恢复已归档的会话。" : sdk ? "当前 SDK 候选尚不支持 Skills / MCP 会话选择。" : inventory.catalog.reason };
643
+ reason: archived ? "请先恢复已归档的会话。" : sdk ? "Wand Agent 尚不支持 Skills / MCP 会话选择。" : inventory.catalog.reason };
618
644
  const controls = {
619
645
  // 基础工具与 CodeMode 在两种引擎下都能表达(CLI 通过 --tools 白名单)。
620
646
  tools: modifiable,
@@ -1346,7 +1372,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1346
1372
  }
1347
1373
  const transcriptOutput = (snapshot.sessionKind ?? "pty") === "pty"
1348
1374
  ? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
1349
- : snapshot.output;
1375
+ : req.query.format === "chat" && req.query.output === "omit" ? "" : snapshot.output;
1350
1376
  const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
1351
1377
  const messages = wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched;
1352
1378
  if (req.query.format === "chat") {
@@ -1355,7 +1381,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1355
1381
  const rawBudget = req.query.blockBudget;
1356
1382
  if (typeof rawBudget === "string" && /^\d+$/.test(rawBudget) && Number(rawBudget) > 0) {
1357
1383
  const blockBudget = parseBoundedInteger(rawBudget, 1, 1, 2_000);
1358
- const windowed = blockWindowMessagesForTransport(messages, config.cardDefaults ?? {}, blockBudget);
1384
+ const windowed = blockWindowMessagesForTransport(messages, config.cardDefaults ?? {}, blockBudget, messageWindowByteBudget(req.query.byteBudget), typeof req.query.byteBudget === "string");
1359
1385
  res.json(toSessionDetailDTO(snapshot, {
1360
1386
  output: transcriptOutput,
1361
1387
  messages: windowed.messages,
@@ -1398,7 +1424,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1398
1424
  && typeof req.query.blockBudget === "string" && /^\d+$/.test(req.query.blockBudget)) {
1399
1425
  const before = parseBoundedInteger(req.query.before, total, 0, total);
1400
1426
  const budget = parseBoundedInteger(req.query.blockBudget, 60, 1, 200);
1401
- const windowed = blockWindowMessagesForTransport(all.slice(0, before), config.cardDefaults ?? {}, budget);
1427
+ const windowed = blockWindowMessagesForTransport(all.slice(0, before), config.cardDefaults ?? {}, budget, messageWindowByteBudget(req.query.byteBudget), typeof req.query.byteBudget === "string");
1402
1428
  res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, ...windowed,
1403
1429
  offset: windowed.messageOffset, total });
1404
1430
  return;
@@ -1422,7 +1448,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1422
1448
  const blockEnd = Math.min(Math.max(Number.isFinite(rawBlockOffset) ? rawBlockOffset : blockTotal, 0), blockTotal);
1423
1449
  const blockStart = Math.max(0, blockEnd - blockLimit);
1424
1450
  const cardDefaults = config.cardDefaults ?? {};
1425
- const startOffset = alignedBlockStart(turn.content, cardDefaults, blockStart, blockEnd);
1451
+ const startOffset = alignedBlockStart(turn.content, cardDefaults, blockStart, blockEnd, Math.min(128 * 1024, messageWindowByteBudget(req.query.byteBudget)));
1426
1452
  const blocks = enrichStructuredMessages([{
1427
1453
  ...turn,
1428
1454
  content: sliceTurnBlocksForTransport(turn, startOffset, blockEnd, cardDefaults),
@@ -127,6 +127,8 @@ export function registerSettingsRoutes(app, deps) {
127
127
  };
128
128
  let touchedDeployField = false;
129
129
  try {
130
+ if (Object.hasOwn(body, "localDecision"))
131
+ throw new Error("请使用本地模型设置管理 LAYA;通用配置不接受运行时路径或命令。");
130
132
  if (body.modelGroups !== undefined && body.expectedModelGroups !== undefined
131
133
  && JSON.stringify(normalizeModelGroups(body.expectedModelGroups)) !== JSON.stringify(config.modelGroups ?? [])) {
132
134
  res.status(409).json({ error: "模型分组已在其他设备修改,请先刷新;当前草稿已保留。" });
@@ -0,0 +1,8 @@
1
+ import { type Express, type RequestHandler } from "express";
2
+ import { type SpeechService } from "./speech-service.js";
3
+ /** Mounted behind /api authentication. Clients may transcribe, only admins configure/download. */
4
+ export declare function registerSpeechRoutes(app: Express, deps: {
5
+ speech: SpeechService;
6
+ requireSessions: RequestHandler;
7
+ requireAdmin: RequestHandler;
8
+ }): void;
@@ -0,0 +1,87 @@
1
+ import express from "express";
2
+ import { SpeechError } from "./speech-service.js";
3
+ import { SPEECH_MAX_WAV_BYTES } from "./speech-types.js";
4
+ /** Mounted behind /api authentication. Clients may transcribe, only admins configure/download. */
5
+ export function registerSpeechRoutes(app, deps) {
6
+ const noStore = (_req, res, next) => { res.set("Cache-Control", "no-store"); next(); };
7
+ app.use("/api/speech", noStore, deps.requireSessions);
8
+ app.get("/api/speech/status", async (_req, res, next) => {
9
+ try {
10
+ res.json(await deps.speech.status());
11
+ }
12
+ catch (error) {
13
+ next(error);
14
+ }
15
+ });
16
+ app.patch("/api/speech/settings", deps.requireAdmin, async (req, res, next) => {
17
+ try {
18
+ deps.speech.configure(req.body);
19
+ res.json(await deps.speech.status());
20
+ }
21
+ catch (error) {
22
+ next(error);
23
+ }
24
+ });
25
+ app.post("/api/speech/models/:id/download", deps.requireAdmin, async (req, res, next) => {
26
+ try {
27
+ await deps.speech.startDownload(req.params.id);
28
+ res.status(202).json(await deps.speech.status());
29
+ }
30
+ catch (error) {
31
+ next(error);
32
+ }
33
+ });
34
+ // Bound raw uploads as well as inference; don't buffer unbounded concurrent audio requests.
35
+ let uploads = 0;
36
+ const admission = (req, res, next) => {
37
+ if (!req.is("audio/wav")) {
38
+ res.status(415).json({ error: "请上传 audio/wav 音频。", code: "INVALID_AUDIO_TYPE" });
39
+ return;
40
+ }
41
+ if (uploads >= 2) {
42
+ res.status(429).json({ error: "语音服务繁忙,请稍后重试。", code: "BUSY" });
43
+ return;
44
+ }
45
+ uploads += 1;
46
+ let released = false;
47
+ const release = () => { if (!released) {
48
+ released = true;
49
+ uploads -= 1;
50
+ } };
51
+ res.once("finish", release);
52
+ res.once("close", release);
53
+ next();
54
+ };
55
+ app.post("/api/speech/transcribe", admission, express.raw({ type: "audio/wav", limit: SPEECH_MAX_WAV_BYTES }), async (req, res, next) => {
56
+ const abort = new AbortController();
57
+ const disconnected = () => { if (!res.writableEnded)
58
+ abort.abort(); };
59
+ res.once("close", disconnected);
60
+ try {
61
+ if (!Buffer.isBuffer(req.body))
62
+ throw new SpeechError("INVALID_AUDIO", "未收到 WAV 音频。");
63
+ const result = await deps.speech.transcribe(req.body, abort.signal);
64
+ if (!res.destroyed)
65
+ res.json(result);
66
+ }
67
+ catch (error) {
68
+ if (!res.destroyed)
69
+ next(error);
70
+ }
71
+ finally {
72
+ res.off("close", disconnected);
73
+ }
74
+ });
75
+ const errors = (error, _req, res, next) => {
76
+ if (error instanceof SpeechError) {
77
+ res.status(error.status).json({ error: error.message, code: error.code });
78
+ return;
79
+ }
80
+ if (error?.type === "entity.too.large") {
81
+ res.status(413).json({ error: "语音最长 60 秒。", code: "AUDIO_TOO_LARGE" });
82
+ return;
83
+ }
84
+ next(error);
85
+ };
86
+ app.use("/api/speech", errors);
87
+ }
@@ -193,7 +193,7 @@ export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MOD
193
193
  if (!isWandTaskAgentEngine(rawEngine))
194
194
  throw new Error("执行引擎无效。");
195
195
  if (rawEngine === "sdk" && (provider !== "pi" || rawKind !== "structured")) {
196
- throw new Error("SDK 执行引擎仅支持 Pi 结构化会话。");
196
+ throw new Error("Wand Agent 仅支持结构化会话。");
197
197
  }
198
198
  const engine = rawEngine === "sdk" ? rawEngine : undefined;
199
199
  return { provider, model, thinkingEffort, mode, kind: rawKind, ...(engine ? { engine } : {}) };
@@ -232,6 +232,8 @@ function taskSessionSummary(session) {
232
232
  thinkingEffort: session.thinkingEffort || "off",
233
233
  // 会话实际跑的执行模式;旧会话没有该字段时留空,前端回落到任务上的配置。
234
234
  mode: session.mode || "",
235
+ // 执行引擎:core = Wand Agent(进程内 SDK);pi 会话没跑过时按 CLI 读。
236
+ engine: session.provider === "pi" ? (session.structuredState?.engine === "core" ? "sdk" : "cli") : undefined,
235
237
  employeeId: session.employeeId,
236
238
  employeeName: session.employeeName,
237
239
  employeeAvatar: session.employeeAvatar,
@@ -38,6 +38,7 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
38
38
  completionRevision: live.completionRevision ?? 0,
39
39
  viewedCompletionRevision: live.viewedCompletionRevision ?? 0,
40
40
  ptyBusy: live.ptyBusy === true,
41
+ ptyCommandRunning: live.ptyCommandRunning === true,
41
42
  archived: live.archived === true,
42
43
  archivedAt: live.archivedAt ?? null,
43
44
  providerCliActive: live.providerCliActive,
@@ -84,6 +85,7 @@ function cheapTasksRevision(storage, registry, includeArchived = false) {
84
85
  session.viewedCompletionRevision ?? 0,
85
86
  session.archived === true ? 1 : 0,
86
87
  session.ptyBusy === true ? 1 : 0,
88
+ session.ptyCommandRunning === true ? 1 : 0,
87
89
  session.providerCliActive ? 1 : 0,
88
90
  session.structuredState?.inFlight === true ? 1 : 0,
89
91
  session.title ?? "",
package/dist/server.js CHANGED
@@ -12,7 +12,7 @@ import process from "node:process";
12
12
  import { WebSocketServer } from "ws";
13
13
  import { AuthService, BROWSER_ADMIN_PRINCIPAL, CONNECTED_APP_PRINCIPAL, principalHasScope, readSessionCookie, SESSION_COOKIE_HTTP, SESSION_COOKIE_HTTPS, SESSION_COOKIE_LEGACY, } from "./auth.js";
14
14
  import { ensureCertificates } from "./cert.js";
15
- import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, } from "./config.js";
15
+ import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, writePreferenceToStorage, } from "./config.js";
16
16
  import { ModelCatalogService } from "./models.js";
17
17
  import { defaultModelGroupSelector } from "./model-groups.js";
18
18
  import { ProcessManager } from "./process-manager.js";
@@ -28,6 +28,10 @@ import { registerDecisionRoutes } from "./server-decision-routes.js";
28
28
  import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
29
29
  import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
30
30
  import { registerSettingsRoutes } from "./server-settings-routes.js";
31
+ import { registerSpeechRoutes } from "./server-speech-routes.js";
32
+ import { registerLocalModelRoutes } from "./server-local-model-routes.js";
33
+ import { LocalModelSetupService } from "./local-model-setup.js";
34
+ import { SpeechService } from "./speech-service.js";
31
35
  import { appTokenLoginPayload, buildStructuredChatPersonaPayload, isBrowserExtensionOrigin, resolveAppConnectCode, resolveStructuredChatAvatarPath, verifyAppToken, } from "./server-app-connect.js";
32
36
  import { firstHeaderValue, sendRouteError } from "./server-request.js";
33
37
  import { inferProviderFromCommand, isSessionProvider } from "./session-provider.js";
@@ -78,6 +82,9 @@ import { getStylesAsset } from "./web-ui/styles.js";
78
82
  import { WsBroadcastManager } from "./ws-broadcast.js";
79
83
  import { TerminalDaemonClient } from "./terminal-daemon-client.js";
80
84
  import { createUpgradeAwareTerminalHost } from "./render-host.js";
85
+ import { DaemonAdmission, DaemonMaintenance } from "./daemon-maintenance.js";
86
+ import { createDaemonMaintenanceTargets } from "./daemon-maintenance-targets.js";
87
+ import { terminalDaemonBuildIsCurrent } from "./terminal-daemon-build.js";
81
88
  import { createUpgradeAwareStructuredHost } from "./render-structured-host.js";
82
89
  import { checkPasswordRateLimit, recordFailedPassword, resetPasswordRateLimit } from "./middleware/rate-limit.js";
83
90
  import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
@@ -88,7 +95,7 @@ const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
88
95
  const PKG_JSON = JSON.parse(readFileSync(path.join(RUNTIME_ROOT_DIR, "package.json"), "utf8"));
89
96
  const PKG_NAME = PKG_JSON.name;
90
97
  const PKG_VERSION = PKG_JSON.version;
91
- const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=26.10.0";
98
+ const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=24.21.0";
92
99
  const PKG_REPO_URL = "https://github.com/co0ontty/wand";
93
100
  /** 结构化聊天头像允许的图片类型;未知扩展名回 415。 */
94
101
  const AVATAR_CONTENT_TYPES = {
@@ -182,6 +189,7 @@ const CONNECTED_APP_PREFERENCE_KEYS = new Set([
182
189
  "defaultThinkingEffort",
183
190
  "defaultProvider",
184
191
  "defaultSessionKind",
192
+ "defaultEngine",
185
193
  "defaultTaskWorktree",
186
194
  ]);
187
195
  function requireAdminOrSessionPreferences(req, res, next) {
@@ -351,6 +359,18 @@ export async function startServer(config, configPath, options = {}) {
351
359
  // Clients receive its persisted snapshot via GET /api/models.
352
360
  const modelCatalog = new ModelCatalogService(getModelRefreshOptions);
353
361
  const configDir = resolveConfigDir(configPath);
362
+ const speech = new SpeechService(storage, configDir);
363
+ const localModels = new LocalModelSetupService({ configDir, speech, decisions,
364
+ decisionConfig: () => config.localDecision ?? { enabled: false, pythonPath: "", modelPath: "" },
365
+ configureDecision(next) {
366
+ decisions.assertConfigurable();
367
+ const candidate = runtimeConfig.createCandidate();
368
+ writePreferenceToStorage(candidate, storage, "localDecision", next);
369
+ decisions.configure(next);
370
+ runtimeConfig.commit(candidate, ["localDecision"]);
371
+ refreshDecisionRuntime();
372
+ },
373
+ });
354
374
  const distributionManager = new DistributionManager({
355
375
  configDir,
356
376
  configPath,
@@ -369,7 +389,8 @@ export async function startServer(config, configPath, options = {}) {
369
389
  binaryPath: config.render?.binaryPath,
370
390
  knownSessionIds: knownPtySessionIds,
371
391
  });
372
- const terminalHost = ptyHosts.host;
392
+ const daemonAdmission = new DaemonAdmission();
393
+ const terminalHost = daemonAdmission.terminal(ptyHosts.host);
373
394
  let decisionRuntime = null;
374
395
  let autoAssignEvaluate;
375
396
  const processes = new ProcessManager(config, storage, configDir, terminalHost, { resolveDecisionEvaluate: () => autoAssignEvaluate });
@@ -377,9 +398,9 @@ export async function startServer(config, configPath, options = {}) {
377
398
  // Production startup provides a daemon-backed host for structured CLI runs
378
399
  // even when Render owns every PTY. In-process hosts are for test injection.
379
400
  const legacyStructuredHost = ptyHosts.legacyHost
380
- ?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : null);
401
+ ?? (ptyHosts.host instanceof TerminalDaemonClient ? ptyHosts.host : null);
381
402
  const structuredHosts = await createUpgradeAwareStructuredHost(configPath, legacyStructuredHost, config.structured?.processHost);
382
- const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host, () => decisionRuntime, openRouter, () => autoAssignEvaluate);
403
+ const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host ? daemonAdmission.structured(structuredHosts.host) : undefined, () => decisionRuntime, openRouter, () => autoAssignEvaluate);
383
404
  // core harness 预热:只读本机 Pi 认证与模型目录,不联网。失败不影响启动,
384
405
  // 但要在开始接输入前得到确定结论,否则首轮引擎裁决会因缓存未就绪而退回 CLI。
385
406
  const coreHarnessStatus = await warmCoreHarness(config.harness);
@@ -436,6 +457,16 @@ export async function startServer(config, configPath, options = {}) {
436
457
  structuredSessions.registerRelay(prefix, (sessionId, input) => forwardConversationRelay(conversationService, sessionId, input));
437
458
  }
438
459
  const updateState = new ServerUpdateState();
460
+ const daemonMaintenance = new DaemonMaintenance({
461
+ targets: testMode ? [] : await createDaemonMaintenanceTargets(configPath, ptyHosts, structuredHosts.rustClient, config.render?.binaryPath),
462
+ admission: daemonAdmission,
463
+ busy: () => structuredSessions.getCoreTurnStatus().hasActiveTurns
464
+ || [...processes.list(), ...structuredSessions.list()].some((session) => session.status === "running" || session.structuredState?.inFlight === true || (session.queuedMessages?.length ?? 0) > 0),
465
+ beginCoreDrain: () => structuredSessions.beginCoreRestartDrain(),
466
+ available: () => !shuttingDown && !updateState.updateInFlight && !updateState.providerCliUpdateInFlight
467
+ && terminalDaemonBuildIsCurrent(),
468
+ log: (error) => wandError("底层组件自动更新暂缓,将稍后重试", getErrorMessage(error)),
469
+ });
439
470
  const getUpdateChannel = () => normalizeUpdateChannel(storage.getConfigValue("updateChannel"));
440
471
  let disconnectAuthenticatedSockets = () => { };
441
472
  const refreshProviderCliUpdates = async () => {
@@ -660,6 +691,9 @@ export async function startServer(config, configPath, options = {}) {
660
691
  });
661
692
  registerDecisionRoutes(app, { storage, decisions, requireAuth, requireSessions });
662
693
  app.use("/api", requireAuth);
694
+ app.get("/api/daemon-maintenance", (_req, res) => {
695
+ res.set("Cache-Control", "no-store").json(daemonMaintenance.status());
696
+ });
663
697
  // Connected apps receive only the route families used by native clients and
664
698
  // the browser extension. Browser-admin sessions implicitly satisfy all scopes.
665
699
  app.use([
@@ -728,6 +762,7 @@ export async function startServer(config, configPath, options = {}) {
728
762
  port: config.port,
729
763
  defaultProvider: config.defaultProvider ?? "claude",
730
764
  defaultSessionKind: config.defaultSessionKind ?? "structured",
765
+ defaultEngine: config.defaultEngine ?? "cli",
731
766
  defaultTaskWorktree: config.defaultTaskWorktree !== false,
732
767
  defaultMode: config.defaultMode,
733
768
  defaultCwd: config.defaultCwd,
@@ -833,6 +868,8 @@ export async function startServer(config, configPath, options = {}) {
833
868
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
834
869
  registerMissionRoutes(app, missions);
835
870
  registerUploadRoutes(app, sessionRegistry);
871
+ registerSpeechRoutes(app, { speech, requireSessions, requireAdmin });
872
+ registerLocalModelRoutes(app, { models: localModels, requireSessions, requireAdmin });
836
873
  app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
837
874
  const body = (req.body ?? {});
838
875
  const text = typeof body.text === "string" ? body.text : "";
@@ -1099,6 +1136,17 @@ export async function startServer(config, configPath, options = {}) {
1099
1136
  }, 600);
1100
1137
  restartTimer.unref?.();
1101
1138
  }));
1139
+ function refreshDecisionRuntime() {
1140
+ const address = server.address();
1141
+ const port = typeof address === "object" && address ? address.port : config.port;
1142
+ const status = decisions.status();
1143
+ const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
1144
+ decisionRuntime = config.localDecision?.enabled ? { url: `${protocol}://${localHost}:${port}`,
1145
+ ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}),
1146
+ evaluate: (value, caller, signal) => decisions.evaluate(value, caller, signal) } : null;
1147
+ autoAssignEvaluate = config.localDecision?.enabled && status.supported && status.configured
1148
+ ? (value, caller, signal) => decisions.evaluate(value, caller, signal) : undefined;
1149
+ }
1102
1150
  let bindAddr = config.host === "0.0.0.0" ? "0.0.0.0" : config.host;
1103
1151
  const collectedUrls = [];
1104
1152
  await new Promise((resolve, reject) => {
@@ -1111,6 +1159,8 @@ export async function startServer(config, configPath, options = {}) {
1111
1159
  }
1112
1160
  catch { /* noop */ }
1113
1161
  conversationService.dispose();
1162
+ localModels.dispose();
1163
+ speech.dispose();
1114
1164
  try {
1115
1165
  structuredSessions.dispose();
1116
1166
  }
@@ -1157,19 +1207,7 @@ export async function startServer(config, configPath, options = {}) {
1157
1207
  const address = server.address();
1158
1208
  const actualPort = typeof address === "object" && address ? address.port : config.port;
1159
1209
  bindAddr = `${config.host}:${actualPort}`;
1160
- const decisionStatus = decisions.status();
1161
- if (config.localDecision?.enabled) {
1162
- const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
1163
- decisionRuntime = { url: `${protocol}://${localHost}:${actualPort}`,
1164
- ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}),
1165
- // core 会话直接调进程内决策服务,不再绕出 HTTP + env token。
1166
- evaluate: (value, caller, signal) => decisions.evaluate(value, caller, signal) };
1167
- }
1168
- // 「智能分配」只要真正能跑(启用 + 平台/运行环境就绪)才由本地决策负责;
1169
- // 否则转给系统员工的一次性文本回退,而不是每次白等一个必然失败的调用。
1170
- if (config.localDecision?.enabled && decisionStatus.supported && decisionStatus.configured) {
1171
- autoAssignEvaluate = (value, caller, signal) => decisions.evaluate(value, caller, signal);
1172
- }
1210
+ refreshDecisionRuntime();
1173
1211
  const scheme = useHttps ? "HTTPS" : "HTTP";
1174
1212
  // 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
1175
1213
  collectedUrls.push({ url: `${protocol}://127.0.0.1:${actualPort}`, scheme });
@@ -1244,6 +1282,10 @@ export async function startServer(config, configPath, options = {}) {
1244
1282
  // async route above is wrapped with asyncRoute, and this final middleware
1245
1283
  // keeps parser, synchronous middleware, and async failures JSON-shaped.
1246
1284
  app.use(jsonErrorHandler);
1285
+ // Daemon-only maintenance does not restart Server, depend on npm auto-update
1286
+ // preferences, or stop a live execution. Startup recovery is already attached.
1287
+ if (!testMode)
1288
+ daemonMaintenance.start();
1247
1289
  // ── Auto-update logic ──
1248
1290
  async function performAutoUpdate() {
1249
1291
  if (shuttingDown)
@@ -1384,6 +1426,7 @@ export async function startServer(config, configPath, options = {}) {
1384
1426
  return closePromise;
1385
1427
  closePromise = (async () => {
1386
1428
  shuttingDown = true;
1429
+ await daemonMaintenance.stop();
1387
1430
  decisions.dispose();
1388
1431
  openRouter.dispose();
1389
1432
  await userMemory.dispose();
@@ -1436,6 +1479,8 @@ export async function startServer(config, configPath, options = {}) {
1436
1479
  }
1437
1480
  catch { /* best-effort shutdown */ }
1438
1481
  conversationService.dispose();
1482
+ localModels.dispose();
1483
+ speech.dispose();
1439
1484
  try {
1440
1485
  structuredSessions.dispose();
1441
1486
  }
@@ -16,6 +16,7 @@ export declare function isSessionJustCompleted(session: SessionCompletionState &
16
16
  status?: string;
17
17
  archived?: boolean;
18
18
  ptyBusy?: boolean;
19
+ ptyCommandRunning?: boolean;
19
20
  inFlight?: boolean;
20
21
  structuredState?: {
21
22
  inFlight?: boolean;
@@ -21,6 +21,7 @@ export function isSessionJustCompleted(session) {
21
21
  return (session.completionRevision ?? 0) > (session.viewedCompletionRevision ?? 0)
22
22
  && !session.archived
23
23
  && !session.ptyBusy
24
+ && !session.ptyCommandRunning
24
25
  && !session.inFlight
25
26
  && !session.structuredState?.inFlight
26
27
  && !session.permissionBlocked
@@ -36,6 +36,7 @@ function sessionBase(snapshot) {
36
36
  providerCliActive: snapshot.providerCliActive,
37
37
  providerCliExitCode: snapshot.providerCliExitCode,
38
38
  ptyBusy: snapshot.ptyBusy === true,
39
+ ptyCommandRunning: snapshot.ptyCommandRunning === true,
39
40
  // 与 ptyBusy 同生命周期的本轮锚点(运行时事实,存储里没有对应列)。和 ptyBusy 一起
40
41
  // 读成 null,客户端才不会在一条已经 idle 的会话上算出「已运行 N 分钟」。
41
42
  ptyTurnStartedAt: snapshot.ptyBusy === true ? snapshot.ptyTurnStartedAt ?? null : null,
@@ -1,13 +1,16 @@
1
1
  import { providerCliInstalled } from "./session-provider.js";
2
+ import { isLocalDecisionModel } from "./decision-expert-identity.js";
2
3
  /** Match child-process PATH without starting a shell or trusting a client availability claim. */
3
4
  export function employeeCliAvailable(agent, pathValue = process.env.PATH ?? "") {
4
5
  return providerCliInstalled(agent.provider, pathValue);
5
6
  }
6
7
  /** First installed candidate wins; when none is installed retain the preferred candidate for a recoverable failure. */
7
8
  export function selectEmployeeCandidate(employee, available = employeeCliAvailable, options = {}) {
8
- const candidates = options.skipSdk ? employee.agents.filter((agent) => agent.engine !== "sdk") : employee.agents;
9
+ // LAYA is invoked only by the bounded decision evaluator; never pass its selector to a chat CLI/harness.
10
+ const generative = employee.agents.filter((agent) => !isLocalDecisionModel(agent.model));
11
+ const candidates = options.skipSdk ? generative.filter((agent) => agent.engine !== "sdk") : generative;
9
12
  if (!candidates.length)
10
- throw new Error("硅基员工没有可用的 CLI 执行候选。");
13
+ throw new Error("LAYA 仅支持有界决策,或员工没有可用 CLI 候选;请配置「决策专家」备用调用链。");
11
14
  if (!candidates.length || candidates.some((agent) => agent.kind !== "structured")) {
12
15
  throw new Error("硅基员工没有可用的结构化执行候选。");
13
16
  }
@@ -115,7 +115,7 @@ function parseEmployeeCandidate(raw, allowed) {
115
115
  throw new SiliconEmployeeDraftError("执行引擎无效。", "DRAFT_INVALID_ENGINE", "engine");
116
116
  }
117
117
  if (engine === "sdk" && provider !== "pi") {
118
- throw new SiliconEmployeeDraftError("SDK 执行引擎仅支持 Pi 结构化会话。", "DRAFT_INVALID_ENGINE", "engine");
118
+ throw new SiliconEmployeeDraftError("Wand Agent 仅支持结构化会话。", "DRAFT_INVALID_ENGINE", "engine");
119
119
  }
120
120
  return {
121
121
  provider,
@@ -0,0 +1,24 @@
1
+ /** Multilingual models (not .en); pinned upstream LFS SHA-256 and byte size. */
2
+ export declare const SPEECH_MODEL_REVISION = "5359861c739e955e79d9a303bcbc70fb988958b1";
3
+ export declare const SPEECH_MODELS: readonly [{
4
+ readonly id: "tiny";
5
+ readonly label: "Whisper Tiny";
6
+ readonly description: "中英及多语言 · 低内存 CPU,速度优先";
7
+ readonly size: 77691713;
8
+ readonly sha256: "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21";
9
+ }, {
10
+ readonly id: "base";
11
+ readonly label: "Whisper Base";
12
+ readonly description: "中英及多语言 · 通用 CPU / Mac mini,均衡推荐";
13
+ readonly size: 147951465;
14
+ readonly sha256: "60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe";
15
+ }, {
16
+ readonly id: "small";
17
+ readonly label: "Whisper Small";
18
+ readonly description: "中英及多语言 · 更高准确度,适合 Metal / CUDA 或较强 CPU";
19
+ readonly size: 487601967;
20
+ readonly sha256: "1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b";
21
+ }];
22
+ export type SpeechModel = typeof SPEECH_MODELS[number];
23
+ export declare function speechModel(id: string): SpeechModel | undefined;
24
+ export declare function speechModelUrl(model: SpeechModel): string;
@@ -0,0 +1,16 @@
1
+ /** Multilingual models (not .en); pinned upstream LFS SHA-256 and byte size. */
2
+ export const SPEECH_MODEL_REVISION = "5359861c739e955e79d9a303bcbc70fb988958b1";
3
+ export const SPEECH_MODELS = [
4
+ { id: "tiny", label: "Whisper Tiny", description: "中英及多语言 · 低内存 CPU,速度优先", size: 77_691_713,
5
+ sha256: "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21" },
6
+ { id: "base", label: "Whisper Base", description: "中英及多语言 · 通用 CPU / Mac mini,均衡推荐", size: 147_951_465,
7
+ sha256: "60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe" },
8
+ { id: "small", label: "Whisper Small", description: "中英及多语言 · 更高准确度,适合 Metal / CUDA 或较强 CPU", size: 487_601_967,
9
+ sha256: "1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b" },
10
+ ];
11
+ export function speechModel(id) {
12
+ return SPEECH_MODELS.find((model) => model.id === id);
13
+ }
14
+ export function speechModelUrl(model) {
15
+ return `https://huggingface.co/ggerganov/whisper.cpp/resolve/${SPEECH_MODEL_REVISION}/ggml-${model.id}.bin`;
16
+ }
@@ -0,0 +1,57 @@
1
+ import { type SpeechModel } from "./speech-models.js";
2
+ import { type SpeechBackend, type SpeechResult, type SpeechSettings, type SpeechStatus } from "./speech-types.js";
3
+ import type { WandStorage } from "./storage.js";
4
+ export declare const SPEECH_SETTINGS_KEY = "pref:speechRecognition";
5
+ export declare class SpeechError extends Error {
6
+ readonly code: string;
7
+ readonly status: number;
8
+ constructor(code: string, message: string, status?: number);
9
+ }
10
+ export declare function defaultSpeechSettings(): SpeechSettings;
11
+ export declare function parseSpeechSettings(value: unknown, previous?: SpeechSettings): SpeechSettings;
12
+ /** Strict canonical WAV; all clients encode this exact shape. No decoder/ffmpeg needed on the host. */
13
+ export declare function validateSpeechWav(audio: Buffer): {
14
+ samples: number;
15
+ silent: boolean;
16
+ };
17
+ export interface SpeechServiceOptions {
18
+ /** Trusted deployment configuration only, never HTTP request input. */
19
+ executable?: string;
20
+ backend?: SpeechBackend;
21
+ fetch?: typeof fetch;
22
+ timeoutMs?: number;
23
+ models?: readonly SpeechModel[];
24
+ }
25
+ /** A single bounded job per host avoids CPU/RAM amplification. Audio is temporary, never history. */
26
+ export declare class SpeechService {
27
+ private readonly storage;
28
+ private readonly options;
29
+ private readonly root;
30
+ private readonly models;
31
+ private readonly verified;
32
+ private readonly verifying;
33
+ private download;
34
+ private downloadAbort;
35
+ private active;
36
+ private disposed;
37
+ private maintenance;
38
+ private readonly initialized;
39
+ constructor(storage: Pick<WandStorage, "getPreference" | "setPreference">, configDir: string, options?: SpeechServiceOptions);
40
+ settings(): SpeechSettings;
41
+ configure(patch: unknown): SpeechSettings;
42
+ acquireMaintenance(): void;
43
+ releaseMaintenance(): void;
44
+ /** Explicit admin health check loads the selected model using synthetic silence, without enabling it. */
45
+ initializeModel(id: string, signal: AbortSignal): Promise<void>;
46
+ private modelPath;
47
+ private modelReady;
48
+ private verifyModel;
49
+ private runtime;
50
+ status(): Promise<SpeechStatus>;
51
+ cancelDownload(): void;
52
+ startDownload(id: string): Promise<void>;
53
+ private downloadModel;
54
+ transcribe(audio: Buffer, signal?: AbortSignal): Promise<SpeechResult>;
55
+ private run;
56
+ dispose(): void;
57
+ }