@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.
- package/README.md +4 -0
- package/dist/agent-dispatch.js +5 -3
- package/dist/ai-team-runner.js +1 -0
- package/dist/ai-team-types.d.ts +1 -0
- package/dist/ai-team-types.js +6 -2
- package/dist/build-info.json +4 -4
- package/dist/cli.js +12 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +33 -0
- package/dist/core-runner.js +1 -1
- package/dist/daemon-maintenance-state.d.ts +6 -0
- package/dist/daemon-maintenance-state.js +9 -0
- package/dist/daemon-maintenance-targets.d.ts +20 -0
- package/dist/daemon-maintenance-targets.js +183 -0
- package/dist/daemon-maintenance.d.ts +53 -0
- package/dist/daemon-maintenance.js +164 -0
- package/dist/decision-expert-employee.d.ts +6 -0
- package/dist/decision-expert-employee.js +22 -0
- package/dist/decision-expert-identity.d.ts +7 -0
- package/dist/decision-expert-identity.js +7 -0
- package/dist/decision-hardware.d.ts +19 -0
- package/dist/decision-hardware.js +19 -0
- package/dist/decision-service.d.ts +6 -1
- package/dist/decision-service.js +60 -1
- package/dist/decision-types.d.ts +1 -0
- package/dist/decision-types.js +12 -0
- package/dist/git-quick-commit.js +1 -1
- package/dist/laya-model-manifest.d.ts +11 -0
- package/dist/laya-model-manifest.js +18 -0
- package/dist/local-model-setup.d.ts +46 -0
- package/dist/local-model-setup.js +248 -0
- package/dist/local-model-types.d.ts +34 -0
- package/dist/local-model-types.js +1 -0
- package/dist/message-truncator.d.ts +4 -2
- package/dist/message-truncator.js +20 -6
- package/dist/missions.js +6 -3
- package/dist/model-file-store.d.ts +13 -0
- package/dist/model-file-store.js +108 -0
- package/dist/model-groups.js +2 -2
- package/dist/model-setup-process.d.ts +2 -0
- package/dist/model-setup-process.js +80 -0
- package/dist/models.js +3 -3
- package/dist/process-manager.js +21 -8
- package/dist/provider-catalog.d.ts +13 -1
- package/dist/provider-catalog.js +14 -2
- package/dist/render-binary.d.ts +1 -1
- package/dist/render-binary.js +4 -4
- package/dist/render-daemon-client.d.ts +12 -1
- package/dist/render-daemon-client.js +72 -4
- package/dist/render-host.d.ts +1 -0
- package/dist/render-host.js +9 -56
- package/dist/render-structured-host.d.ts +1 -0
- package/dist/render-structured-host.js +2 -2
- package/dist/server-employee-routes.js +3 -3
- package/dist/server-local-model-routes.d.ts +8 -0
- package/dist/server-local-model-routes.js +62 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.js +34 -8
- package/dist/server-settings-routes.js +2 -0
- package/dist/server-speech-routes.d.ts +8 -0
- package/dist/server-speech-routes.js +87 -0
- package/dist/server-task-routes.js +3 -1
- package/dist/server-workspace-routes.js +2 -0
- package/dist/server.js +63 -18
- package/dist/session-completion-state.d.ts +1 -0
- package/dist/session-completion-state.js +1 -0
- package/dist/session-transport.js +1 -0
- package/dist/silicon-employee-dispatch.js +5 -2
- package/dist/silicon-employee-draft.js +1 -1
- package/dist/speech-models.d.ts +24 -0
- package/dist/speech-models.js +16 -0
- package/dist/speech-service.d.ts +57 -0
- package/dist/speech-service.js +399 -0
- package/dist/speech-types.d.ts +47 -0
- package/dist/speech-types.js +3 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +13 -0
- package/dist/structured-failure.js +1 -1
- package/dist/structured-session-manager.d.ts +15 -2
- package/dist/structured-session-manager.js +58 -19
- package/dist/terminal-daemon-build.d.ts +4 -0
- package/dist/terminal-daemon-build.js +54 -0
- package/dist/terminal-daemon-protocol.d.ts +1 -1
- package/dist/terminal-daemon-server.js +20 -1
- package/dist/terminal-host.d.ts +4 -0
- package/dist/terminal-host.js +7 -4
- package/dist/tui/commands.js +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/web-ui/chat-history-window.d.ts +23 -0
- package/dist/web-ui/chat-history-window.js +29 -0
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/scripts.js +316 -233
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +28 -1
- package/dist/web-ui/provider-identity.js +31 -2
- package/dist/web-ui/session-activity.d.ts +1 -0
- package/dist/web-ui/session-activity.js +5 -2
- package/dist/web-ui/speech-audio.d.ts +2 -0
- package/dist/web-ui/speech-audio.js +41 -0
- package/dist/web-ui/vendor-loader.d.ts +2 -0
- package/dist/web-ui/vendor-loader.js +50 -0
- package/dist/ws-broadcast.js +10 -4
- package/package.json +7 -3
- package/scripts/install-laya-runtime.js +67 -0
- 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: "
|
|
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 ? "
|
|
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("
|
|
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 ?? ">=
|
|
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
|
|
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
|
-
?? (
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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("
|
|
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("
|
|
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
|
+
}
|