@co0ontty/wand 4.88.0 → 4.89.0-beta.gde0b3cd
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/dist/agent-dispatch.d.ts +5 -0
- package/dist/agent-dispatch.js +8 -0
- package/dist/ai-team-runner.d.ts +1 -1
- package/dist/ai-team-runner.js +13 -5
- package/dist/ai-team-types.js +3 -2
- package/dist/build-info.json +4 -4
- package/dist/commit-task-archive.d.ts +3 -0
- package/dist/commit-task-archive.js +15 -0
- package/dist/config.js +1 -0
- package/dist/daemon-maintenance-state.d.ts +2 -0
- package/dist/daemon-maintenance-targets.d.ts +1 -0
- package/dist/daemon-maintenance-targets.js +26 -0
- package/dist/daemon-maintenance.d.ts +7 -0
- package/dist/daemon-maintenance.js +69 -0
- package/dist/decision-expert-employee.d.ts +1 -1
- package/dist/decision-expert-employee.js +3 -3
- package/dist/decision-expert-service.d.ts +6 -5
- package/dist/decision-expert-service.js +75 -49
- package/dist/decision-types.d.ts +2 -1
- package/dist/employee-text.d.ts +28 -0
- package/dist/employee-text.js +124 -0
- package/dist/git-quick-commit.d.ts +13 -4
- package/dist/git-quick-commit.js +112 -33
- package/dist/local-model-setup.d.ts +7 -1
- package/dist/local-model-setup.js +54 -7
- package/dist/local-model-types.d.ts +2 -1
- package/dist/process-manager.d.ts +4 -1
- package/dist/process-manager.js +5 -6
- package/dist/render-daemon-client.d.ts +2 -0
- package/dist/render-daemon-client.js +6 -0
- package/dist/server-daemon-maintenance-routes.d.ts +8 -0
- package/dist/server-daemon-maintenance-routes.js +19 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +19 -45
- package/dist/server-file-routes.js +61 -25
- package/dist/server-local-model-routes.js +12 -0
- package/dist/server-session-routes.d.ts +2 -1
- package/dist/server-session-routes.js +13 -10
- package/dist/server-speech-routes.d.ts +2 -0
- package/dist/server-speech-routes.js +7 -1
- package/dist/server-task-routes.d.ts +3 -0
- package/dist/server-task-routes.js +13 -10
- package/dist/server.js +38 -11
- package/dist/session-ai-context.d.ts +6 -4
- package/dist/session-ai-context.js +10 -9
- package/dist/silicon-employee-dispatch.d.ts +4 -6
- package/dist/silicon-employee-dispatch.js +9 -6
- package/dist/speech-polisher-employee.d.ts +6 -0
- package/dist/speech-polisher-employee.js +23 -0
- package/dist/speech-polisher-identity.d.ts +4 -0
- package/dist/speech-polisher-identity.js +4 -0
- package/dist/speech-polisher-service.d.ts +31 -0
- package/dist/speech-polisher-service.js +88 -0
- package/dist/speech-service.d.ts +3 -1
- package/dist/speech-service.js +14 -3
- package/dist/speech-support.d.ts +6 -0
- package/dist/speech-support.js +26 -0
- package/dist/speech-types.d.ts +5 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +18 -1
- package/dist/structured-session-manager.d.ts +3 -2
- package/dist/structured-session-manager.js +11 -7
- package/dist/system-employee.d.ts +6 -4
- package/dist/system-employee.js +8 -5
- package/dist/types.d.ts +3 -1
- package/dist/user-memory.d.ts +2 -0
- package/dist/user-memory.js +1 -1
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +324 -212
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -0
- package/dist/web-ui/embedded-assets.js +4 -3
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +2 -0
- package/dist/web-ui/provider-identity.js +7 -1
- package/dist/web-ui/scripts.d.ts +2 -0
- package/dist/web-ui/scripts.js +5 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { LAYA_FILES, LAYA_REPOSITORY, layaFileUrl } from "./laya-model-manifest.
|
|
|
7
7
|
import { ModelFileStore } from "./model-file-store.js";
|
|
8
8
|
import { runModelSetup } from "./model-setup-process.js";
|
|
9
9
|
import { speechModel } from "./speech-models.js";
|
|
10
|
+
import { speechSupport } from "./speech-support.js";
|
|
10
11
|
import { systemEnvValue } from "./env-utils.js";
|
|
11
12
|
import { decisionHardware } from "./decision-hardware.js";
|
|
12
13
|
import { DECISION_EXPERT_ID } from "./decision-expert-identity.js";
|
|
@@ -20,6 +21,8 @@ export class LocalModelSetupService {
|
|
|
20
21
|
jobs = new Map();
|
|
21
22
|
sequence = 0;
|
|
22
23
|
disposed = false;
|
|
24
|
+
speechEnableRevision = 0;
|
|
25
|
+
supportCache = null;
|
|
23
26
|
constructor(deps) {
|
|
24
27
|
this.deps = deps;
|
|
25
28
|
this.files = new ModelFileStore(deps.fetch);
|
|
@@ -55,6 +58,7 @@ export class LocalModelSetupService {
|
|
|
55
58
|
const hardware = decisionHardware();
|
|
56
59
|
const operation = this.operations.get("laya") ?? null;
|
|
57
60
|
const selected = speech.models.find((value) => value.id === speech.settings.model);
|
|
61
|
+
const support = await this.speechSupport(speech.runtime.available);
|
|
58
62
|
return {
|
|
59
63
|
laya: { kind: "laya", label: "LAYA 本地决策", supported,
|
|
60
64
|
reason: !supported ? hardware.message
|
|
@@ -62,12 +66,38 @@ export class LocalModelSetupService {
|
|
|
62
66
|
enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
|
|
63
67
|
downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
|
|
64
68
|
operation: operation ? { ...operation } : null, hardware, decisionEmployeeId: DECISION_EXPERT_ID },
|
|
65
|
-
speech: { kind: "speech", label: "服务端语音识别", supported:
|
|
69
|
+
speech: { kind: "speech", label: "服务端语音识别", supported: support.supported, supportReason: support.reason, reason: speech.reason,
|
|
66
70
|
enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
|
|
67
71
|
downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
|
|
68
72
|
busy: speech.busy || this.jobs.has("speech"), operation: this.operations.has("speech") ? { ...this.operations.get("speech") } : null },
|
|
69
73
|
};
|
|
70
74
|
}
|
|
75
|
+
speechSupport(runtimeAvailable) {
|
|
76
|
+
if (!this.supportCache || this.supportCache.expires < Date.now() || this.supportCache.runtimeAvailable !== runtimeAvailable) {
|
|
77
|
+
this.supportCache = { expires: Date.now() + 30_000, runtimeAvailable, value: (this.deps.speechSupport ?? speechSupport)(runtimeAvailable) };
|
|
78
|
+
}
|
|
79
|
+
return this.supportCache.value;
|
|
80
|
+
}
|
|
81
|
+
async setSpeechEnabled(enabled) {
|
|
82
|
+
const revision = ++this.speechEnableRevision;
|
|
83
|
+
const settingsRevision = this.deps.speech.settingsRevision();
|
|
84
|
+
if (this.disposed)
|
|
85
|
+
throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
|
|
86
|
+
if (!enabled) {
|
|
87
|
+
this.cancel("speech");
|
|
88
|
+
this.deps.speech.configure({ enabled: false });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const status = await this.deps.speech.status();
|
|
92
|
+
const support = await this.speechSupport(status.runtime.available);
|
|
93
|
+
if (revision !== this.speechEnableRevision || this.disposed || settingsRevision !== this.deps.speech.settingsRevision())
|
|
94
|
+
return;
|
|
95
|
+
if (!support.supported)
|
|
96
|
+
throw new DecisionError("UNSUPPORTED", support.reason || "本机不支持语音识别。", 409);
|
|
97
|
+
if (status.busy || status.download?.phase === "downloading")
|
|
98
|
+
throw new DecisionError("BUSY", "语音识别或模型下载正在进行,请稍后启用。", 409);
|
|
99
|
+
this.start("speech", "activate", {});
|
|
100
|
+
}
|
|
71
101
|
parse(kind, raw) {
|
|
72
102
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
73
103
|
throw new DecisionError("INVALID_REQUEST", "模型操作参数无效。");
|
|
@@ -90,6 +120,8 @@ export class LocalModelSetupService {
|
|
|
90
120
|
throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
|
|
91
121
|
if (kind === "laya" && !this.supported())
|
|
92
122
|
throw new DecisionError("UNSUPPORTED", `LAYA-MLX 需要 Apple Silicon macOS 与 Metal;${decisionHardware().message}`, 409);
|
|
123
|
+
if (kind === "laya" && action === "activate")
|
|
124
|
+
throw new DecisionError("INVALID_REQUEST", "一键启用仅用于语音识别。");
|
|
93
125
|
const input = this.parse(kind, raw);
|
|
94
126
|
if (this.jobs.has(kind))
|
|
95
127
|
throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
|
|
@@ -119,6 +151,8 @@ export class LocalModelSetupService {
|
|
|
119
151
|
timer.unref();
|
|
120
152
|
const signal = abort.signal;
|
|
121
153
|
const phase = (value, message) => this.update(kind, id, { phase: value, message });
|
|
154
|
+
let activationMaintenance = false;
|
|
155
|
+
const settingsRevision = this.deps.speech.settingsRevision();
|
|
122
156
|
try {
|
|
123
157
|
if (kind === "laya") {
|
|
124
158
|
let model = await this.layaModel();
|
|
@@ -170,7 +204,7 @@ export class LocalModelSetupService {
|
|
|
170
204
|
}
|
|
171
205
|
else {
|
|
172
206
|
const selected = input.model ?? this.deps.speech.settings().model;
|
|
173
|
-
if (action === "download") {
|
|
207
|
+
if (action === "download" || action === "activate") {
|
|
174
208
|
phase("downloading", "下载并校验语音模型");
|
|
175
209
|
await this.deps.speech.startDownload(selected);
|
|
176
210
|
while (true) {
|
|
@@ -188,7 +222,12 @@ export class LocalModelSetupService {
|
|
|
188
222
|
});
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
|
-
|
|
225
|
+
if (action !== "download") {
|
|
226
|
+
if (action === "activate") {
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
this.deps.speech.acquireMaintenance();
|
|
229
|
+
activationMaintenance = true;
|
|
230
|
+
}
|
|
192
231
|
const status = await this.deps.speech.status();
|
|
193
232
|
signal.throwIfAborted();
|
|
194
233
|
const backend = input.backend === "auto" || !input.backend ? process.platform === "darwin" ? "metal" : "cpu" : input.backend;
|
|
@@ -207,22 +246,30 @@ export class LocalModelSetupService {
|
|
|
207
246
|
await rm(work, { recursive: true, force: true }).catch(() => { });
|
|
208
247
|
}
|
|
209
248
|
}
|
|
210
|
-
phase("initializing", "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
|
|
211
|
-
await this.deps.speech.initializeModel(selected, signal);
|
|
249
|
+
phase("initializing", action === "activate" ? "正在检查语音模型,完成后自动启用。" : "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
|
|
250
|
+
await this.deps.speech.initializeModel(selected, signal, action === "activate" ? "auto" : undefined);
|
|
251
|
+
if (action === "activate") {
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
if (settingsRevision !== this.deps.speech.settingsRevision())
|
|
254
|
+
throw new DecisionError("SETTINGS_CHANGED", "语音设置已由其他操作修改,本次启用已取消。");
|
|
255
|
+
this.deps.speech.configure({ enabled: true, acceleration: "auto" });
|
|
256
|
+
}
|
|
212
257
|
}
|
|
213
258
|
}
|
|
214
259
|
signal.throwIfAborted();
|
|
215
|
-
phase("completed", action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
|
|
260
|
+
phase("completed", action === "activate" ? "服务端语音识别已启用。" : action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
|
|
216
261
|
}
|
|
217
262
|
catch (error) {
|
|
218
263
|
const cancelled = signal.aborted;
|
|
219
264
|
const message = cancelled ? "安装任务已取消,完整文件保留,可重试" : error instanceof DecisionError || (error instanceof Error && /请先|校验|自定义|独立/.test(error.message))
|
|
220
265
|
? error.message : "安装或初始化失败,请检查网络、编译工具/Python版本、磁盘空间和平台能力,再重试。";
|
|
221
266
|
this.update(kind, id, { phase: cancelled ? "cancelled" : "failed", message, ...(cancelled ? {} : { error: message }) });
|
|
222
|
-
if (kind === "speech" && action === "download" && cancelled)
|
|
267
|
+
if (kind === "speech" && (action === "download" || action === "activate") && cancelled)
|
|
223
268
|
this.deps.speech.cancelDownload();
|
|
224
269
|
}
|
|
225
270
|
finally {
|
|
271
|
+
if (activationMaintenance)
|
|
272
|
+
this.deps.speech.releaseMaintenance();
|
|
226
273
|
clearTimeout(timer);
|
|
227
274
|
}
|
|
228
275
|
}
|
|
@@ -3,7 +3,7 @@ export type LocalModelKind = "laya" | "speech";
|
|
|
3
3
|
export type ModelSetupPhase = "downloading" | "verifying" | "runtime" | "initializing" | "completed" | "failed" | "cancelled";
|
|
4
4
|
export interface ModelSetupOperation {
|
|
5
5
|
id: number;
|
|
6
|
-
action: "download" | "initialize";
|
|
6
|
+
action: "download" | "initialize" | "activate";
|
|
7
7
|
phase: ModelSetupPhase;
|
|
8
8
|
message: string;
|
|
9
9
|
received: number;
|
|
@@ -14,6 +14,7 @@ export interface LocalModelStatus {
|
|
|
14
14
|
kind: LocalModelKind;
|
|
15
15
|
label: string;
|
|
16
16
|
supported: boolean;
|
|
17
|
+
supportReason?: string | null;
|
|
17
18
|
reason: string | null;
|
|
18
19
|
enabled: boolean;
|
|
19
20
|
model: string;
|
|
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { WandStorage } from "./storage.js";
|
|
4
4
|
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
5
5
|
import { type PermissionResolution } from "./claude-pty-bridge.js";
|
|
6
|
+
import type { EmployeeTextDeps } from "./employee-text.js";
|
|
6
7
|
import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
|
|
7
8
|
import { samplePtyForegrounds } from "./pty-foreground.js";
|
|
8
9
|
import { type TerminalHost } from "./terminal-host.js";
|
|
@@ -34,6 +35,7 @@ export declare const PTY_TURN_IDLE_MS = 3000;
|
|
|
34
35
|
*/
|
|
35
36
|
export declare const PTY_FOREGROUND_SAMPLE_MS = 1000;
|
|
36
37
|
export interface ProcessManagerOptions {
|
|
38
|
+
free?: EmployeeTextDeps["free"];
|
|
37
39
|
/** Overrides the non-Claude PTY turn quiet window (tests use a short one). */
|
|
38
40
|
ptyTurnIdleMs?: number;
|
|
39
41
|
/** Overrides the foreground-process-group sampling interval (tests use a short one). */
|
|
@@ -55,6 +57,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
55
57
|
private readonly ptyForegroundSampleMs;
|
|
56
58
|
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
57
59
|
private readonly probePtyForegrounds;
|
|
60
|
+
private readonly free;
|
|
58
61
|
private readonly resolveDecisionEvaluate;
|
|
59
62
|
/** 24h archive scan timer */
|
|
60
63
|
private archiveTimer;
|
|
@@ -177,7 +180,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
177
180
|
*/
|
|
178
181
|
setSessionModel(id: string, model: string | null): SessionSnapshot;
|
|
179
182
|
/**
|
|
180
|
-
*
|
|
183
|
+
* 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
|
|
181
184
|
* 取消归档只清标记;重新拉起由恢复入口负责。
|
|
182
185
|
*/
|
|
183
186
|
setSessionArchived(id: string, archived: boolean): SessionSnapshot;
|
package/dist/process-manager.js
CHANGED
|
@@ -563,6 +563,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
563
563
|
ptyForegroundSampleMs;
|
|
564
564
|
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
565
565
|
probePtyForegrounds;
|
|
566
|
+
free;
|
|
566
567
|
resolveDecisionEvaluate;
|
|
567
568
|
/** 24h archive scan timer */
|
|
568
569
|
archiveTimer = null;
|
|
@@ -595,6 +596,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
595
596
|
this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
|
|
596
597
|
this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
|
|
597
598
|
this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
|
|
599
|
+
this.free = options.free;
|
|
598
600
|
this.resolveDecisionEvaluate = options.resolveDecisionEvaluate ?? (() => undefined);
|
|
599
601
|
this.terminalHost = terminalHost ?? new InProcessTerminalHost();
|
|
600
602
|
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
|
|
@@ -1229,7 +1231,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1229
1231
|
ai: resolveSystemAiContext({
|
|
1230
1232
|
provider: groupProvider, runner: undefined, command: providerCliCommand(groupProvider),
|
|
1231
1233
|
structuredState: undefined, selectedModel: null, thinkingEffort: opts?.thinkingEffort,
|
|
1232
|
-
}, this.config, this.storage.getSystemSiliconEmployee()),
|
|
1234
|
+
}, this.config, this.storage.getSystemSiliconEmployee(), this.free),
|
|
1233
1235
|
cwd: resolveSessionCwd(cwd, this.config.defaultCwd),
|
|
1234
1236
|
language: this.config.language,
|
|
1235
1237
|
});
|
|
@@ -1775,13 +1777,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1775
1777
|
return this.snapshot(record);
|
|
1776
1778
|
}
|
|
1777
1779
|
/**
|
|
1778
|
-
*
|
|
1780
|
+
* 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
|
|
1779
1781
|
* 取消归档只清标记;重新拉起由恢复入口负责。
|
|
1780
1782
|
*/
|
|
1781
1783
|
setSessionArchived(id, archived) {
|
|
1782
|
-
if (archived && this.mustGet(id).status === "running") {
|
|
1783
|
-
this.stop(id);
|
|
1784
|
-
}
|
|
1785
1784
|
const record = this.mustGet(id);
|
|
1786
1785
|
record.archived = archived;
|
|
1787
1786
|
record.archivedAt = archived ? (record.archivedAt ?? new Date().toISOString()) : null;
|
|
@@ -2354,7 +2353,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
2354
2353
|
input: prompt,
|
|
2355
2354
|
cwd: record.cwd,
|
|
2356
2355
|
language: this.config.language,
|
|
2357
|
-
ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
|
|
2356
|
+
ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee(), this.free),
|
|
2358
2357
|
readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
|
|
2359
2358
|
hasNativeTitle,
|
|
2360
2359
|
onGenerating: (generating) => {
|
|
@@ -108,6 +108,8 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
108
108
|
sessions: TerminalSessionState[] | null;
|
|
109
109
|
}>;
|
|
110
110
|
requestShutdownDrain(): Promise<void>;
|
|
111
|
+
/** Explicit confirmed maintenance only; does not shut down the daemon itself. */
|
|
112
|
+
interruptMaintenanceSessions(): Promise<void>;
|
|
111
113
|
connect(): Promise<void>;
|
|
112
114
|
private open;
|
|
113
115
|
/**
|
|
@@ -233,6 +233,12 @@ export class RenderDaemonClient {
|
|
|
233
233
|
async requestShutdownDrain() {
|
|
234
234
|
await this.request("shutdown", { mode: "drain" });
|
|
235
235
|
}
|
|
236
|
+
/** Explicit confirmed maintenance only; does not shut down the daemon itself. */
|
|
237
|
+
async interruptMaintenanceSessions() {
|
|
238
|
+
const sessions = await this.listSessions();
|
|
239
|
+
await Promise.all(sessions.filter(session => session.status === "running")
|
|
240
|
+
.map(session => this.request("kill", { sessionId: session.sessionId, signal: "SIGTERM" })));
|
|
241
|
+
}
|
|
236
242
|
connect() {
|
|
237
243
|
if (this.disposed)
|
|
238
244
|
return Promise.reject(new Error("Render client disposed"));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Express, Request, RequestHandler } from "express";
|
|
2
|
+
import type { DaemonMaintenance } from "./daemon-maintenance.js";
|
|
3
|
+
export declare function registerDaemonMaintenanceRoutes(app: Express, options: {
|
|
4
|
+
requireAdmin: RequestHandler;
|
|
5
|
+
canForceUpdate(req: Request): boolean;
|
|
6
|
+
maintenance: Pick<DaemonMaintenance, "status" | "forceUpdate">;
|
|
7
|
+
log(error: unknown): void;
|
|
8
|
+
}): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function registerDaemonMaintenanceRoutes(app, options) {
|
|
2
|
+
const { maintenance } = options;
|
|
3
|
+
app.get("/api/daemon-maintenance", (req, res) => {
|
|
4
|
+
res.set("Cache-Control", "no-store").json({ ...maintenance.status(), canForceUpdate: options.canForceUpdate(req) });
|
|
5
|
+
});
|
|
6
|
+
app.post("/api/daemon-maintenance/force-update", options.requireAdmin, async (req, res) => {
|
|
7
|
+
if (req.body?.confirmInterrupt !== true) {
|
|
8
|
+
res.status(400).json({ error: "请先确认中断正在执行的任务。" });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
res.set("Cache-Control", "no-store").json(await maintenance.forceUpdate());
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
options.log(error);
|
|
16
|
+
res.status(409).json({ error: "强制更新未完成,执行可能仍在停止或组件暂不可用。系统会在空闲后自动重试。", ...maintenance.status() });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
2
|
import { type SiliconEmployee, type SiliconEmployeeDraft } from "./ai-team-types.js";
|
|
3
|
+
import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
|
|
3
4
|
import type { WandStorage } from "./storage.js";
|
|
4
5
|
import type { WandTaskAgent } from "./task-types.js";
|
|
5
6
|
import type { WandConfig } from "./types.js";
|
|
@@ -17,6 +18,7 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
|
|
|
17
18
|
storage: WandStorage;
|
|
18
19
|
notifyEmployeeChanged?: (employeeId: string) => void;
|
|
19
20
|
config?: WandConfig;
|
|
21
|
+
free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
|
|
20
22
|
/** 测试注入点:默认按系统 AI 配置真实调用模型。 */
|
|
21
23
|
generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
|
|
22
24
|
/** 测试注入点:CLI 可用性探测。 */
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
|
-
import { DECISION_EXPERT_KEY, isLocalDecisionModel } from "./decision-expert-identity.js";
|
|
5
|
-
import { OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
|
|
6
4
|
import { asyncRoute } from "./express-async.js";
|
|
7
5
|
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
8
6
|
import { parseTaskAgent } from "./server-task-routes.js";
|
|
@@ -14,7 +12,7 @@ import { SESSION_PROVIDERS } from "./session-provider.js";
|
|
|
14
12
|
import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
|
|
15
13
|
import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
|
|
16
14
|
/** 员工起草没有会话上下文,按「默认 provider」解析,与任务标题 / 提示词优化一致。 */
|
|
17
|
-
function employeeDraftAiOptions(storage, config) {
|
|
15
|
+
function employeeDraftAiOptions(storage, config, free) {
|
|
18
16
|
if (!config)
|
|
19
17
|
return {};
|
|
20
18
|
const provider = config.defaultProvider ?? "claude";
|
|
@@ -25,7 +23,7 @@ function employeeDraftAiOptions(storage, config) {
|
|
|
25
23
|
command: provider === "qoder" ? "qodercli" : provider,
|
|
26
24
|
selectedModel: null,
|
|
27
25
|
thinkingEffort: config.defaultThinkingEffort,
|
|
28
|
-
}, config, storage.getSystemSiliconEmployee());
|
|
26
|
+
}, config, storage.getSystemSiliconEmployee(), free);
|
|
29
27
|
}
|
|
30
28
|
function sendEmployeeError(res, error, context) {
|
|
31
29
|
const message = getErrorMessage(error, "硅基员工操作失败。");
|
|
@@ -99,23 +97,23 @@ export function parseSystemEmployeeAgents(value, existing) {
|
|
|
99
97
|
if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
|
|
100
98
|
throw new Error("该员工是内置的,身份不可修改。");
|
|
101
99
|
}
|
|
102
|
-
|
|
100
|
+
return parseEmployeeAgents(body.agents);
|
|
101
|
+
}
|
|
102
|
+
/** One validation owner for every employee candidate list, including legacy single agents. */
|
|
103
|
+
function parseEmployeeAgents(value) {
|
|
104
|
+
if (!Array.isArray(value))
|
|
103
105
|
throw new Error("请提供执行候选数组(CLI / SDK)。");
|
|
104
|
-
if (
|
|
106
|
+
if (value.length === 0)
|
|
105
107
|
throw new Error("至少需要一个执行候选。");
|
|
106
|
-
if (
|
|
108
|
+
if (value.length > AI_TEAM_MAX_CANDIDATES) {
|
|
107
109
|
throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
|
|
108
110
|
}
|
|
109
|
-
const agents =
|
|
111
|
+
const agents = value.map((rawAgent) => {
|
|
110
112
|
const parsed = parseTaskAgent(rawAgent ?? {});
|
|
111
113
|
if (!parsed)
|
|
112
114
|
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
113
115
|
if (parsed.kind !== "structured")
|
|
114
116
|
throw new Error("硅基员工只支持结构化会话。");
|
|
115
|
-
if (existing.systemKey === DECISION_EXPERT_KEY && (parsed.provider !== "pi" || parsed.engine !== "sdk"
|
|
116
|
-
|| !(isLocalDecisionModel(parsed.model) || parsed.model === OPENROUTER_FREE_SELECTOR || isOpenRouterFreeSelector(parsed.model)))) {
|
|
117
|
-
throw new Error("决策专家仅支持 LAYA 或 Wand 免费分组的决策候选;请使用 Wand 内部执行入口。");
|
|
118
|
-
}
|
|
119
117
|
return parsed;
|
|
120
118
|
});
|
|
121
119
|
if (new Set(agents.map(agentKey)).size !== agents.length) {
|
|
@@ -132,40 +130,16 @@ export function parseSiliconEmployeeInput(value, existing, now) {
|
|
|
132
130
|
const prompt = text(body.prompt);
|
|
133
131
|
if (prompt.length > 20_000)
|
|
134
132
|
throw new Error("员工设定 Prompt 不能超过 20000 个字符。");
|
|
135
|
-
let
|
|
136
|
-
if (Array.isArray(body.agents))
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
|
|
140
|
-
throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
|
|
141
|
-
}
|
|
142
|
-
agents = body.agents.map((rawAgent) => {
|
|
143
|
-
const parsed = parseTaskAgent(rawAgent ?? {});
|
|
144
|
-
if (!parsed)
|
|
145
|
-
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
146
|
-
if (parsed.kind !== "structured")
|
|
147
|
-
throw new Error("硅基员工只支持结构化会话。");
|
|
148
|
-
return parsed;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
else if (body.agents !== undefined && body.agents !== null) {
|
|
133
|
+
let candidates;
|
|
134
|
+
if (Array.isArray(body.agents))
|
|
135
|
+
candidates = body.agents;
|
|
136
|
+
else if (body.agents !== undefined && body.agents !== null)
|
|
152
137
|
throw new Error("候选执行配置必须是数组。");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!singleAgent)
|
|
157
|
-
throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
|
|
158
|
-
if (singleAgent.kind !== "structured")
|
|
159
|
-
throw new Error("硅基员工只支持结构化会话。");
|
|
160
|
-
agents = [singleAgent];
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
138
|
+
else if (body.agent !== undefined && body.agent !== null)
|
|
139
|
+
candidates = [body.agent];
|
|
140
|
+
else
|
|
163
141
|
throw new Error("至少需要一个执行候选。");
|
|
164
|
-
|
|
165
|
-
const seenKeys = new Set(agents.map(agentKey));
|
|
166
|
-
if (seenKeys.size !== agents.length) {
|
|
167
|
-
throw new Error("执行候选存在重复。");
|
|
168
|
-
}
|
|
142
|
+
const agents = parseEmployeeAgents(candidates);
|
|
169
143
|
const avatar = parseAvatar(body.avatar, name);
|
|
170
144
|
const tags = parseSiliconEmployeeTags(body.tags === undefined ? existing?.tags ?? [] : body.tags);
|
|
171
145
|
const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
|
|
@@ -203,7 +177,7 @@ export function registerSiliconEmployeeRoutes(app, deps) {
|
|
|
203
177
|
const available = availableEmployeeProviders(resolveDefaults().provider, isProviderAvailable);
|
|
204
178
|
return available.length ? available : [...SESSION_PROVIDERS];
|
|
205
179
|
};
|
|
206
|
-
const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config), {
|
|
180
|
+
const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config, deps.free), {
|
|
207
181
|
cwd: config?.defaultCwd || process.cwd(),
|
|
208
182
|
language: config?.language ?? "",
|
|
209
183
|
existingNames: request.existingNames,
|
|
@@ -10,6 +10,7 @@ import { asyncRoute } from "./express-async.js";
|
|
|
10
10
|
import { sendRouteError } from "./server-request.js";
|
|
11
11
|
import { isBlockedFolderPath, isPathWithinBase, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
12
12
|
import { parseBoundedInteger } from "./request-limits.js";
|
|
13
|
+
import { runGitRawAsync } from "./git-utils.js";
|
|
13
14
|
const execAsync = promisify(exec);
|
|
14
15
|
const DIRECTORY_MAX_ITEMS = 200;
|
|
15
16
|
const MAX_TEXT_PREVIEW_SIZE = 512 * 1024;
|
|
@@ -593,7 +594,10 @@ export function registerFileRoutes(app, deps) {
|
|
|
593
594
|
const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
|
|
594
595
|
const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
|
|
595
596
|
const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
|
|
596
|
-
const
|
|
597
|
+
const includeGenerated = req.query.includeGenerated === "true";
|
|
598
|
+
// Internal metadata remains excluded even when the user opts into generated files.
|
|
599
|
+
const ignoredDirectories = new Set([".git", ".wand", ".wand-uploads", ".wand-team"]);
|
|
600
|
+
const generatedDirectories = new Set(["node_modules", ".next", "dist", "build", "coverage"]);
|
|
597
601
|
const maxVisitedEntries = 20_000;
|
|
598
602
|
let resolvedCwd;
|
|
599
603
|
try {
|
|
@@ -612,38 +616,70 @@ export function registerFileRoutes(app, deps) {
|
|
|
612
616
|
return;
|
|
613
617
|
}
|
|
614
618
|
try {
|
|
619
|
+
let gitIgnored = new Set();
|
|
620
|
+
try {
|
|
621
|
+
const ignored = await runGitRawAsync(["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"], resolvedCwd, { timeout: 2_000, maxBuffer: 2 * 1024 * 1024 });
|
|
622
|
+
gitIgnored = new Set(ignored.split("\0").filter(Boolean).map((entry) => path.resolve(resolvedCwd, entry.replace(/\/$/, ""))));
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// Ordinary directories and hosts without Git still retain the existing known-directory exclusions.
|
|
626
|
+
}
|
|
615
627
|
const results = [];
|
|
628
|
+
const deferred = [];
|
|
616
629
|
const queryLower = query.toLowerCase();
|
|
617
630
|
let visitedEntries = 0;
|
|
618
|
-
async function searchDir(dirPath, currentDepth) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (results.length >= maxResults || visitedEntries >= maxVisitedEntries)
|
|
624
|
-
break;
|
|
625
|
-
visitedEntries += 1;
|
|
626
|
-
if (entry.isDirectory() && ignoredDirectories.has(entry.name))
|
|
631
|
+
async function searchDir(dirPath, currentDepth, generated = false) {
|
|
632
|
+
const directories = [{ path: dirPath, depth: currentDepth }];
|
|
633
|
+
for (let index = 0; index < directories.length && visitedEntries < maxVisitedEntries; index += 1) {
|
|
634
|
+
const directory = directories[index];
|
|
635
|
+
if (directory.depth > maxDepth)
|
|
627
636
|
continue;
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
+
const entries = await readdir(directory.path, { withFileTypes: true });
|
|
638
|
+
for (const entry of entries) {
|
|
639
|
+
if (visitedEntries >= maxVisitedEntries)
|
|
640
|
+
break;
|
|
641
|
+
visitedEntries += 1;
|
|
642
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name))
|
|
643
|
+
continue;
|
|
644
|
+
const entryPath = path.join(directory.path, entry.name);
|
|
645
|
+
if (!generated && entry.isDirectory() && generatedDirectories.has(entry.name)) {
|
|
646
|
+
if (includeGenerated)
|
|
647
|
+
deferred.push({ path: entryPath, name: entry.name, depth: directory.depth + 1 });
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (!generated && gitIgnored.has(entryPath))
|
|
651
|
+
continue;
|
|
652
|
+
const matchIndex = entry.name.toLowerCase().indexOf(queryLower);
|
|
653
|
+
if (matchIndex !== -1) {
|
|
654
|
+
results.push({
|
|
655
|
+
path: entryPath,
|
|
656
|
+
name: entry.name,
|
|
657
|
+
type: entry.isDirectory() ? "dir" : "file",
|
|
658
|
+
matchScore: matchIndex,
|
|
659
|
+
generated,
|
|
660
|
+
depth: directory.depth,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (entry.isDirectory() && directory.depth < maxDepth)
|
|
664
|
+
directories.push({ path: entryPath, depth: directory.depth + 1 });
|
|
637
665
|
}
|
|
638
|
-
if (entry.isDirectory())
|
|
639
|
-
await searchDir(entryPath, currentDepth + 1);
|
|
640
666
|
}
|
|
641
667
|
}
|
|
642
668
|
await searchDir(resolvedCwd, 0);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
669
|
+
// Complete the project-file pass before dependencies can consume the bounded result budget.
|
|
670
|
+
for (const directory of deferred) {
|
|
671
|
+
if (visitedEntries >= maxVisitedEntries)
|
|
672
|
+
break;
|
|
673
|
+
const matchScore = directory.name.toLowerCase().indexOf(queryLower);
|
|
674
|
+
if (matchScore !== -1)
|
|
675
|
+
results.push({ path: directory.path, name: directory.name, type: "dir", matchScore, generated: true, depth: directory.depth - 1 });
|
|
676
|
+
await searchDir(directory.path, directory.depth, true);
|
|
677
|
+
}
|
|
678
|
+
results.sort((a, b) => Number(a.generated) - Number(b.generated)
|
|
679
|
+
|| Number(b.name.toLowerCase() === queryLower) - Number(a.name.toLowerCase() === queryLower)
|
|
680
|
+
|| a.depth - b.depth || a.matchScore - b.matchScore || a.name.localeCompare(b.name));
|
|
681
|
+
res.json({ results: results.slice(0, maxResults), query, cwd: resolvedCwd,
|
|
682
|
+
truncated: results.length > maxResults || visitedEntries >= maxVisitedEntries });
|
|
647
683
|
}
|
|
648
684
|
catch (error) {
|
|
649
685
|
sendRouteError(res, error, "搜索失败。可能原因:路径不存在或权限不足。");
|
|
@@ -51,6 +51,18 @@ export function registerLocalModelRoutes(app, deps) {
|
|
|
51
51
|
next(error);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
+
app.patch("/api/local-models/speech/settings", deps.requireAdmin, async (req, res, next) => {
|
|
55
|
+
try {
|
|
56
|
+
if (!req.body || Array.isArray(req.body) || Object.keys(req.body).length !== 1 || typeof req.body.enabled !== "boolean") {
|
|
57
|
+
throw new DecisionError("INVALID_REQUEST", "仅允许修改语音启用状态。");
|
|
58
|
+
}
|
|
59
|
+
await deps.models.setSpeechEnabled(req.body.enabled);
|
|
60
|
+
res.status(202).json(await deps.models.status());
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
next(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
54
66
|
const errors = (error, _req, res, _next) => {
|
|
55
67
|
if (error instanceof DecisionError || error instanceof SpeechError) {
|
|
56
68
|
res.status(error.status).json({ error: error.message, code: error.code });
|
|
@@ -3,6 +3,7 @@ import { ProcessManager } from "./process-manager.js";
|
|
|
3
3
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
5
|
import { ExecutionMode, ProcessEvent, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
6
|
+
import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
|
|
6
7
|
import { toSessionListItemDTO } from "./session-transport.js";
|
|
7
8
|
import { type ProviderHistoryDeleter, SessionRegistry } from "./session-registry.js";
|
|
8
9
|
import { type PiRecommendationRuntime } from "./server-pi-recommendation-routes.js";
|
|
@@ -46,6 +47,6 @@ export interface SessionDirectoryTreeResponse {
|
|
|
46
47
|
}
|
|
47
48
|
export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
|
|
48
49
|
export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
|
|
49
|
-
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void, decisions?: PiRecommendationRuntime): void;
|
|
50
|
+
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void, decisions?: PiRecommendationRuntime, free?: Pick<OpenRouterFreeModelsService, "resolveForCall">): void;
|
|
50
51
|
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
|
|
51
52
|
export {};
|