@co0ontty/wand 4.73.0 → 4.75.1-beta.gbf7dafd
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/build-info.json +4 -4
- package/dist/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +28 -0
- package/dist/message-truncator.js +3 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
- package/dist/native/darwin-arm64/wand-render.version +1 -1
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -1
- package/dist/native/darwin-x64/wand-render.version +1 -1
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -1
- package/dist/native/linux-arm64/wand-render.version +1 -1
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -1
- package/dist/native/linux-x64/wand-render.version +1 -1
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +2 -1
- package/dist/process-manager.js +15 -3
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-daemon-client.d.ts +33 -2
- package/dist/render-daemon-client.js +66 -16
- package/dist/render-host.d.ts +27 -3
- package/dist/render-host.js +156 -17
- package/dist/server-session-routes.js +42 -3
- package/dist/server-task-routes.js +3 -3
- package/dist/server.js +12 -4
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/task-types.d.ts +2 -1
- package/dist/terminal-daemon-server.js +27 -4
- package/dist/tui/commands.js +183 -10
- package/dist/types.d.ts +11 -9
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +78 -72
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +9 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.js +11 -4
- package/package.json +1 -1
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
const NATIVE_THINKING_EFFORT = /^(claude|codex|opencode|grok|qoder|pi):[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
2
|
+
/** 判断任意值是否为合法的 thinking effort(旧四档,或 `provider:level`)。 */
|
|
2
3
|
export function isThinkingEffort(value) {
|
|
3
4
|
return value === "off"
|
|
4
5
|
|| value === "standard"
|
|
5
6
|
|| value === "deep"
|
|
6
7
|
|| value === "max"
|
|
7
|
-
|| (typeof value === "string" &&
|
|
8
|
+
|| (typeof value === "string" && NATIVE_THINKING_EFFORT.test(value));
|
|
9
|
+
}
|
|
10
|
+
/** 只取本 provider 的原生档位。别的 CLI 的前缀不能串过去。 */
|
|
11
|
+
export function prefixedThinkingEffort(provider, effort) {
|
|
12
|
+
if (typeof effort !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const separator = effort.indexOf(":");
|
|
15
|
+
if (separator <= 0)
|
|
16
|
+
return null;
|
|
17
|
+
if (effort.slice(0, separator) !== provider)
|
|
18
|
+
return null;
|
|
19
|
+
const level = effort.slice(separator + 1);
|
|
20
|
+
return /^[a-z0-9][a-z0-9_-]{0,31}$/.test(level) ? level : null;
|
|
8
21
|
}
|
|
9
22
|
export function isStructuredRunnerForProvider(provider, runner) {
|
|
10
23
|
if (provider === "claude")
|
|
@@ -48,20 +61,24 @@ export function normalizeThinkingEffort(value) {
|
|
|
48
61
|
const normalized = value.trim().toLowerCase();
|
|
49
62
|
if (normalized === "off" || normalized === "standard" || normalized === "deep" || normalized === "max")
|
|
50
63
|
return normalized;
|
|
51
|
-
if (
|
|
64
|
+
if (NATIVE_THINKING_EFFORT.test(normalized))
|
|
52
65
|
return normalized;
|
|
53
66
|
return null;
|
|
54
67
|
}
|
|
55
68
|
export function thinkingEffortToSdkBudget(effort) {
|
|
56
|
-
|
|
69
|
+
const native = prefixedThinkingEffort("claude", effort) ?? effort;
|
|
70
|
+
if (native === "standard" || native === "low")
|
|
57
71
|
return 4096;
|
|
58
|
-
if (
|
|
72
|
+
if (native === "deep" || native === "medium" || native === "high")
|
|
59
73
|
return 16000;
|
|
60
|
-
if (
|
|
74
|
+
if (native === "max" || native === "xhigh" || native === "ultra")
|
|
61
75
|
return 31999;
|
|
62
76
|
return 0;
|
|
63
77
|
}
|
|
64
78
|
export function thinkingEffortToClaudeCliEffort(effort) {
|
|
79
|
+
const native = prefixedThinkingEffort("claude", effort);
|
|
80
|
+
if (native)
|
|
81
|
+
return native;
|
|
65
82
|
if (effort === "standard")
|
|
66
83
|
return "low";
|
|
67
84
|
if (effort === "deep")
|
|
@@ -74,8 +91,9 @@ export function thinkingEffortToClaudeSlashEffort(effort) {
|
|
|
74
91
|
return thinkingEffortToClaudeCliEffort(effort) ?? "auto";
|
|
75
92
|
}
|
|
76
93
|
export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
const native = prefixedThinkingEffort("codex", effort);
|
|
95
|
+
if (native)
|
|
96
|
+
return native;
|
|
79
97
|
if (effort === "standard")
|
|
80
98
|
return "low";
|
|
81
99
|
if (effort === "deep")
|
|
@@ -84,11 +102,36 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
|
84
102
|
return "xhigh";
|
|
85
103
|
return null;
|
|
86
104
|
}
|
|
105
|
+
function thinkingEffortToNamedLevels(provider, effort, legacy) {
|
|
106
|
+
const native = prefixedThinkingEffort(provider, effort);
|
|
107
|
+
if (native)
|
|
108
|
+
return native;
|
|
109
|
+
if (!effort || effort === "off")
|
|
110
|
+
return null;
|
|
111
|
+
if (effort === "standard")
|
|
112
|
+
return legacy.standard;
|
|
113
|
+
if (effort === "deep")
|
|
114
|
+
return legacy.deep;
|
|
115
|
+
if (effort === "max")
|
|
116
|
+
return legacy.max;
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/** OpenCode `--variant`。旧四档仍是 low/high/max;`opencode:<level>` 原样传递。 */
|
|
120
|
+
export function thinkingEffortToOpenCodeVariant(effort) {
|
|
121
|
+
return thinkingEffortToNamedLevels("opencode", effort, { standard: "low", deep: "high", max: "max" });
|
|
122
|
+
}
|
|
123
|
+
/** Qoder `--reasoning-effort`。旧四档仍是 low/high/max;`qoder:<level>` 原样传递。 */
|
|
124
|
+
export function thinkingEffortToQoderEffort(effort) {
|
|
125
|
+
return thinkingEffortToNamedLevels("qoder", effort, { standard: "low", deep: "high", max: "max" });
|
|
126
|
+
}
|
|
87
127
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
128
|
+
* Grok `--effort`。当前 CLI 接受 xhigh/high/medium/low,不接受 `max`,
|
|
129
|
+
* 所以旧的「最大」和误存的 `grok:max` 都落到 xhigh。
|
|
90
130
|
*/
|
|
91
|
-
function
|
|
131
|
+
export function thinkingEffortToGrokEffort(effort) {
|
|
132
|
+
const native = prefixedThinkingEffort("grok", effort);
|
|
133
|
+
if (native)
|
|
134
|
+
return native === "max" ? "xhigh" : native;
|
|
92
135
|
if (!effort || effort === "off")
|
|
93
136
|
return null;
|
|
94
137
|
if (effort === "standard")
|
|
@@ -96,21 +139,20 @@ function thinkingEffortToLowHighMax(effort) {
|
|
|
96
139
|
if (effort === "deep")
|
|
97
140
|
return "high";
|
|
98
141
|
if (effort === "max")
|
|
99
|
-
return "
|
|
100
|
-
return
|
|
142
|
+
return "xhigh";
|
|
143
|
+
return null;
|
|
101
144
|
}
|
|
102
|
-
export const thinkingEffortToOpenCodeVariant = thinkingEffortToLowHighMax;
|
|
103
|
-
export const thinkingEffortToGrokEffort = thinkingEffortToLowHighMax;
|
|
104
|
-
export const thinkingEffortToQoderEffort = thinkingEffortToLowHighMax;
|
|
105
145
|
export function thinkingEffortToPiLevel(effort) {
|
|
146
|
+
const native = prefixedThinkingEffort("pi", effort);
|
|
147
|
+
if (native)
|
|
148
|
+
return native;
|
|
106
149
|
if (!effort || effort === "off")
|
|
107
150
|
return "off";
|
|
108
151
|
if (effort === "standard")
|
|
109
152
|
return "low";
|
|
110
153
|
if (effort === "deep")
|
|
111
154
|
return "high";
|
|
112
|
-
// Pi uses `max`; `xhigh` is a Codex-only level and makes Pi reject the run.
|
|
113
155
|
if (effort === "max")
|
|
114
156
|
return "max";
|
|
115
|
-
return
|
|
157
|
+
return null;
|
|
116
158
|
}
|
package/dist/task-types.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export declare const DEFAULT_WAND_TASK_PRIORITY: WandTaskPriority;
|
|
|
7
7
|
/** 任务派发时选择的 CLI 工具;空串表示尚未指定。 */
|
|
8
8
|
export type WandTaskAgentProvider = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
|
|
9
9
|
export type WandTaskAgentModel = string;
|
|
10
|
-
|
|
10
|
+
/** 与会话思考档位同一套:旧四档,或 `provider:level` 原生档。 */
|
|
11
|
+
export type WandTaskAgentEffort = "off" | "standard" | "deep" | "max" | `${WandTaskAgentProvider}:${string}`;
|
|
11
12
|
/**
|
|
12
13
|
* 派发时的执行模式,只暴露三种:
|
|
13
14
|
* - managed:托管,自动确认全部权限并使用完全自主提示词;
|
|
@@ -493,14 +493,28 @@ export async function runTerminalDaemon(configPath) {
|
|
|
493
493
|
for (const client of clients)
|
|
494
494
|
client.socket.destroy();
|
|
495
495
|
server.close(() => process.exit(0));
|
|
496
|
-
cleanupDaemonFiles(paths
|
|
496
|
+
cleanupDaemonFiles(paths, { token, pid: process.pid });
|
|
497
497
|
};
|
|
498
498
|
process.once("SIGINT", shutdown);
|
|
499
499
|
process.once("SIGTERM", shutdown);
|
|
500
500
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
501
|
+
/**
|
|
502
|
+
* 只清理**自己拥有**的 daemon 文件。
|
|
503
|
+
*
|
|
504
|
+
* 升级期/僵尸期可能有两个 daemon 指向同一个 config(老进程端点已被清理、新进程刚接上):
|
|
505
|
+
* 老进程退出时无条件 unlink,会把新进程正在使用的 token/pid 一起删掉 —— 新 daemon 随即变成
|
|
506
|
+
* 「进程活着但凭据不见了」,所有终端永久断联。所以凭据按**内容**判归属,不是自己写的就不动。
|
|
507
|
+
* socket 仍然照删:它属于本进程的监听(真正被占时 bind 会 EADDRINUSE)。
|
|
508
|
+
*/
|
|
509
|
+
function cleanupDaemonFiles(paths, owner) {
|
|
510
|
+
if (process.platform !== "win32") {
|
|
511
|
+
try {
|
|
512
|
+
unlinkSync(paths.socketPath);
|
|
513
|
+
}
|
|
514
|
+
catch { /* already absent */ }
|
|
515
|
+
}
|
|
516
|
+
for (const file of [paths.tokenPath, paths.pidPath]) {
|
|
517
|
+
if (!fileHoldsValue(file, file === paths.tokenPath ? owner.token : String(owner.pid)))
|
|
504
518
|
continue;
|
|
505
519
|
try {
|
|
506
520
|
unlinkSync(file);
|
|
@@ -508,6 +522,15 @@ function cleanupDaemonFiles(socketPath, tokenPath, pidPath) {
|
|
|
508
522
|
catch { /* already absent */ }
|
|
509
523
|
}
|
|
510
524
|
}
|
|
525
|
+
/** 文件内容是否还是本进程写进去的值(读不到/已被别人改写都算「不是自己的」)。 */
|
|
526
|
+
function fileHoldsValue(filePath, expected) {
|
|
527
|
+
try {
|
|
528
|
+
return readFileSync(filePath, "utf8").trim() === expected;
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
511
534
|
export function readTerminalDaemonPid(configPath) {
|
|
512
535
|
try {
|
|
513
536
|
const value = Number(readFileSync(terminalDaemonPaths(configPath).pidPath, "utf8").trim());
|
package/dist/tui/commands.js
CHANGED
|
@@ -15,6 +15,10 @@ import { computeRelaunch } from "../relaunch.js";
|
|
|
15
15
|
import { ensureDatabaseFile, resolveDatabasePath, WandStorage } from "../storage.js";
|
|
16
16
|
import { getErrorMessage } from "../error-utils.js";
|
|
17
17
|
import { readTerminalDaemonPid } from "../terminal-daemon-server.js";
|
|
18
|
+
import { terminalDaemonPaths } from "../terminal-daemon-protocol.js";
|
|
19
|
+
import { renderPaths } from "../render-protocol.js";
|
|
20
|
+
import { resolveRenderBinaryPath } from "../render-binary.js";
|
|
21
|
+
import { compareSemver } from "../version-utils.js";
|
|
18
22
|
import { isPidAlive } from "../pidfile.js";
|
|
19
23
|
// ─── 重启 ────────────────────────────────────────────────────────────────
|
|
20
24
|
/**
|
|
@@ -232,13 +236,49 @@ export function serviceLogs(lines = 80, opts) {
|
|
|
232
236
|
return { ok: false, message: "journalctl 调用失败", detail: r.stderr.trim() || `exit ${r.status}` };
|
|
233
237
|
}
|
|
234
238
|
if (process.platform === "darwin") {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
// plist 里配了 StandardOutPath/StandardErrorPath(见 installLaunchdService)。
|
|
240
|
+
// 老安装没有这两项,退回到 Console.app 的提示。
|
|
241
|
+
const configDir = readLaunchdConfigDir(scope);
|
|
242
|
+
if (!configDir) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
message: "launchd 不直接写日志,请用 Console.app 或在 plist 里配置 StandardOutPath",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const sections = ["web.log", "web-error.log"]
|
|
249
|
+
.map((name) => ({ name, file: path.join(configDir, name) }))
|
|
250
|
+
.filter(({ file }) => existsSync(file))
|
|
251
|
+
.map(({ name, file }) => `--- ${name} ---\n${tailLines(file, lines)}`);
|
|
252
|
+
if (sections.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
message: `还没有日志文件(${configDir}/web.log、web-error.log)`,
|
|
256
|
+
detail: "重新跑一次 `wand service:install` 让 plist 带上 StandardOutPath/StandardErrorPath",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return { ok: true, message: `读取 ${configDir} 下的服务日志`, detail: sections.join("\n") };
|
|
239
260
|
}
|
|
240
261
|
return unsupported();
|
|
241
262
|
}
|
|
263
|
+
/** 从已安装的 plist 里读 config 路径(ProgramArguments 的第 5 项);读不到返回 null。 */
|
|
264
|
+
function readLaunchdConfigDir(scope) {
|
|
265
|
+
const plistPath = servicePathFor(scope);
|
|
266
|
+
if (!existsSync(plistPath))
|
|
267
|
+
return null;
|
|
268
|
+
const result = spawnSync("plutil", ["-extract", "ProgramArguments.4", "raw", "-o", "-", plistPath], { encoding: "utf8", timeout: 5_000 });
|
|
269
|
+
const configPath = result.status === 0 ? (result.stdout || "").trim() : "";
|
|
270
|
+
return configPath ? path.dirname(path.resolve(configPath)) : null;
|
|
271
|
+
}
|
|
272
|
+
/** 文件末尾 N 行(整个文件读进内存:服务日志本来就是小文件)。 */
|
|
273
|
+
function tailLines(filePath, lines) {
|
|
274
|
+
try {
|
|
275
|
+
const all = readFileSync(filePath, "utf8").split("\n");
|
|
276
|
+
return all.slice(Math.max(0, all.length - lines)).join("\n").trim();
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return `读取失败:${getErrorMessage(error)}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
242
282
|
/** systemctl 调用根据 scope 决定要不要 --user。system scope 也意味着调用方需要 root。 */
|
|
243
283
|
function systemctlBaseArgs(scope) {
|
|
244
284
|
return scope === "user" ? ["--user"] : [];
|
|
@@ -607,6 +647,118 @@ function hasLiveTerminalDaemon(configPath) {
|
|
|
607
647
|
const pid = readTerminalDaemonPid(configPath);
|
|
608
648
|
return pid !== null && isPidAlive(pid);
|
|
609
649
|
}
|
|
650
|
+
/** 按 config 派生两套 daemon 的端点(socket 在 /tmp、pid 在 config 目录旁边)。 */
|
|
651
|
+
function daemonEndpointStates(configPath) {
|
|
652
|
+
const render = renderPaths(configPath);
|
|
653
|
+
const terminal = terminalDaemonPaths(configPath);
|
|
654
|
+
return [
|
|
655
|
+
{ label: "Render", pidPath: render.pidPath, socketPath: render.socketPath },
|
|
656
|
+
{ label: "terminald", pidPath: terminal.pidPath, socketPath: terminal.socketPath },
|
|
657
|
+
].map(({ label, pidPath, socketPath }) => {
|
|
658
|
+
const pid = readDaemonPidFile(pidPath);
|
|
659
|
+
return {
|
|
660
|
+
label,
|
|
661
|
+
pid,
|
|
662
|
+
pidAlive: pid !== null && isPidAlive(pid),
|
|
663
|
+
socketPath,
|
|
664
|
+
socketExists: existsSync(socketPath),
|
|
665
|
+
};
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
function readDaemonPidFile(pidPath) {
|
|
669
|
+
try {
|
|
670
|
+
const pid = Number(readFileSync(pidPath, "utf8").trim());
|
|
671
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 清掉「进程活着但端点没了」的僵尸 daemon。
|
|
679
|
+
*
|
|
680
|
+
* 这种 daemon 对任何客户端都已不可达(`/tmp` 下的 socket 文件被临时目录清理器删掉,
|
|
681
|
+
* 它抱着一个已经无名的 inode),却会让 `wand web` 启动判定「pid 活着但 socket 不可用」
|
|
682
|
+
* 而拒绝启动 —— 表现是整个服务起不回来,只能人工 kill。
|
|
683
|
+
*
|
|
684
|
+
* 自愈版 daemon(Node/Rust 两侧都有 recovery)会在 1s 内 rebind,所以先等一会儿
|
|
685
|
+
* 复查,真的还是不可达才动手;杀掉后由服务(terminald 有 unit/plist,Render 由 Server
|
|
686
|
+
* 启动时拉起)重新起一份新进程。
|
|
687
|
+
*/
|
|
688
|
+
function reapZombieDaemons(configPath) {
|
|
689
|
+
const actions = [];
|
|
690
|
+
const suspects = daemonEndpointStates(configPath).filter((state) => state.pidAlive && !state.socketExists);
|
|
691
|
+
if (suspects.length === 0)
|
|
692
|
+
return actions;
|
|
693
|
+
// 给自愈看门狗一点时间:端点丢得比这里早得多的话,下一轮就直接判死。
|
|
694
|
+
sleepSync(1_500);
|
|
695
|
+
for (const state of daemonEndpointStates(configPath)) {
|
|
696
|
+
if (!state.pidAlive || state.socketExists)
|
|
697
|
+
continue;
|
|
698
|
+
try {
|
|
699
|
+
process.kill(state.pid, "SIGKILL");
|
|
700
|
+
actions.push(`${state.label} daemon pid ${state.pid} 进程活着但端点已丢失(${state.socketPath}),已清理`);
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
actions.push(`${state.label} daemon pid ${state.pid} 清理失败:${getErrorMessage(error)}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return actions;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* 「正在跑的 daemon 比装好的二进制旧」的提示(没有这种情形时返回 null)。
|
|
710
|
+
*
|
|
711
|
+
* npm 升级 / service:install 都不重启 daemon(那里可能挂着用户的 shell),所以升级完
|
|
712
|
+
* 很容易一直在跑旧代码 —— 2026-09-24 的终端全断就是这么来的(9/23 补的端点自愈
|
|
713
|
+
* 在 9/19 启的 terminald 里根本不存在)。service:install 的输出不带 --verbose 时
|
|
714
|
+
* 看不到 detail,所以这句拼进 message 里。
|
|
715
|
+
*/
|
|
716
|
+
function staleDaemonNote(configPath) {
|
|
717
|
+
const render = renderPaths(configPath);
|
|
718
|
+
const pid = readDaemonPidFile(render.pidPath);
|
|
719
|
+
if (pid === null || !isPidAlive(pid))
|
|
720
|
+
return null;
|
|
721
|
+
const daemonVersion = (() => {
|
|
722
|
+
try {
|
|
723
|
+
const meta = JSON.parse(readFileSync(render.metaPath, "utf8"));
|
|
724
|
+
return typeof meta.version === "string" && meta.version ? meta.version : null;
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
})();
|
|
730
|
+
const binaryPath = resolveRenderBinaryPath(configPath);
|
|
731
|
+
if (!binaryPath)
|
|
732
|
+
return null;
|
|
733
|
+
const binaryVersion = (() => {
|
|
734
|
+
try {
|
|
735
|
+
const sidecar = readFileSync(`${binaryPath}.version`, "utf8").trim();
|
|
736
|
+
if (/^\d+\.\d+\.\d+/.test(sidecar))
|
|
737
|
+
return sidecar;
|
|
738
|
+
}
|
|
739
|
+
catch { /* 没有 sidecar 就跑一次 --version */ }
|
|
740
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf8", timeout: 5_000 });
|
|
741
|
+
const match = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?)/.exec(result.stdout ?? "");
|
|
742
|
+
return match?.[1] ?? null;
|
|
743
|
+
})();
|
|
744
|
+
if (!binaryVersion)
|
|
745
|
+
return null;
|
|
746
|
+
if (daemonVersion && compareSemver(daemonVersion, binaryVersion) >= 0)
|
|
747
|
+
return null;
|
|
748
|
+
const shown = daemonVersion ? `Render ${daemonVersion}` : "Render daemon";
|
|
749
|
+
return `;但 ${shown} 仍在跑(装好的二进制是 ${binaryVersion}),它要重启才会用上新代码:./start.sh --restart-daemons`;
|
|
750
|
+
}
|
|
751
|
+
/** daemon 端点状态摘要,给服务命令的 detail 用。 */
|
|
752
|
+
function describeDaemonEndpoints(configPath) {
|
|
753
|
+
return daemonEndpointStates(configPath)
|
|
754
|
+
.map((state) => {
|
|
755
|
+
const pid = state.pid ?? "-";
|
|
756
|
+
const alive = state.pidAlive ? "alive" : "-";
|
|
757
|
+
const socket = state.socketExists ? "endpoint ok" : "endpoint MISSING";
|
|
758
|
+
return ` ${state.label}: pid ${pid} (${alive}), ${socket} ${state.socketPath}`;
|
|
759
|
+
})
|
|
760
|
+
.join("\n");
|
|
761
|
+
}
|
|
610
762
|
/** 读取已安装服务 unit / plist 的入口命令;未安装或平台不支持时返回 null。 */
|
|
611
763
|
export function readServiceEntrypoint(scope) {
|
|
612
764
|
const servicePath = servicePathFor(scope);
|
|
@@ -762,6 +914,9 @@ function readFileStat(targetPath) {
|
|
|
762
914
|
function installSystemdService(ctx, scope) {
|
|
763
915
|
const unitPath = servicePathFor(scope);
|
|
764
916
|
const terminalUnitPath = terminalServicePathFor(scope);
|
|
917
|
+
// 先清掉会阻塞启动的僵尸 daemon(pid 活着但端点没了),否则下一步 `wand web` 会
|
|
918
|
+
// 因为「活着但 socket 不可用」直接拒绝启动。
|
|
919
|
+
const daemonNotes = reapZombieDaemons(ctx.configPath);
|
|
765
920
|
const wandBin = resolveWandBin(ctx);
|
|
766
921
|
const nodeBin = process.execPath;
|
|
767
922
|
const nodeBinDir = path.dirname(nodeBin);
|
|
@@ -868,6 +1023,9 @@ function installSystemdService(ctx, scope) {
|
|
|
868
1023
|
`scope: ${scope}`,
|
|
869
1024
|
`unit: ${unitPath}`,
|
|
870
1025
|
`terminal unit: ${terminalUnitPath}`,
|
|
1026
|
+
...daemonNotes,
|
|
1027
|
+
"daemons:",
|
|
1028
|
+
describeDaemonEndpoints(ctx.configPath),
|
|
871
1029
|
`daemon-reload: ${reload.status === 0 ? "ok" : `failed (${reload.stderr.trim()})`}`,
|
|
872
1030
|
`terminal enable${terminalAlreadyLive ? " (kept existing daemon)" : " --now"}: ${enableTerminal.status === 0 ? "ok" : `failed (${enableTerminal.stderr.trim()})`}`,
|
|
873
1031
|
`enable --now: ${enable.status === 0 ? "ok" : `failed (${enable.stderr.trim()})`}`,
|
|
@@ -883,7 +1041,9 @@ function installSystemdService(ctx, scope) {
|
|
|
883
1041
|
}
|
|
884
1042
|
return {
|
|
885
1043
|
ok: true,
|
|
886
|
-
message: `已注册 systemd ${scope === "user" ? "用户" : "系统"}服务: ${unitPath}
|
|
1044
|
+
message: `已注册 systemd ${scope === "user" ? "用户" : "系统"}服务: ${unitPath}` +
|
|
1045
|
+
(daemonNotes.length > 0 ? `(已清理 ${daemonNotes.length} 个僵尸 daemon)` : "") +
|
|
1046
|
+
(staleDaemonNote(ctx.configPath) ?? ""),
|
|
887
1047
|
detail,
|
|
888
1048
|
};
|
|
889
1049
|
}
|
|
@@ -917,6 +1077,8 @@ function uninstallSystemdService(scope) {
|
|
|
917
1077
|
function installLaunchdService(ctx, scope) {
|
|
918
1078
|
const plistPath = servicePathFor(scope);
|
|
919
1079
|
const terminalPlistPath = terminalServicePathFor(scope);
|
|
1080
|
+
// 同 systemd:先例行清理僵尸 daemon,否则新起的 web 会因为端点缺失而拒绝启动。
|
|
1081
|
+
const daemonNotes = reapZombieDaemons(ctx.configPath);
|
|
920
1082
|
const wandBin = resolveWandBin(ctx);
|
|
921
1083
|
const nodeBin = process.execPath;
|
|
922
1084
|
const nodeBinDir = path.dirname(nodeBin);
|
|
@@ -925,6 +1087,12 @@ function installLaunchdService(ctx, scope) {
|
|
|
925
1087
|
const runHome = scope === "system" ? homeForUser(runUser, fallbackHome) : fallbackHome;
|
|
926
1088
|
// 与 systemd 同理:launchd 默认 PATH 极简,spawn 出的 claude 会找不到。
|
|
927
1089
|
const servicePath = buildServicePath(nodeBinDir, runHome);
|
|
1090
|
+
// launchd 默认把 stdout/stderr 丢进 /dev/null:线上出问题时连“daemon 活着但端点没了”
|
|
1091
|
+
// 这种一行告警都看不到(只能靠 lsof / 系统日志反推)。写进 config 目录旁边,
|
|
1092
|
+
// `wand service:logs` 直接读。
|
|
1093
|
+
const configDir = path.dirname(path.resolve(ctx.configPath));
|
|
1094
|
+
const logFields = (out, err) => ` <key>StandardOutPath</key><string>${path.join(configDir, out)}</string>\n` +
|
|
1095
|
+
` <key>StandardErrorPath</key><string>${path.join(configDir, err)}</string>\n`;
|
|
928
1096
|
const userNameField = scope === "system"
|
|
929
1097
|
? ` <key>UserName</key><string>${runUser}</string>\n`
|
|
930
1098
|
: "";
|
|
@@ -949,7 +1117,7 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
949
1117
|
<key>HOME</key><string>${runHome}</string>
|
|
950
1118
|
</dict>
|
|
951
1119
|
<key>RunAtLoad</key><true/>
|
|
952
|
-
<key>KeepAlive</key><true/>
|
|
1120
|
+
${logFields("web.log", "web-error.log")} <key>KeepAlive</key><true/>
|
|
953
1121
|
</dict>
|
|
954
1122
|
</plist>
|
|
955
1123
|
`;
|
|
@@ -973,12 +1141,13 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
973
1141
|
<key>HOME</key><string>${runHome}</string>
|
|
974
1142
|
</dict>
|
|
975
1143
|
<key>RunAtLoad</key><true/>
|
|
976
|
-
<key>KeepAlive</key><true/>
|
|
1144
|
+
${logFields("terminald.log", "terminald-error.log")} <key>KeepAlive</key><true/>
|
|
977
1145
|
</dict>
|
|
978
1146
|
</plist>
|
|
979
1147
|
`;
|
|
980
1148
|
try {
|
|
981
1149
|
mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
1150
|
+
mkdirSync(configDir, { recursive: true });
|
|
982
1151
|
writeFileSync(terminalPlistPath, terminalPlist, "utf8");
|
|
983
1152
|
writeFileSync(plistPath, plist, "utf8");
|
|
984
1153
|
}
|
|
@@ -1023,8 +1192,12 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
1023
1192
|
}
|
|
1024
1193
|
return {
|
|
1025
1194
|
ok: true,
|
|
1026
|
-
message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}
|
|
1027
|
-
|
|
1195
|
+
message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}` +
|
|
1196
|
+
(daemonNotes.length > 0 ? `(已清理 ${daemonNotes.length} 个僵尸 daemon)` : "") +
|
|
1197
|
+
(staleDaemonNote(ctx.configPath) ?? ""),
|
|
1198
|
+
detail: [terminalDetail, started.detail, ...daemonNotes, "daemons:", describeDaemonEndpoints(ctx.configPath)]
|
|
1199
|
+
.filter(Boolean)
|
|
1200
|
+
.join("\n"),
|
|
1028
1201
|
};
|
|
1029
1202
|
}
|
|
1030
1203
|
function uninstallLaunchdService(scope) {
|
|
@@ -1057,4 +1230,4 @@ function uninstallLaunchdService(scope) {
|
|
|
1057
1230
|
};
|
|
1058
1231
|
}
|
|
1059
1232
|
// ─── 工具 ────────────────────────────────────────────────────────────────
|
|
1060
|
-
// compareSemver 已统一到 ../version-utils.ts
|
|
1233
|
+
// compareSemver 已统一到 ../version-utils.ts(上方已按需 import)
|
package/dist/types.d.ts
CHANGED
|
@@ -161,8 +161,12 @@ export interface WandConfig {
|
|
|
161
161
|
commitModel?: string;
|
|
162
162
|
/** 快捷提交生成 commit message / tag 时使用 CLI 或直连 API。 */
|
|
163
163
|
commitAiSource?: CommitAiSource;
|
|
164
|
-
/** Wand 自身轻量 AI 功能与 Commit 直连模式复用的 API 配置。 */
|
|
164
|
+
/** Wand 自身轻量 AI 功能与 Commit 直连模式复用的 API 配置。enabled 表示系统 AI 优先使用 API。 */
|
|
165
165
|
systemAi?: SystemAiConfig;
|
|
166
|
+
/** 系统 AI 使用 CLI 模式时的专用工具;未配置时沿用当前会话工具(兼容旧设置)。 */
|
|
167
|
+
systemAiCli?: SessionProvider;
|
|
168
|
+
/** 系统 AI 专用 CLI 模型;留空跟随该工具的新会话默认模型。 */
|
|
169
|
+
systemAiModel?: string;
|
|
166
170
|
/** 新建会话时默认使用的思考深度。 */
|
|
167
171
|
defaultThinkingEffort?: ThinkingEffort;
|
|
168
172
|
/** 结构化会话使用的 runner: "cli"(默认,spawn claude -p)或 "sdk"(@anthropic-ai/claude-agent-sdk)。 */
|
|
@@ -222,10 +226,10 @@ interface ReasoningEffortInfo {
|
|
|
222
226
|
description?: string;
|
|
223
227
|
}
|
|
224
228
|
/**
|
|
225
|
-
* 旧的四档值需要继续兼容已有会话。
|
|
226
|
-
* 避免 `max
|
|
229
|
+
* 旧的四档值需要继续兼容已有会话。CLI 动态档位加 provider 前缀,
|
|
230
|
+
* 避免 `max`(旧值,各家含义不同)和 CLI 新增的原生档位冲突。
|
|
227
231
|
*/
|
|
228
|
-
export type ThinkingEffort = "off" | "standard" | "deep" | "max" |
|
|
232
|
+
export type ThinkingEffort = "off" | "standard" | "deep" | "max" | `${"claude" | "codex" | "opencode" | "grok" | "qoder" | "pi"}:${string}`;
|
|
229
233
|
export interface WorktreeInfo {
|
|
230
234
|
branch: string;
|
|
231
235
|
path: string;
|
|
@@ -625,11 +629,9 @@ export interface SessionSnapshot {
|
|
|
625
629
|
selectedModel?: string | null;
|
|
626
630
|
/**
|
|
627
631
|
* 用户选定的思考深度。
|
|
628
|
-
* - off:
|
|
629
|
-
* - standard
|
|
630
|
-
* -
|
|
631
|
-
* - max: 旧版最深档(SDK: budget 31999;Claude CLI: max;Codex: xhigh)
|
|
632
|
-
* - codex:*: Codex CLI 动态声明的原生推理档位
|
|
632
|
+
* - off: 不覆盖默认思考深度
|
|
633
|
+
* - standard / deep / max: 旧四档,按各 CLI 当时的低、中、高档换算
|
|
634
|
+
* - provider:level: 该 CLI 自己报出来的原生档位,原样传给对应 flag
|
|
633
635
|
*/
|
|
634
636
|
thinkingEffort?: ThinkingEffort | null;
|
|
635
637
|
/** 当前 PTY 列宽,由最近一次 resize 决定。前端用它来判断本端 fit 是否需要校准。 */
|
package/dist/wand-task-sync.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { getErrorMessage } from "./error-utils.js";
|
|
3
3
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
4
4
|
import { isSessionProvider } from "./session-provider.js";
|
|
5
|
+
import { isThinkingEffort } from "./structured-provider-common.js";
|
|
5
6
|
import { isClosedWandTaskStatus, normalizeWandTaskAgentMode } from "./task-types.js";
|
|
6
7
|
import { firstLayoutTabId } from "./layout-tree.js";
|
|
7
8
|
/** 未命名任务的占位名;服务端新建时写入,自动命名成功后被真实标题覆盖。 */
|
|
@@ -97,7 +98,7 @@ function agentFromSession(session) {
|
|
|
97
98
|
return {
|
|
98
99
|
provider: session.provider,
|
|
99
100
|
model: session.selectedModel || session.structuredState?.model || "default",
|
|
100
|
-
thinkingEffort: effort
|
|
101
|
+
thinkingEffort: isThinkingEffort(effort) ? effort : "off",
|
|
101
102
|
mode: normalizeWandTaskAgentMode(session.provider, session.mode),
|
|
102
103
|
// 会话形态按会话自身的 sessionKind 还原,PTY 会话不会在「再指派」时被误当结构化。
|
|
103
104
|
kind: (session.sessionKind ?? "pty") === "structured" ? "structured" : "pty",
|