@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
@@ -0,0 +1,248 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { DecisionError } from "./decision-types.js";
6
+ import { LAYA_FILES, LAYA_REPOSITORY, layaFileUrl } from "./laya-model-manifest.js";
7
+ import { ModelFileStore } from "./model-file-store.js";
8
+ import { runModelSetup } from "./model-setup-process.js";
9
+ import { speechModel } from "./speech-models.js";
10
+ import { systemEnvValue } from "./env-utils.js";
11
+ /** Deployment jobs only; existing inference owners retain their protocols, rate limits and capabilities. */
12
+ export class LocalModelSetupService {
13
+ deps;
14
+ files;
15
+ root;
16
+ manifest;
17
+ operations = new Map();
18
+ jobs = new Map();
19
+ sequence = 0;
20
+ disposed = false;
21
+ constructor(deps) {
22
+ this.deps = deps;
23
+ this.files = new ModelFileStore(deps.fetch);
24
+ this.root = path.join(deps.configDir, "local-models", "laya");
25
+ this.manifest = deps.layaFiles ?? LAYA_FILES;
26
+ }
27
+ supported() { return (this.deps.supportedLaya ?? (() => process.platform === "darwin" && process.arch === "arm64"))(); }
28
+ managedModel() { return path.join(this.root, "model"); }
29
+ async layaModel() {
30
+ const current = this.deps.decisionConfig().modelPath;
31
+ if (current && path.isAbsolute(current) && await this.files.ready(current, this.manifest))
32
+ return { modelPath: current, downloaded: true };
33
+ const managed = this.managedModel();
34
+ return { modelPath: managed, downloaded: await this.files.ready(managed, this.manifest) };
35
+ }
36
+ async python() {
37
+ const current = this.deps.decisionConfig().pythonPath;
38
+ if (current && path.isAbsolute(current) && existsSync(current))
39
+ return current;
40
+ try {
41
+ const receipt = JSON.parse(await readFile(path.join(this.root, "runtime.json"), "utf8"));
42
+ if (receipt.layaVersion === "0.3.0" && receipt.mlxVersion === "0.32.2" && typeof receipt.pythonPath === "string"
43
+ && receipt.pythonPath.startsWith(path.join(this.root, "environments") + path.sep) && existsSync(receipt.pythonPath))
44
+ return receipt.pythonPath;
45
+ }
46
+ catch { }
47
+ return null;
48
+ }
49
+ async status() {
50
+ const [model, python, speech] = await Promise.all([this.layaModel(), this.python(), this.deps.speech.status()]);
51
+ const decision = this.deps.decisions.status();
52
+ const supported = this.supported();
53
+ const operation = this.operations.get("laya") ?? null;
54
+ const selected = speech.models.find((value) => value.id === speech.settings.model);
55
+ return {
56
+ laya: { kind: "laya", label: "LAYA 本地决策", supported,
57
+ reason: !supported ? "LAYA-MLX 当前仅支持 Apple Silicon macOS / Metal;不是聊天或授权模型。"
58
+ : !model.downloaded ? "模型尚未下载或完整性校验失败。" : !python ? "独立 Python/MLX 运行时尚未初始化。" : null,
59
+ enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
60
+ downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
61
+ operation: operation ? { ...operation } : null },
62
+ speech: { kind: "speech", label: "服务端语音识别", supported: ["darwin", "linux", "win32"].includes(process.platform), reason: speech.reason,
63
+ enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
64
+ downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
65
+ busy: speech.busy || this.jobs.has("speech"), operation: this.operations.has("speech") ? { ...this.operations.get("speech") } : null },
66
+ };
67
+ }
68
+ parse(kind, raw) {
69
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
70
+ throw new DecisionError("INVALID_REQUEST", "模型操作参数无效。");
71
+ const body = raw;
72
+ if (Object.keys(body).some((key) => !["model", "backend"].includes(key))
73
+ || (body.model !== undefined && typeof body.model !== "string")
74
+ || (body.backend !== undefined && !["auto", "cpu", "metal", "cuda"].includes(body.backend))) {
75
+ throw new DecisionError("INVALID_REQUEST", "仅允许选择固定模型与受支持的后端;不接受命令、路径或URL。");
76
+ }
77
+ if (kind === "laya" && ((body.model && body.model !== LAYA_REPOSITORY) || body.backend !== undefined))
78
+ throw new DecisionError("INVALID_MODEL", "LAYA 使用固定多语言 MLX 模型。");
79
+ if (kind === "speech" && body.model !== undefined && !speechModel(body.model))
80
+ throw new DecisionError("INVALID_MODEL", "未知语音模型。");
81
+ if (body.backend === "metal" && process.platform !== "darwin")
82
+ throw new DecisionError("UNSUPPORTED", "Metal 仅适用于 macOS。");
83
+ return body;
84
+ }
85
+ start(kind, action, raw) {
86
+ if (this.disposed)
87
+ throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
88
+ if (kind === "laya" && !this.supported())
89
+ throw new DecisionError("UNSUPPORTED", "LAYA-MLX 需要 Apple Silicon macOS 与 Metal。", 409);
90
+ const input = this.parse(kind, raw);
91
+ if (this.jobs.has(kind))
92
+ throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
93
+ if (kind === "laya")
94
+ this.deps.decisions.assertConfigurable();
95
+ if (kind === "speech" && action === "initialize")
96
+ this.deps.speech.acquireMaintenance();
97
+ const abort = new AbortController(), id = ++this.sequence;
98
+ this.operations.set(kind, { id, action, phase: "verifying", message: "检查已安装资源,不覆盖既有环境", received: 0, total: null });
99
+ const promise = this.work(kind, action, input, abort, id).catch(() => { }).finally(() => {
100
+ if (kind === "speech" && action === "initialize")
101
+ this.deps.speech.releaseMaintenance();
102
+ if (this.jobs.get(kind)?.abort === abort)
103
+ this.jobs.delete(kind);
104
+ });
105
+ this.jobs.set(kind, { abort, promise });
106
+ // Failure is represented by operation status. Always observe the background promise.
107
+ void promise;
108
+ }
109
+ update(kind, id, patch) {
110
+ const current = this.operations.get(kind);
111
+ if (current?.id === id)
112
+ this.operations.set(kind, { ...current, ...patch });
113
+ }
114
+ async work(kind, action, input, abort, id) {
115
+ const timer = setTimeout(() => abort.abort(), this.deps.timeoutMs ?? 30 * 60_000);
116
+ timer.unref();
117
+ const signal = abort.signal;
118
+ const phase = (value, message) => this.update(kind, id, { phase: value, message });
119
+ try {
120
+ if (kind === "laya") {
121
+ let model = await this.layaModel();
122
+ signal.throwIfAborted();
123
+ if (action === "download") {
124
+ phase("downloading", "下载固定多语言 LAYA 模型与 tokenizer(不下载聊天模型)");
125
+ await this.files.download(model.modelPath, this.manifest, layaFileUrl, signal, (received, total) => this.update(kind, id, { received, total }));
126
+ }
127
+ else {
128
+ if (!model.downloaded)
129
+ throw new Error("请先下载并校验 LAYA 模型,再初始化运行环境。");
130
+ let python = await this.python();
131
+ signal.throwIfAborted();
132
+ if (python) {
133
+ phase("verifying", "检查已配置 Python 的 LAYA / MLX 版本与 Metal 能力,不修改该环境");
134
+ const compatible = await this.checkRuntime(python, signal);
135
+ signal.throwIfAborted();
136
+ if (!compatible)
137
+ python = null;
138
+ }
139
+ if (!python) {
140
+ phase("runtime", "安装独立 LAYA / MLX 环境,需服务器具备 Python 3.11+;不修改系统 Python");
141
+ await mkdir(path.join(this.root, "environments"), { recursive: true, mode: 0o700 });
142
+ const environment = path.join(this.root, "environments", `setup-${id}-${Date.now()}`);
143
+ try {
144
+ await (this.deps.run ?? runModelSetup)(process.execPath, [this.installer("install-laya-runtime.js"), "--dir", this.root, "--environment", environment, "--events"], this.deps.configDir, signal, (name, message) => phase(name === "verifying" ? "verifying" : "runtime", message));
145
+ }
146
+ catch (error) {
147
+ await rm(environment, { recursive: true, force: true }).catch(() => { });
148
+ throw error;
149
+ }
150
+ const receipt = JSON.parse(await readFile(path.join(this.root, "runtime.json"), "utf8"));
151
+ python = typeof receipt.pythonPath === "string" && receipt.pythonPath.startsWith(path.join(this.root, "environments") + path.sep) && existsSync(receipt.pythonPath)
152
+ ? receipt.pythonPath : null;
153
+ if (!python)
154
+ throw new Error("独立运行时校验未通过。请检查 Python 3.11+、PyPI 网络、Metal 和磁盘空间。");
155
+ }
156
+ signal.throwIfAborted();
157
+ model = await this.layaModel();
158
+ if (!model.downloaded)
159
+ throw new Error("LAYA 模型校验失败,请重新下载。");
160
+ const current = this.deps.decisionConfig();
161
+ if (current.pythonPath !== python || current.modelPath !== model.modelPath) {
162
+ this.deps.configureDecision({ enabled: current.enabled, pythonPath: python, modelPath: model.modelPath });
163
+ }
164
+ phase("initializing", "加载并检查 LAYA 模型,未自动启用、派工或授予任何工具权限");
165
+ await this.deps.decisions.initialize(signal);
166
+ }
167
+ }
168
+ else {
169
+ const selected = input.model ?? this.deps.speech.settings().model;
170
+ if (action === "download") {
171
+ phase("downloading", "下载并校验语音模型");
172
+ await this.deps.speech.startDownload(selected);
173
+ while (true) {
174
+ signal.throwIfAborted();
175
+ const status = await this.deps.speech.status();
176
+ if (status.download?.phase === "failed")
177
+ throw new Error(status.download.error || "语音模型下载失败。");
178
+ if (!status.download || status.download.phase !== "downloading")
179
+ break;
180
+ this.update(kind, id, { received: status.download.received, total: status.download.total });
181
+ await new Promise((resolve, reject) => {
182
+ const cancelled = () => { clearTimeout(delay); reject(new Error("Cancelled")); };
183
+ const delay = setTimeout(() => { signal.removeEventListener("abort", cancelled); resolve(); }, 250);
184
+ signal.addEventListener("abort", cancelled, { once: true });
185
+ });
186
+ }
187
+ }
188
+ else {
189
+ const status = await this.deps.speech.status();
190
+ signal.throwIfAborted();
191
+ const backend = input.backend === "auto" || !input.backend ? process.platform === "darwin" ? "metal" : "cpu" : input.backend;
192
+ if (!status.models.find((value) => value.id === selected)?.downloaded)
193
+ throw new Error("请先下载所选语音模型,再初始化。");
194
+ if (!status.runtime.available || (input.backend && input.backend !== "auto" && status.runtime.backend !== backend)) {
195
+ if (systemEnvValue("WAND_WHISPER_BIN"))
196
+ throw new Error("服务器使用自定义语音运行时,请由管理员核对环境配置;不会覆盖它。");
197
+ phase("runtime", "安装固定 whisper.cpp 运行时(需 Git、CMake、C++工具链;CUDA需已安装Toolkit)");
198
+ await mkdir(path.join(this.deps.configDir, "speech"), { recursive: true, mode: 0o700 });
199
+ const work = await mkdtemp(path.join(this.deps.configDir, "speech", ".setup-work-"));
200
+ try {
201
+ await (this.deps.run ?? runModelSetup)(process.execPath, [this.installer("install-speech-runtime.js"), "--dir", path.join(this.deps.configDir, "speech"), "--backend", backend, "--work-dir", work, "--events"], this.deps.configDir, signal, (name, message) => phase(name === "verifying" ? "verifying" : "runtime", message));
202
+ }
203
+ finally {
204
+ await rm(work, { recursive: true, force: true }).catch(() => { });
205
+ }
206
+ }
207
+ phase("initializing", "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
208
+ await this.deps.speech.initializeModel(selected, signal);
209
+ }
210
+ }
211
+ signal.throwIfAborted();
212
+ phase("completed", action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
213
+ }
214
+ catch (error) {
215
+ const cancelled = signal.aborted;
216
+ const message = cancelled ? "安装任务已取消,完整文件保留,可重试" : error instanceof DecisionError || (error instanceof Error && /请先|校验|自定义|独立/.test(error.message))
217
+ ? error.message : "安装或初始化失败,请检查网络、编译工具/Python版本、磁盘空间和平台能力,再重试。";
218
+ this.update(kind, id, { phase: cancelled ? "cancelled" : "failed", message, ...(cancelled ? {} : { error: message }) });
219
+ if (kind === "speech" && action === "download" && cancelled)
220
+ this.deps.speech.cancelDownload();
221
+ }
222
+ finally {
223
+ clearTimeout(timer);
224
+ }
225
+ }
226
+ async checkRuntime(python, signal) {
227
+ if (this.deps.checkRuntime)
228
+ return this.deps.checkRuntime(python, signal);
229
+ try {
230
+ await runModelSetup(python, ["-c", "import importlib.metadata as m; import mlx.core as mx; import laya_mlx; assert m.version('laya-mlx') == '0.3.0'; assert m.version('mlx') == '0.32.2'; assert mx.metal.is_available()"], this.deps.configDir, signal, () => { }, 30_000);
231
+ return true;
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ installer(name) { return fileURLToPath(new URL(`../scripts/${name}`, import.meta.url)); }
238
+ cancel(kind) { this.jobs.get(kind)?.abort.abort(); }
239
+ setLayaEnabled(enabled) {
240
+ if (this.jobs.has("laya"))
241
+ throw new DecisionError("BUSY", "LAYA 安装任务进行中,请稍后修改。", 409);
242
+ if (enabled && !this.supported())
243
+ throw new DecisionError("UNSUPPORTED", "当前平台不支持 LAYA-MLX。", 409);
244
+ this.deps.configureDecision({ ...this.deps.decisionConfig(), enabled });
245
+ }
246
+ dispose() { this.disposed = true; for (const job of this.jobs.values())
247
+ job.abort.abort(); }
248
+ }
@@ -0,0 +1,34 @@
1
+ import type { SpeechBackend } from "./speech-types.js";
2
+ export type LocalModelKind = "laya" | "speech";
3
+ export type ModelSetupPhase = "downloading" | "verifying" | "runtime" | "initializing" | "completed" | "failed" | "cancelled";
4
+ export interface ModelSetupOperation {
5
+ id: number;
6
+ action: "download" | "initialize";
7
+ phase: ModelSetupPhase;
8
+ message: string;
9
+ received: number;
10
+ total: number | null;
11
+ error?: string;
12
+ }
13
+ export interface LocalModelStatus {
14
+ kind: LocalModelKind;
15
+ label: string;
16
+ supported: boolean;
17
+ reason: string | null;
18
+ enabled: boolean;
19
+ model: string;
20
+ modelSize: number;
21
+ downloaded: boolean;
22
+ runtimeAvailable: boolean;
23
+ initialized: boolean;
24
+ busy: boolean;
25
+ operation: ModelSetupOperation | null;
26
+ }
27
+ export interface LocalModelsStatus {
28
+ laya: LocalModelStatus;
29
+ speech: LocalModelStatus;
30
+ }
31
+ export interface ModelSetupInput {
32
+ model?: string;
33
+ backend?: "auto" | SpeechBackend;
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -25,6 +25,8 @@ export interface BlockWindowedMessages extends WindowedMessages {
25
25
  * 超出的会话才按下面的预算从尾部切:客户端先翻这条 turn 的头部块,再按 turn 往前翻。
26
26
  */
27
27
  export declare const MESSAGE_FIRST_PAINT_BYTES: number;
28
+ /** Explicit small-page clients may narrow, never enlarge, the transport budget. */
29
+ export declare function messageWindowByteBudget(value: unknown): number;
28
30
  /** 每个块在客户端是否「默认收起」(思考块 + 默认收起的工具卡片,与传输截断同一口径)。 */
29
31
  export declare function collapsedBlockFlags(content: ContentBlock[], cardDefaults: CardExpandDefaults): boolean[];
30
32
  /**
@@ -44,7 +46,7 @@ export declare function contentTransportBytes(content: ContentBlock[], cardDefau
44
46
  *
45
47
  * 返回吸附后的起点;起点越界时回退到 0(整条 turn 入窗,保证窗口不为空)。
46
48
  */
47
- export declare function alignedBlockStart(content: ContentBlock[], cardDefaults: CardExpandDefaults, rawStart: number, forwardLimit?: number): number;
49
+ export declare function alignedBlockStart(content: ContentBlock[], cardDefaults: CardExpandDefaults, rawStart: number, forwardLimit?: number, maxSnapBackBytes?: number): number;
48
50
  /** [0, end) 里用户可感知的块数(默认收起的工具 / 思考块不计入)。 */
49
51
  export declare function visibleBlockCount(content: ContentBlock[], cardDefaults: CardExpandDefaults, end: number): number;
50
52
  /**
@@ -56,7 +58,7 @@ export declare function visibleBlockCount(content: ContentBlock[], cardDefaults:
56
58
  * 另外叠加一层载荷上限 —— 否则一段长工具调用会把预算吃光,把用户自己的提示词挤出首屏,
57
59
  * 一条提示词的会话也会莫名出现「更早消息」。
58
60
  */
59
- export declare function blockWindowMessagesForTransport(all: ConversationTurn[] | undefined, cardDefaults: CardExpandDefaults, blockBudget?: number, byteBudget?: number): BlockWindowedMessages;
61
+ export declare function blockWindowMessagesForTransport(all: ConversationTurn[] | undefined, cardDefaults: CardExpandDefaults, blockBudget?: number, byteBudget?: number, strictByteBudget?: boolean): BlockWindowedMessages;
60
62
  /**
61
63
  * 块级翻页:取某条 turn 的 content[start, end) 这一段(已做 transport 截断)。
62
64
  * 客户端滚动到顶、且当前最旧 turn 仍有更早块时调用,end = 客户端当前 leadingBlockOffset。
@@ -26,6 +26,13 @@ const MESSAGE_BLOCK_WINDOW = 60;
26
26
  * 超出的会话才按下面的预算从尾部切:客户端先翻这条 turn 的头部块,再按 turn 往前翻。
27
27
  */
28
28
  export const MESSAGE_FIRST_PAINT_BYTES = 1024 * 1024;
29
+ /** Explicit small-page clients may narrow, never enlarge, the transport budget. */
30
+ export function messageWindowByteBudget(value) {
31
+ const parsed = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
32
+ return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed > 0
33
+ ? Math.min(MESSAGE_FIRST_PAINT_BYTES, Math.max(16 * 1024, parsed))
34
+ : MESSAGE_FIRST_PAINT_BYTES;
35
+ }
29
36
  /**
30
37
  * 吸附回退的额外载荷上限:折叠段往回吃掉的体积超过它就改为跳过整段,
31
38
  * 而不是把整段拉进首屏(窗口的目标是压住首屏载荷,吸附不能反过来把它撑大)。
@@ -171,7 +178,7 @@ function collapsedRunEnd(collapsed, index) {
171
178
  *
172
179
  * 返回吸附后的起点;起点越界时回退到 0(整条 turn 入窗,保证窗口不为空)。
173
180
  */
174
- export function alignedBlockStart(content, cardDefaults, rawStart, forwardLimit = content.length) {
181
+ export function alignedBlockStart(content, cardDefaults, rawStart, forwardLimit = content.length, maxSnapBackBytes = MAX_BLOCK_SNAP_BACK_BYTES) {
175
182
  const total = content.length;
176
183
  const start = Math.min(Math.max(rawStart, 0), total);
177
184
  if (start <= 0 || total === 0)
@@ -196,12 +203,19 @@ export function alignedBlockStart(content, cardDefaults, rawStart, forwardLimit
196
203
  cursor = next;
197
204
  }
198
205
  const snapBackBytes = contentTransportBytes(content.slice(cursor, start), cardDefaults);
199
- if (snapBackBytes <= MAX_BLOCK_SNAP_BACK_BYTES)
206
+ if (snapBackBytes <= maxSnapBackBytes)
200
207
  return cursor;
201
208
  // 折叠段太长:不再往回吃,跳到该段之后,保证首屏载荷不因吸附膨胀。
202
209
  const skipped = collapsed[cursor] ? collapsedRunEnd(collapsed, cursor) : cursor;
203
- if (skipped >= total)
204
- return 0;
210
+ if (skipped >= total) {
211
+ if (maxSnapBackBytes >= MAX_BLOCK_SNAP_BACK_BYTES)
212
+ return 0;
213
+ // Opted-in small pages must not absorb an entire huge closed timeline.
214
+ // Keep a boundary tool_result paired with its call, even if that single
215
+ // semantic unit is larger than the soft byte budget.
216
+ const head = content[start];
217
+ return head?.type === "tool_result" ? useIndexById.get(head.tool_use_id) ?? start : start;
218
+ }
205
219
  // 跳不到更晚的位置(只可能还是往回吃)或超过本页末尾时保持原切点。
206
220
  if (skipped <= start || skipped >= forwardLimit)
207
221
  return start;
@@ -227,7 +241,7 @@ export function visibleBlockCount(content, cardDefaults, end) {
227
241
  * 另外叠加一层载荷上限 —— 否则一段长工具调用会把预算吃光,把用户自己的提示词挤出首屏,
228
242
  * 一条提示词的会话也会莫名出现「更早消息」。
229
243
  */
230
- export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget = MESSAGE_BLOCK_WINDOW, byteBudget = MESSAGE_FIRST_PAINT_BYTES) {
244
+ export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget = MESSAGE_BLOCK_WINDOW, byteBudget = MESSAGE_FIRST_PAINT_BYTES, strictByteBudget = false) {
231
245
  const turns = all ?? [];
232
246
  const total = turns.length;
233
247
  if (total === 0) {
@@ -280,7 +294,7 @@ export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget =
280
294
  // 最旧入窗 turn 的切点吸附到干净边界:不切开折叠的工具段,也不留下无头 tool_result。
281
295
  const headContent = turns[startTurn].content;
282
296
  const startOffset = leadingBlockOffset > 0
283
- ? alignedBlockStart(headContent, cardDefaults, leadingBlockOffset)
297
+ ? alignedBlockStart(headContent, cardDefaults, leadingBlockOffset, headContent.length, strictByteBudget ? Math.min(MAX_BLOCK_SNAP_BACK_BYTES, byteBudget) : MAX_BLOCK_SNAP_BACK_BYTES)
284
298
  : 0;
285
299
  const windowedTurns = [];
286
300
  for (let i = startTurn; i < total; i++) {
package/dist/missions.js CHANGED
@@ -187,12 +187,15 @@ export class Missions {
187
187
  ingest(event) {
188
188
  if (!event.sessionId || event.sessionId === "__system__")
189
189
  return;
190
- const snapshot = this.sessions.getLatest(event.sessionId);
191
- if (!snapshot)
192
- return;
190
+ // Most PTY output belongs to no mission. Check the indexed ownership first
191
+ // instead of projecting a session (and reading its workspace/completion) for
192
+ // every unrelated output batch.
193
193
  const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
194
194
  if (!attempt)
195
195
  return;
196
+ const snapshot = this.sessions.getLatest(event.sessionId);
197
+ if (!snapshot)
198
+ return;
196
199
  const state = activityState(snapshot, event);
197
200
  const updatedAt = nowIso();
198
201
  this.storage.saveMissionAttempt({
@@ -0,0 +1,13 @@
1
+ import type { ModelFile } from "./laya-model-manifest.js";
2
+ /** Fixed manifests only. No URL/path from a client is passed to this utility. */
3
+ export declare class ModelFileStore {
4
+ private readonly fetcher;
5
+ private readonly verified;
6
+ private readonly pending;
7
+ constructor(fetcher?: typeof fetch);
8
+ ready(root: string, files: readonly ModelFile[]): Promise<boolean>;
9
+ private filePath;
10
+ private fileReady;
11
+ private verify;
12
+ download(root: string, files: readonly ModelFile[], url: (file: ModelFile) => string, signal: AbortSignal, progress: (received: number, total: number) => void): Promise<void>;
13
+ }
@@ -0,0 +1,108 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { mkdir, open, rename, rm, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ /** Fixed manifests only. No URL/path from a client is passed to this utility. */
6
+ export class ModelFileStore {
7
+ fetcher;
8
+ verified = new Map();
9
+ pending = new Map();
10
+ constructor(fetcher = fetch) {
11
+ this.fetcher = fetcher;
12
+ }
13
+ async ready(root, files) {
14
+ return (await Promise.all(files.map((file) => this.fileReady(root, file)))).every(Boolean);
15
+ }
16
+ filePath(root, file) {
17
+ if (!file.path || path.isAbsolute(file.path) || file.path.split(/[\\/]/).some((part) => !part || part === "." || part === ".."))
18
+ throw new Error("Invalid fixed model manifest");
19
+ return path.join(root, file.path);
20
+ }
21
+ async fileReady(root, file) {
22
+ const target = this.filePath(root, file);
23
+ const key = `${target}:${file.sha256}`;
24
+ const existing = this.pending.get(key);
25
+ if (existing)
26
+ return existing;
27
+ const result = this.verify(target, file, key);
28
+ this.pending.set(key, result);
29
+ try {
30
+ return await result;
31
+ }
32
+ finally {
33
+ this.pending.delete(key);
34
+ }
35
+ }
36
+ async verify(target, file, key) {
37
+ try {
38
+ const info = await stat(target);
39
+ if (!info.isFile() || info.size !== file.size)
40
+ return false;
41
+ const signature = `${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
42
+ if (this.verified.get(key) === signature)
43
+ return true;
44
+ const hash = createHash("sha256");
45
+ for await (const chunk of createReadStream(target))
46
+ hash.update(chunk);
47
+ if (hash.digest("hex") !== file.sha256)
48
+ return false;
49
+ this.verified.set(key, signature);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ async download(root, files, url, signal, progress) {
57
+ const total = files.reduce((sum, file) => sum + file.size, 0);
58
+ let completed = 0;
59
+ for (const file of files) {
60
+ signal.throwIfAborted();
61
+ if (await this.fileReady(root, file)) {
62
+ completed += file.size;
63
+ progress(completed, total);
64
+ continue;
65
+ }
66
+ signal.throwIfAborted();
67
+ const target = this.filePath(root, file);
68
+ const partial = `${target}.${randomBytes(6).toString("hex")}.part`;
69
+ try {
70
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
71
+ const response = await this.fetcher(url(file), { signal });
72
+ if (!response.ok || !response.body)
73
+ throw new Error("Model download failed");
74
+ const length = response.headers.get("content-length");
75
+ if (length && Number(length) !== file.size) {
76
+ await response.body.cancel();
77
+ throw new Error("Model size mismatch");
78
+ }
79
+ const output = await open(partial, "wx", 0o600);
80
+ let received = 0;
81
+ const hash = createHash("sha256");
82
+ try {
83
+ for await (const chunk of response.body) {
84
+ signal.throwIfAborted();
85
+ received += chunk.length;
86
+ if (received > file.size)
87
+ throw new Error("Model too large");
88
+ hash.update(chunk);
89
+ await output.writeFile(chunk);
90
+ progress(completed + received, total);
91
+ }
92
+ }
93
+ finally {
94
+ await output.close();
95
+ }
96
+ signal.throwIfAborted();
97
+ if (received !== file.size || hash.digest("hex") !== file.sha256)
98
+ throw new Error("Model hash mismatch");
99
+ await rename(partial, target);
100
+ completed += file.size;
101
+ progress(completed, total);
102
+ }
103
+ finally {
104
+ await rm(partial, { force: true, maxRetries: 3, retryDelay: 100 }).catch(() => { });
105
+ }
106
+ }
107
+ }
108
+ }
@@ -73,7 +73,7 @@ export function normalizeModelGroups(value) {
73
73
  throw new Error("组内必须是具体模型 ID,不能嵌套分组或使用默认值。");
74
74
  }
75
75
  if (isOpenRouterFreeSelector(trimmed) && provider !== "pi")
76
- throw new Error("免费模型仅属于 one 的 Agent。");
76
+ throw new Error("免费模型仅属于 Pi。");
77
77
  if (free && !trimmed.startsWith(`${OPENROUTER_FREE_PROVIDER}/`))
78
78
  throw new Error("免费分组只能包含免费池中的模型。");
79
79
  return trimmed;
@@ -113,7 +113,7 @@ export function resolveModelGroupModels(groups, provider, selector, options = {}
113
113
  const value = modelSelectorWithoutAutoAssign(selector);
114
114
  if (value === OPENROUTER_FREE_SELECTOR) {
115
115
  if (provider !== "pi")
116
- throw new Error("免费分组仅属于 one 的 Agent。");
116
+ throw new Error("免费分组仅属于 Pi。");
117
117
  return [value];
118
118
  }
119
119
  const group = findModelGroup(groups, provider, value, options);
@@ -0,0 +1,2 @@
1
+ /** Runs one fixed deployment command. Only whitelisted server code constructs executable/args. */
2
+ export declare function runModelSetup(executable: string, args: string[], cwd: string, signal: AbortSignal, progress: (phase: string, message: string) => void, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { spawn } from "node:child_process";
2
+ import { buildChildEnv, systemEnvValue } from "./env-utils.js";
3
+ /** Runs one fixed deployment command. Only whitelisted server code constructs executable/args. */
4
+ export function runModelSetup(executable, args, cwd, signal, progress, timeoutMs = 20 * 60_000) {
5
+ return new Promise((resolve, reject) => {
6
+ if (signal.aborted) {
7
+ reject(new Error("Model setup cancelled"));
8
+ return;
9
+ }
10
+ const grouped = process.platform !== "win32";
11
+ const child = spawn(executable, args, { cwd, detached: grouped, windowsHide: true, stdio: ["ignore", "pipe", "pipe"],
12
+ env: buildChildEnv(false, {
13
+ HTTPS_PROXY: systemEnvValue("HTTPS_PROXY"), HTTP_PROXY: systemEnvValue("HTTP_PROXY"), NO_PROXY: systemEnvValue("NO_PROXY"),
14
+ WAND_LAYA_PYTHON_BIN: systemEnvValue("WAND_LAYA_PYTHON_BIN"),
15
+ }) });
16
+ let failed = false, buffer = "", kill;
17
+ const terminate = () => {
18
+ failed = true;
19
+ const stop = (hard) => {
20
+ if (grouped && child.pid) {
21
+ try {
22
+ process.kill(-child.pid, hard ? "SIGKILL" : "SIGTERM");
23
+ }
24
+ catch { }
25
+ }
26
+ else if (child.pid) {
27
+ const cleanup = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { env: buildChildEnv(false), windowsHide: true, stdio: "ignore" });
28
+ cleanup.on("error", () => { try {
29
+ child.kill();
30
+ }
31
+ catch { } });
32
+ }
33
+ };
34
+ stop(false);
35
+ kill ??= setTimeout(() => stop(true), 1_000);
36
+ kill.unref();
37
+ };
38
+ const timer = setTimeout(terminate, timeoutMs);
39
+ timer.unref();
40
+ signal.addEventListener("abort", terminate, { once: true });
41
+ // Compiler/Python diagnostics may contain proxy credentials or local paths: discard, never echo.
42
+ child.stderr.resume();
43
+ child.stdout.setEncoding("utf8");
44
+ child.stdout.on("data", (chunk) => {
45
+ buffer += chunk;
46
+ if (buffer.length > 16_384)
47
+ buffer = buffer.slice(-4096);
48
+ let end;
49
+ while ((end = buffer.indexOf("\n")) !== -1) {
50
+ const line = buffer.slice(0, end);
51
+ buffer = buffer.slice(end + 1);
52
+ if (!line.startsWith("[wand-model] "))
53
+ continue;
54
+ try {
55
+ const value = JSON.parse(line.slice(13));
56
+ if (["runtime", "verifying", "completed"].includes(value.phase) && typeof value.message === "string" && value.message.length < 240)
57
+ progress(value.phase, value.message);
58
+ }
59
+ catch { }
60
+ }
61
+ });
62
+ child.once("error", () => { failed = true; });
63
+ child.once("close", (code) => {
64
+ if (failed && grouped && child.pid) {
65
+ try {
66
+ process.kill(-child.pid, "SIGKILL");
67
+ }
68
+ catch { }
69
+ }
70
+ clearTimeout(timer);
71
+ if (kill)
72
+ clearTimeout(kill);
73
+ signal.removeEventListener("abort", terminate);
74
+ if (failed || code !== 0)
75
+ reject(new Error("Model runtime setup failed"));
76
+ else
77
+ resolve();
78
+ });
79
+ });
80
+ }
package/dist/models.js CHANGED
@@ -61,7 +61,7 @@ const QODER_FALLBACK_MODELS = [
61
61
  { id: "ultimate", label: "Ultimate" },
62
62
  ];
63
63
  const PI_FALLBACK_MODELS = [
64
- { id: "default", label: "跟随 one 的 Agent 默认", alias: true },
64
+ { id: "default", label: "跟随 Pi 默认", alias: true },
65
65
  ];
66
66
  /**
67
67
  * Gemini CLI 没有列模型的子命令,只能给出稳定别名(`packages/core/src/config/models.ts`
@@ -799,7 +799,7 @@ export function parsePiRpcModels(stdout) {
799
799
  return [
800
800
  {
801
801
  id: "default",
802
- label: "跟随 one 的 Agent 默认",
802
+ label: "跟随 Pi 默认",
803
803
  alias: true,
804
804
  ...(union.length ? { reasoningEfforts: union } : {}),
805
805
  },
@@ -832,7 +832,7 @@ export function parsePiModels(stdout) {
832
832
  if (!discovered.length)
833
833
  return cloneModels(PI_FALLBACK_MODELS);
834
834
  return [
835
- { id: "default", label: "跟随 one 的 Agent 默认", alias: true },
835
+ { id: "default", label: "跟随 Pi 默认", alias: true },
836
836
  ...discovered,
837
837
  ];
838
838
  }