@co0ontty/wand 4.85.1 → 4.87.0
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 +3 -3
- 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
package/dist/web-ui/index.js
CHANGED
|
@@ -16,7 +16,9 @@ export function renderApp(configPath, page = "console", settingsAuth = "browser"
|
|
|
16
16
|
<html lang="zh-CN"${page === "settings" ? ` data-wand-page="settings" data-wand-settings-auth="${settingsAuth}"` : ""}>
|
|
17
17
|
<head>
|
|
18
18
|
<meta charset="utf-8" />
|
|
19
|
-
<meta name="viewport" content="width=device-width, initial-scale=1,
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
20
|
+
<meta name="wand-xterm-script" content="${xtermSrc}" />
|
|
21
|
+
<meta name="wand-qrcode-script" content="${qrcodeSrc}" />
|
|
20
22
|
<title>${page === "settings" ? "Wand 设置" : "Wand Console"}</title>
|
|
21
23
|
<link rel="icon" type="image/svg+xml" href="${WAND_FAVICON_URL}" />
|
|
22
24
|
<meta name="description" content="Local CLI Console for Vibe Coding - Manage terminal sessions from your browser" />
|
|
@@ -30,8 +32,6 @@ export function renderApp(configPath, page = "console", settingsAuth = "browser"
|
|
|
30
32
|
<body>
|
|
31
33
|
<div id="app"></div>
|
|
32
34
|
<div id="overlay-root" data-wand-ui-root></div>
|
|
33
|
-
<script src="${xtermSrc}"></script>
|
|
34
|
-
<script src="${qrcodeSrc}"></script>
|
|
35
35
|
<script src="${scriptSrc}"></script>
|
|
36
36
|
</body>
|
|
37
37
|
</html>`;
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { SESSION_PROVIDERS as PROVIDER_IDS, inferProviderFromCommand, normalizeProviderId, type SessionProvider } from "../provider-catalog.js";
|
|
2
|
-
export { inferProviderFromCommand, providerCliCommand, providerDisplayName, isNativeThinkingEffort, NATIVE_THINKING_EFFORT_PATTERN, PROVIDER_LABELS, } from "../provider-catalog.js";
|
|
2
|
+
export { inferProviderFromCommand, providerCliCommand, providerDisplayName, isNativeThinkingEffort, NATIVE_THINKING_EFFORT_PATTERN, PROVIDER_LABELS, WAND_AGENT_LABEL, } from "../provider-catalog.js";
|
|
3
3
|
export { PROVIDER_IDS, normalizeProviderId };
|
|
4
4
|
export type ProviderId = SessionProvider;
|
|
5
5
|
/** 兼容旧名:浏览器层的调用点叫 inferProviderIdFromCommand。 */
|
|
6
6
|
export declare const inferProviderIdFromCommand: typeof inferProviderFromCommand;
|
|
7
|
+
/** 同一 provider 的不同执行路径:CLI 起外部进程,SDK 在 Wand 进程内跑(目前只有 pi 两种都有)。 */
|
|
8
|
+
export type AgentToolEngine = "cli" | "sdk";
|
|
9
|
+
/**
|
|
10
|
+
* 一条可选的执行工具。
|
|
11
|
+
*
|
|
12
|
+
* `pi` 与 `wand-agent` 共享同一个 provider(都是 pi),但执行方式不同:
|
|
13
|
+
* 前者起 Pi CLI 进程(结构化 JSON / PTY),后者用进程内 SDK harness。
|
|
14
|
+
* 所以 UI 上的「工具」比 provider 多一个维度,不能只拿 provider 当选项值。
|
|
15
|
+
*/
|
|
16
|
+
export interface AgentToolOption {
|
|
17
|
+
id: string;
|
|
18
|
+
provider: ProviderId;
|
|
19
|
+
engine?: AgentToolEngine;
|
|
20
|
+
label: string;
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
/** Wand Agent(进程内 SDK)的选项 id;不是 provider 名,只是一个 UI 侧的稳定标识。 */
|
|
24
|
+
export declare const WAND_AGENT_TOOL_ID = "wand-agent";
|
|
25
|
+
/** 执行工具清单:顺序即 UI 顺序,与 provider-catalog 的 SESSION_PROVIDERS 保持一致。 */
|
|
26
|
+
export declare const AGENT_TOOL_OPTIONS: readonly AgentToolOption[];
|
|
27
|
+
/** 按选项 id 取执行工具;认不出来时返回 null(调用方自己决定回退)。 */
|
|
28
|
+
export declare function agentToolOption(id: string | null | undefined): AgentToolOption | null;
|
|
29
|
+
/**
|
|
30
|
+
* 由 provider + 引擎反推选项 id。
|
|
31
|
+
* 缺省引擎视为 CLI:历史上只有 pi 有两条路,其他 provider 只有 CLI。
|
|
32
|
+
*/
|
|
33
|
+
export declare function agentToolIdFor(provider: ProviderId, engine?: AgentToolEngine | null): string;
|
|
7
34
|
/** 严格判断:只接受 7 个 provider 字面量,不做别名 / 大小写归一。 */
|
|
8
35
|
export declare function isProviderId(value: unknown): value is ProviderId;
|
|
9
36
|
export declare const CLAUDE_BRAND_COLOR = "#D97757";
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
// Provider 的列表 / 别名 / 展示名 / 推断规则统一在 src/provider-catalog.ts(服务端共用)。
|
|
2
2
|
// 这里只补充浏览器层特有的品牌 logo 与 HTML 渲染。
|
|
3
|
-
import { SESSION_PROVIDERS as PROVIDER_IDS, inferProviderFromCommand, normalizeProviderId, } from "../provider-catalog.js";
|
|
4
|
-
export { inferProviderFromCommand, providerCliCommand, providerDisplayName, isNativeThinkingEffort, NATIVE_THINKING_EFFORT_PATTERN, PROVIDER_LABELS, } from "../provider-catalog.js";
|
|
3
|
+
import { SESSION_PROVIDERS as PROVIDER_IDS, PROVIDER_LABELS, WAND_AGENT_LABEL, inferProviderFromCommand, normalizeProviderId, } from "../provider-catalog.js";
|
|
4
|
+
export { inferProviderFromCommand, providerCliCommand, providerDisplayName, isNativeThinkingEffort, NATIVE_THINKING_EFFORT_PATTERN, PROVIDER_LABELS, WAND_AGENT_LABEL, } from "../provider-catalog.js";
|
|
5
5
|
export { PROVIDER_IDS, normalizeProviderId };
|
|
6
6
|
/** 兼容旧名:浏览器层的调用点叫 inferProviderIdFromCommand。 */
|
|
7
7
|
export const inferProviderIdFromCommand = inferProviderFromCommand;
|
|
8
|
+
/** Wand Agent(进程内 SDK)的选项 id;不是 provider 名,只是一个 UI 侧的稳定标识。 */
|
|
9
|
+
export const WAND_AGENT_TOOL_ID = "wand-agent";
|
|
10
|
+
/** 执行工具清单:顺序即 UI 顺序,与 provider-catalog 的 SESSION_PROVIDERS 保持一致。 */
|
|
11
|
+
export const AGENT_TOOL_OPTIONS = [
|
|
12
|
+
{ id: "claude", provider: "claude", label: PROVIDER_LABELS.claude, description: "Claude Code" },
|
|
13
|
+
{ id: "codex", provider: "codex", label: PROVIDER_LABELS.codex, description: "OpenAI Codex CLI" },
|
|
14
|
+
{ id: "opencode", provider: "opencode", label: PROVIDER_LABELS.opencode, description: "OpenCode CLI" },
|
|
15
|
+
{ id: "grok", provider: "grok", label: PROVIDER_LABELS.grok, description: "Grok Build CLI" },
|
|
16
|
+
{ id: "qoder", provider: "qoder", label: PROVIDER_LABELS.qoder, description: "Qoder CLI" },
|
|
17
|
+
{ id: "pi", provider: "pi", engine: "cli", label: PROVIDER_LABELS.pi, description: "Pi CLI:结构化 JSON 或 PTY 终端" },
|
|
18
|
+
{ id: WAND_AGENT_TOOL_ID, provider: "pi", engine: "sdk", label: WAND_AGENT_LABEL,
|
|
19
|
+
description: "Wand 自带 Agent:进程内 SDK 执行,只支持结构化会话" },
|
|
20
|
+
{ id: "gemini", provider: "gemini", label: PROVIDER_LABELS.gemini, description: "Gemini CLI" },
|
|
21
|
+
];
|
|
22
|
+
/** 按选项 id 取执行工具;认不出来时返回 null(调用方自己决定回退)。 */
|
|
23
|
+
export function agentToolOption(id) {
|
|
24
|
+
if (!id)
|
|
25
|
+
return null;
|
|
26
|
+
return AGENT_TOOL_OPTIONS.find((option) => option.id === id) ?? null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 由 provider + 引擎反推选项 id。
|
|
30
|
+
* 缺省引擎视为 CLI:历史上只有 pi 有两条路,其他 provider 只有 CLI。
|
|
31
|
+
*/
|
|
32
|
+
export function agentToolIdFor(provider, engine) {
|
|
33
|
+
if (provider === "pi" && engine === "sdk")
|
|
34
|
+
return WAND_AGENT_TOOL_ID;
|
|
35
|
+
return provider;
|
|
36
|
+
}
|
|
8
37
|
const PROVIDER_ID_SET = new Set(PROVIDER_IDS);
|
|
9
38
|
/** 严格判断:只接受 7 个 provider 字面量,不做别名 / 大小写归一。 */
|
|
10
39
|
export function isProviderId(value) {
|
|
@@ -5,6 +5,7 @@ export interface SessionActivitySource {
|
|
|
5
5
|
readonly status?: string;
|
|
6
6
|
readonly provider?: string;
|
|
7
7
|
readonly ptyBusy?: boolean;
|
|
8
|
+
readonly ptyCommandRunning?: boolean;
|
|
8
9
|
readonly providerCliActive?: boolean;
|
|
9
10
|
readonly archived?: boolean;
|
|
10
11
|
readonly permissionBlocked?: boolean;
|
|
@@ -5,7 +5,9 @@ function isStructured(session) {
|
|
|
5
5
|
export function ptyTurnActive(session) {
|
|
6
6
|
if (!session || session.status !== "running" || isStructured(session))
|
|
7
7
|
return false;
|
|
8
|
-
return session.provider && PROVIDER_CLI_IDS.has(session.provider)
|
|
8
|
+
return session.provider && PROVIDER_CLI_IDS.has(session.provider)
|
|
9
|
+
? session.ptyBusy === true
|
|
10
|
+
: session.ptyCommandRunning === true;
|
|
9
11
|
}
|
|
10
12
|
export function computeRunningSignal(session) {
|
|
11
13
|
if (!session || session.archived) {
|
|
@@ -13,7 +15,8 @@ export function computeRunningSignal(session) {
|
|
|
13
15
|
}
|
|
14
16
|
const permissionBlocked = Boolean(session.permissionBlocked);
|
|
15
17
|
const inFlight = isStructured(session) && Boolean(session.structuredState?.inFlight);
|
|
16
|
-
|
|
18
|
+
// The launch marker describes the original provider CLI, not manual commands in a blank shell.
|
|
19
|
+
const ptyRunning = ptyTurnActive(session) && (!session.provider || session.providerCliActive !== false);
|
|
17
20
|
return { active: inFlight || ptyRunning || permissionBlocked, inFlight, ptyRunning, permissionBlocked };
|
|
18
21
|
}
|
|
19
22
|
export function formatElapsedShort(ms) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SPEECH_MAX_SECONDS, SPEECH_SAMPLE_RATE } from "../speech-types.js";
|
|
2
|
+
/** Encode and downsample locally so hosts need no ffmpeg/codec dependencies. */
|
|
3
|
+
export function encodeSpeechWav(channels, sourceRate) {
|
|
4
|
+
if (!channels.length || !Number.isFinite(sourceRate) || sourceRate < SPEECH_SAMPLE_RATE
|
|
5
|
+
|| !channels[0]?.length || channels.some((channel) => channel.length !== channels[0].length))
|
|
6
|
+
throw new Error("麦克风音频格式无效。");
|
|
7
|
+
const frames = channels[0].length;
|
|
8
|
+
if (frames / sourceRate > SPEECH_MAX_SECONDS + 0.05)
|
|
9
|
+
throw new Error("语音最长 60 秒,请分段录音。");
|
|
10
|
+
const count = Math.min(SPEECH_SAMPLE_RATE * SPEECH_MAX_SECONDS, Math.floor(frames * SPEECH_SAMPLE_RATE / sourceRate));
|
|
11
|
+
if (count < 1600)
|
|
12
|
+
throw new Error("录音太短,请按住麦克风说话。");
|
|
13
|
+
const bytes = new ArrayBuffer(44 + count * 2);
|
|
14
|
+
const view = new DataView(bytes);
|
|
15
|
+
const ascii = (offset, value) => { for (let index = 0; index < value.length; index += 1)
|
|
16
|
+
view.setUint8(offset + index, value.charCodeAt(index)); };
|
|
17
|
+
ascii(0, "RIFF");
|
|
18
|
+
view.setUint32(4, bytes.byteLength - 8, true);
|
|
19
|
+
ascii(8, "WAVEfmt ");
|
|
20
|
+
view.setUint32(16, 16, true);
|
|
21
|
+
view.setUint16(20, 1, true);
|
|
22
|
+
view.setUint16(22, 1, true);
|
|
23
|
+
view.setUint32(24, SPEECH_SAMPLE_RATE, true);
|
|
24
|
+
view.setUint32(28, SPEECH_SAMPLE_RATE * 2, true);
|
|
25
|
+
view.setUint16(32, 2, true);
|
|
26
|
+
view.setUint16(34, 16, true);
|
|
27
|
+
ascii(36, "data");
|
|
28
|
+
view.setUint32(40, count * 2, true);
|
|
29
|
+
const ratio = sourceRate / SPEECH_SAMPLE_RATE;
|
|
30
|
+
for (let index = 0; index < count; index += 1) {
|
|
31
|
+
const start = Math.floor(index * ratio);
|
|
32
|
+
const end = Math.max(start + 1, Math.floor((index + 1) * ratio));
|
|
33
|
+
let sample = 0;
|
|
34
|
+
for (const channel of channels)
|
|
35
|
+
for (let source = start; source < end; source += 1)
|
|
36
|
+
sample += channel[source] ?? 0;
|
|
37
|
+
sample = Math.max(-1, Math.min(1, sample / ((end - start) * channels.length)));
|
|
38
|
+
view.setInt16(44 + index * 2, Math.round(sample < 0 ? sample * 32768 : sample * 32767), true);
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Optional browser libraries keep their versioned URLs in the server-rendered shell. */
|
|
2
|
+
const pending = new Map();
|
|
3
|
+
function loadVendor(name, ready) {
|
|
4
|
+
if (ready())
|
|
5
|
+
return Promise.resolve();
|
|
6
|
+
const existing = pending.get(name);
|
|
7
|
+
if (existing)
|
|
8
|
+
return existing;
|
|
9
|
+
const src = document.querySelector(`meta[name="wand-${name}-script"]`)?.content;
|
|
10
|
+
if (!src)
|
|
11
|
+
return Promise.reject(new Error("组件地址不可用,请刷新页面后重试。"));
|
|
12
|
+
const promise = new Promise((resolve, reject) => {
|
|
13
|
+
const script = document.createElement("script");
|
|
14
|
+
script.src = src;
|
|
15
|
+
script.async = true;
|
|
16
|
+
let finished = false;
|
|
17
|
+
const finish = (error) => {
|
|
18
|
+
if (finished)
|
|
19
|
+
return;
|
|
20
|
+
finished = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
script.onload = null;
|
|
23
|
+
script.onerror = null;
|
|
24
|
+
if (error) {
|
|
25
|
+
script.remove();
|
|
26
|
+
reject(error);
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(() => finish(new Error("组件加载超时,请重试。")), 20_000);
|
|
32
|
+
script.onload = () => finish(ready() ? undefined : new Error("组件加载未完成,请重试。"));
|
|
33
|
+
script.onerror = () => finish(new Error("组件加载失败,请重试。"));
|
|
34
|
+
document.head.appendChild(script);
|
|
35
|
+
}).finally(() => { if (pending.get(name) === promise)
|
|
36
|
+
pending.delete(name); });
|
|
37
|
+
pending.set(name, promise);
|
|
38
|
+
return promise;
|
|
39
|
+
}
|
|
40
|
+
export async function ensureQrCodeLibrary() {
|
|
41
|
+
const globals = globalThis;
|
|
42
|
+
await loadVendor("qrcode", () => typeof globals.QRCodeLib?.toCanvas === "function");
|
|
43
|
+
return globals.QRCodeLib;
|
|
44
|
+
}
|
|
45
|
+
export async function ensureTerminalLibrary() {
|
|
46
|
+
await loadVendor("xterm", () => {
|
|
47
|
+
const library = globalThis.XTermLib;
|
|
48
|
+
return typeof library?.Terminal === "function" && typeof library.FitAddon === "function";
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/ws-broadcast.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { WebSocket } from "ws";
|
|
6
6
|
import { readSessionCookie } from "./auth.js";
|
|
7
|
-
import { blockWindowMessagesForTransport, compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
7
|
+
import { blockWindowMessagesForTransport, compactToolMessagesForTransport, messageWindowByteBudget, windowMessagesForTransport } from "./message-truncator.js";
|
|
8
8
|
import { boundSessionEventData, toSessionDetailDTO } from "./session-transport.js";
|
|
9
9
|
import { enrichStructuredMessages } from "./structured-client-protocol.js";
|
|
10
10
|
// ── Constants ──
|
|
@@ -154,6 +154,10 @@ export class WsBroadcastManager {
|
|
|
154
154
|
if (Number.isSafeInteger(msg.blockBudget) && msg.blockBudget > 0) {
|
|
155
155
|
client.blockBudget = Math.min(msg.blockBudget, MAX_BLOCK_BUDGET);
|
|
156
156
|
}
|
|
157
|
+
if (Number.isSafeInteger(msg.byteBudget) && msg.byteBudget > 0) {
|
|
158
|
+
client.byteBudget = messageWindowByteBudget(msg.byteBudget);
|
|
159
|
+
}
|
|
160
|
+
client.omitStructuredOutput = client.omitStructuredOutput || msg.omitStructuredOutput === true;
|
|
157
161
|
// 默认仍是历史的“切换当前会话”;分屏池显式用 mode:add 叠加订阅。
|
|
158
162
|
if (msg.mode !== "add") {
|
|
159
163
|
client.ptySubscriptions.clear();
|
|
@@ -398,7 +402,7 @@ export class WsBroadcastManager {
|
|
|
398
402
|
...(resync ? { resync: true } : {}),
|
|
399
403
|
data: {
|
|
400
404
|
...toSessionDetailDTO(snapshot, {
|
|
401
|
-
output: snapshot.output,
|
|
405
|
+
output: client.omitStructuredOutput && snapshot.sessionKind === "structured" ? "" : snapshot.output,
|
|
402
406
|
outputLimit: terminalState ? 16_000 : undefined,
|
|
403
407
|
...windowed,
|
|
404
408
|
}),
|
|
@@ -420,7 +424,7 @@ export class WsBroadcastManager {
|
|
|
420
424
|
messages = compactToolMessagesForTransport(messages);
|
|
421
425
|
}
|
|
422
426
|
if (client.blockBudget && client.blockBudget > 0) {
|
|
423
|
-
const w = blockWindowMessagesForTransport(messages, this.getCardDefaults(), client.blockBudget);
|
|
427
|
+
const w = blockWindowMessagesForTransport(messages, this.getCardDefaults(), client.blockBudget, client.byteBudget, client.byteBudget !== undefined);
|
|
424
428
|
return {
|
|
425
429
|
messages: w.messages,
|
|
426
430
|
messageOffset: w.messageOffset,
|
|
@@ -484,7 +488,9 @@ export class WsBroadcastManager {
|
|
|
484
488
|
if (client.compactTools || (client.blockBudget && client.blockBudget > 0)) {
|
|
485
489
|
return {
|
|
486
490
|
...boundedEvent,
|
|
487
|
-
data: { ...data, ...this.windowForClient(client, event.sessionId, rawMessages)
|
|
491
|
+
data: { ...data, ...this.windowForClient(client, event.sessionId, rawMessages),
|
|
492
|
+
...(client.omitStructuredOutput && (data?.sessionKind === "structured" || data?.structuredState)
|
|
493
|
+
? { output: "" } : {}) },
|
|
488
494
|
};
|
|
489
495
|
}
|
|
490
496
|
if (!turnWindowedEvent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@co0ontty/wand",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.87.0",
|
|
4
4
|
"description": "A web console for Claude Code, Codex, OpenCode, and other local CLI tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"skills/wand-decision",
|
|
17
17
|
"browser-extension",
|
|
18
18
|
"scripts/install-render-binary.js",
|
|
19
|
-
"scripts/stage-render-binaries.js"
|
|
19
|
+
"scripts/stage-render-binaries.js",
|
|
20
|
+
"scripts/install-speech-runtime.js",
|
|
21
|
+
"scripts/install-laya-runtime.js"
|
|
20
22
|
],
|
|
21
23
|
"preferGlobal": true,
|
|
22
24
|
"scripts": {
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
"build": "node scripts/check-node-version.js && rm -rf dist && node scripts/bundle-tailwind.js && node scripts/bundle-xterm.js && node scripts/bundle-qrcode.js && node scripts/bundle-browser.js && npm run generate:web-assets && tsc -p tsconfig.json && npm run build:copy-content && node scripts/check-bundle-budget.js && (npm run build:render-bin || echo '[wand] WARNING: build:render-bin failed; dist/native/ has no wand-render (run: npm run build:render-bin)' >&2) && node scripts/stamp-build-info.js && node scripts/fix-dist-permissions.js",
|
|
25
27
|
"build:copy-content": "cp -r src/web-ui/content dist/web-ui/ && cp src/decision-worker.py dist/decision-worker.py && node scripts/minify-dist-content.js",
|
|
26
28
|
"build:render-bin": "node scripts/stage-render-binaries.js --all",
|
|
29
|
+
"speech:install": "node scripts/install-speech-runtime.js",
|
|
30
|
+
"laya:install": "node scripts/install-laya-runtime.js",
|
|
27
31
|
"build:render-native": "cargo build --release --manifest-path render/Cargo.toml -p wand-renderd",
|
|
28
32
|
"build:structured-render-native": "cargo build --release --manifest-path render/Cargo.toml -p wand-structured-renderd",
|
|
29
33
|
"dev": "node scripts/check-node-version.js && node scripts/bundle-tailwind.js && node scripts/bundle-xterm.js && node scripts/bundle-qrcode.js && node scripts/bundle-browser.js --development && npm run generate:web-assets && tsx src/cli.ts web",
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
],
|
|
46
50
|
"license": "MIT",
|
|
47
51
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
52
|
+
"node": ">=24.21.0"
|
|
49
53
|
},
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"@anthropic-ai/sdk": "^0.81.0",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Explicit, isolated Apple Silicon runtime. Never installs into the user's/system Python.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { buildChildEnv, systemEnvValue } from "../dist/env-utils.js";
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
let root = path.join(os.homedir(), ".wand", "local-models", "laya"), environment, events = false;
|
|
12
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
13
|
+
const option = args[index];
|
|
14
|
+
if (option === "--events") events = true;
|
|
15
|
+
else if (option === "--dir" || option === "--environment") {
|
|
16
|
+
const value = args[++index];
|
|
17
|
+
if (!value || value.startsWith("--")) throw Error(`${option} requires a directory`);
|
|
18
|
+
if (option === "--dir") root = path.resolve(value); else environment = path.resolve(value);
|
|
19
|
+
} else if (option === "--help") {
|
|
20
|
+
console.log("node scripts/install-laya-runtime.js [--dir <managed model root>] [--environment <fresh venv directory>] [--events]"); process.exit(0);
|
|
21
|
+
} else throw Error(`Unknown option: ${option}`);
|
|
22
|
+
}
|
|
23
|
+
if (process.platform !== "darwin" || process.arch !== "arm64") throw Error("LAYA-MLX requires Apple Silicon macOS with Metal");
|
|
24
|
+
environment ??= path.join(root, "environments", `laya-0.3.0-mlx-0.32.2-${process.pid}`);
|
|
25
|
+
if (existsSync(environment)) throw Error("A fresh environment is required; existing Python environments are never overwritten");
|
|
26
|
+
const env = buildChildEnv(false, {
|
|
27
|
+
HTTPS_PROXY: systemEnvValue("HTTPS_PROXY"), HTTP_PROXY: systemEnvValue("HTTP_PROXY"), NO_PROXY: systemEnvValue("NO_PROXY"),
|
|
28
|
+
PIP_DISABLE_PIP_VERSION_CHECK: "1", PIP_NO_INPUT: "1", HF_HUB_DISABLE_TELEMETRY: "1", HF_HUB_DISABLE_IMPLICIT_TOKEN: "1",
|
|
29
|
+
});
|
|
30
|
+
function progress(phase, message) {
|
|
31
|
+
if (events) console.log("[wand-model] " + JSON.stringify({ phase, message })); else console.log(message);
|
|
32
|
+
}
|
|
33
|
+
function run(executable, arguments_, quiet = false) {
|
|
34
|
+
const result = spawnSync(executable, arguments_, { env, shell: false, stdio: quiet || events ? "pipe" : "inherit", encoding: "utf8", windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
|
|
35
|
+
if (result.error || result.status !== 0) throw Error("LAYA runtime setup failed; check Python >=3.11, network access to PyPI, free disk space and Metal support");
|
|
36
|
+
return result.stdout?.trim();
|
|
37
|
+
}
|
|
38
|
+
let completed = false;
|
|
39
|
+
try {
|
|
40
|
+
progress("runtime", "检查 Python 3.11+ 与 Apple Silicon 环境");
|
|
41
|
+
const explicit = systemEnvValue("WAND_LAYA_PYTHON_BIN");
|
|
42
|
+
if (explicit && !path.isAbsolute(explicit)) throw Error("WAND_LAYA_PYTHON_BIN must be an absolute trusted path");
|
|
43
|
+
const candidates = explicit ? [explicit] : ["python3.13", "python3.12", "python3.11", "python3"];
|
|
44
|
+
const python = candidates.find(candidate => {
|
|
45
|
+
const probe = spawnSync(candidate, ["--version"], { env, shell: false, encoding: "utf8", timeout: 10000 });
|
|
46
|
+
const version = /Python (\d+)\.(\d+)/.exec(probe.stdout || "");
|
|
47
|
+
return probe.status === 0 && version && Number(version[1]) === 3 && Number(version[2]) >= 11;
|
|
48
|
+
});
|
|
49
|
+
if (!python) throw Error("Python 3.11+ is required; install it or set trusted WAND_LAYA_PYTHON_BIN");
|
|
50
|
+
await mkdir(path.dirname(environment), { recursive: true, mode: 0o700 });
|
|
51
|
+
run(python, ["-m", "venv", environment]);
|
|
52
|
+
const executable = path.join(environment, "bin", "python");
|
|
53
|
+
progress("runtime", "在独立环境安装固定 LAYA 0.3.0 / MLX 0.32.2 依赖");
|
|
54
|
+
run(executable, ["-m", "pip", "--isolated", "install", "--index-url", "https://pypi.org/simple", "--only-binary=:all:",
|
|
55
|
+
"laya-mlx==0.3.0", "mlx==0.32.2", "huggingface-hub==1.33.0"]);
|
|
56
|
+
progress("verifying", "校验运行时与 Metal 能力");
|
|
57
|
+
run(executable, ["-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()"], true);
|
|
58
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
59
|
+
const receipt = path.join(root, "runtime.json"), partial = receipt + `.${process.pid}.part`;
|
|
60
|
+
const { rename } = await import("node:fs/promises");
|
|
61
|
+
await writeFile(partial, JSON.stringify({ pythonPath: executable, layaVersion: "0.3.0", mlxVersion: "0.32.2", platform: process.platform, arch: process.arch }) + "\n", { mode: 0o600 });
|
|
62
|
+
await rename(partial, receipt);
|
|
63
|
+
completed = true;
|
|
64
|
+
progress("completed", "LAYA 独立运行时已安装,未修改系统 Python、模型启用状态或技能授权");
|
|
65
|
+
} finally {
|
|
66
|
+
if (!completed) await rm(environment, { recursive: true, force: true }).catch(() => {});
|
|
67
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Explicit host-side installation; admin model setup may launch this fixed script, never arbitrary HTTP commands.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { cp, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { buildChildEnv } from "../dist/env-utils.js";
|
|
9
|
+
|
|
10
|
+
const SOURCE_COMMIT = "2eeeba56e9edd762b4b38467bab96c2517163158"; // whisper.cpp v1.8.3
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let backend = "auto";
|
|
13
|
+
let root = path.join(os.homedir(), ".wand", "speech");
|
|
14
|
+
let workDirectory;
|
|
15
|
+
let events = false;
|
|
16
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
17
|
+
if (args[i] === "--backend") backend = args[++i];
|
|
18
|
+
else if (args[i] === "--events") events = true;
|
|
19
|
+
else if (args[i] === "--work-dir") {
|
|
20
|
+
const directory = args[++i];
|
|
21
|
+
if (!directory || directory.startsWith("--")) throw new Error("--work-dir requires a directory");
|
|
22
|
+
workDirectory = path.resolve(directory);
|
|
23
|
+
}
|
|
24
|
+
else if (args[i] === "--dir") {
|
|
25
|
+
const directory = args[++i];
|
|
26
|
+
if (!directory || directory.startsWith("--")) throw new Error("--dir requires an installation directory");
|
|
27
|
+
root = path.resolve(directory);
|
|
28
|
+
}
|
|
29
|
+
else if (args[i] === "--help") {
|
|
30
|
+
console.log("node scripts/install-speech-runtime.js [--backend auto|cpu|metal|cuda] [--dir ~/.wand/speech]");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
} else throw new Error(`Unknown option: ${args[i]}`);
|
|
33
|
+
}
|
|
34
|
+
if (!["auto", "cpu", "metal", "cuda"].includes(backend)) throw new Error("Invalid backend");
|
|
35
|
+
if (!["darwin", "linux", "win32"].includes(process.platform)) throw new Error("Supported hosts: macOS, Linux, Windows");
|
|
36
|
+
if (backend === "auto") backend = process.platform === "darwin" ? "metal" : "cpu";
|
|
37
|
+
if (backend === "metal" && process.platform !== "darwin") throw new Error("Metal requires macOS");
|
|
38
|
+
|
|
39
|
+
function progress(phase, message) {
|
|
40
|
+
if (events) console.log("[wand-model] " + JSON.stringify({ phase, message })); else console.log(message);
|
|
41
|
+
}
|
|
42
|
+
function run(command, arguments_, cwd, quiet = false) {
|
|
43
|
+
const result = spawnSync(command, arguments_, { cwd, env: buildChildEnv(true), shell: false, stdio: quiet || events ? "pipe" : "inherit", encoding: "utf8", windowsHide: true });
|
|
44
|
+
if (result.error || result.status !== 0) throw new Error(`${command} failed: install Git, CMake and a C++ toolchain first (CUDA also needs its toolkit)`);
|
|
45
|
+
return result.stdout?.trim();
|
|
46
|
+
}
|
|
47
|
+
const work = workDirectory ?? await mkdtemp(path.join(os.tmpdir(), "wand-whisper-build-"));
|
|
48
|
+
if (workDirectory) await mkdir(workDirectory, { recursive: true, mode: 0o700 });
|
|
49
|
+
const staging = path.join(root, `bin-staging-${process.pid}`);
|
|
50
|
+
const bin = path.join(root, "bin");
|
|
51
|
+
const backup = path.join(root, `bin-previous-${process.pid}`);
|
|
52
|
+
try {
|
|
53
|
+
progress("runtime", "准备固定 whisper.cpp v1.8.3 源码");
|
|
54
|
+
run("git", ["init", work], work);
|
|
55
|
+
run("git", ["remote", "add", "origin", "https://github.com/ggml-org/whisper.cpp.git"], work);
|
|
56
|
+
run("git", ["fetch", "--depth", "1", "origin", SOURCE_COMMIT], work);
|
|
57
|
+
run("git", ["checkout", "--detach", "FETCH_HEAD"], work);
|
|
58
|
+
if (run("git", ["rev-parse", "HEAD"], work, true) !== SOURCE_COMMIT) throw new Error("Source revision mismatch");
|
|
59
|
+
progress("runtime", `配置 ${backend.toUpperCase()} 运行时与编译工具链`);
|
|
60
|
+
run("cmake", ["-S", work, "-B", path.join(work, "build"), "-DCMAKE_BUILD_TYPE=Release",
|
|
61
|
+
"-DBUILD_SHARED_LIBS=OFF", "-DWHISPER_BUILD_TESTS=OFF", "-DWHISPER_CURL=OFF", "-DGGML_NATIVE=OFF",
|
|
62
|
+
`-DGGML_METAL=${backend === "metal" ? "ON" : "OFF"}`, "-DGGML_METAL_EMBED_LIBRARY=ON",
|
|
63
|
+
`-DGGML_CUDA=${backend === "cuda" ? "ON" : "OFF"}`], work);
|
|
64
|
+
progress("runtime", "编译 whisper.cpp,仅安装可选语音组件");
|
|
65
|
+
run("cmake", ["--build", path.join(work, "build"), "--config", "Release", "--target", "whisper-cli", "-j", String(Math.min(8, os.availableParallelism()))], work);
|
|
66
|
+
const name = process.platform === "win32" ? "whisper-cli.exe" : "whisper-cli";
|
|
67
|
+
const candidates = [path.join(work, "build", "bin", "Release"), path.join(work, "build", "bin")];
|
|
68
|
+
const built = candidates.find((directory) => existsSync(path.join(directory, name)));
|
|
69
|
+
if (!built) throw new Error("whisper-cli was not produced");
|
|
70
|
+
progress("verifying", "校验语音运行时可执行文件");
|
|
71
|
+
run(path.join(built, name), ["--help"], work, true);
|
|
72
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
73
|
+
await cp(built, staging, { recursive: true });
|
|
74
|
+
if (existsSync(bin)) await rename(bin, backup);
|
|
75
|
+
try { await rename(staging, bin); } catch (error) { if (existsSync(backup)) await rename(backup, bin); throw error; }
|
|
76
|
+
await writeFile(path.join(root, "runtime.json"), JSON.stringify({ version: "1.8.3", sourceCommit: SOURCE_COMMIT, backend, platform: process.platform, arch: process.arch }, null, 2) + "\n", { mode: 0o600 });
|
|
77
|
+
await rm(backup, { recursive: true, force: true });
|
|
78
|
+
progress("completed", `whisper.cpp v1.8.3 (${backend}) 已安装;未改变模型启用状态`);
|
|
79
|
+
} finally {
|
|
80
|
+
await rm(work, { recursive: true, force: true });
|
|
81
|
+
await rm(staging, { recursive: true, force: true });
|
|
82
|
+
}
|