@co0ontty/wand 1.41.4 → 1.43.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/dist/build-info.json +3 -3
- package/dist/server.js +40 -18
- package/dist/update-helper.d.ts +18 -0
- package/dist/update-helper.js +238 -0
- package/dist/web-ui/content/scripts.js +331 -446
- package/dist/web-ui/content/styles.css +202 -180
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-05-
|
|
4
|
-
"version": "1.
|
|
2
|
+
"commit": "a2fcf61e1ef6825cb1767b63fb9593e8f6e4ecea",
|
|
3
|
+
"builtAt": "2026-05-31T02:16:53.049Z",
|
|
4
|
+
"version": "1.43.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { installPackageGloballyAsync, resolveGlobalWandCli } from "./npm-update-
|
|
|
25
25
|
import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
|
|
26
26
|
import { computeRelaunch } from "./relaunch.js";
|
|
27
27
|
import { isServiceInstalled } from "./tui/commands.js";
|
|
28
|
+
import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
|
|
28
29
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
29
30
|
import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
|
|
30
31
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
@@ -1569,31 +1570,40 @@ export async function startServer(config, configPath) {
|
|
|
1569
1570
|
installSpec = `${PKG_NAME}@latest`;
|
|
1570
1571
|
targetLabel = onBetaBuild ? "最新正式版" : latest;
|
|
1571
1572
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
await installPackageGloballyAsync(installSpec, 300000, (line) => {
|
|
1576
|
-
logLines.push(line);
|
|
1577
|
-
process.stdout.write(`${line}\n`);
|
|
1578
|
-
});
|
|
1573
|
+
if (!canUseDetachedUpdateHelper()) {
|
|
1574
|
+
res.status(500).json({ error: "当前平台暂不支持 Web 异步更新,请在终端运行 install.sh 更新。" });
|
|
1575
|
+
return;
|
|
1579
1576
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1577
|
+
const helper = startDetachedUpdateHelper({
|
|
1578
|
+
installSpec,
|
|
1579
|
+
configPath,
|
|
1580
|
+
parentPid: process.pid,
|
|
1581
|
+
cliArgs: process.argv.slice(2),
|
|
1582
|
+
cwd: process.cwd(),
|
|
1583
|
+
env: process.env,
|
|
1584
|
+
timeoutMs: 300000,
|
|
1585
|
+
});
|
|
1586
|
+
if (!helper.started) {
|
|
1587
|
+
res.status(500).json({ error: helper.message, detail: `script=${helper.scriptPath}\nlog=${helper.logPath}` });
|
|
1585
1588
|
return;
|
|
1586
1589
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
+
process.stdout.write(`[wand] ${helper.message}\n`);
|
|
1591
|
+
wsManager.emitEvent({
|
|
1592
|
+
type: "notification",
|
|
1593
|
+
sessionId: "__system__",
|
|
1594
|
+
data: { kind: "auto-update-restart", current: PKG_VERSION, latest: targetLabel },
|
|
1595
|
+
});
|
|
1590
1596
|
res.json({
|
|
1591
1597
|
ok: true,
|
|
1592
|
-
message:
|
|
1593
|
-
restartRequired:
|
|
1598
|
+
message: `已开始更新到 ${targetLabel}`,
|
|
1599
|
+
restartRequired: false,
|
|
1600
|
+
detachedUpdate: true,
|
|
1594
1601
|
version: targetLabel,
|
|
1595
|
-
|
|
1602
|
+
logPath: helper.logPath,
|
|
1596
1603
|
});
|
|
1604
|
+
setTimeout(() => {
|
|
1605
|
+
shutdownForDetachedUpdate();
|
|
1606
|
+
}, 500);
|
|
1597
1607
|
}
|
|
1598
1608
|
catch (error) {
|
|
1599
1609
|
res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
|
|
@@ -2187,6 +2197,18 @@ export async function startServer(config, configPath) {
|
|
|
2187
2197
|
// Force exit after 5s if graceful shutdown stalls
|
|
2188
2198
|
setTimeout(() => process.exit(0), 5000);
|
|
2189
2199
|
}
|
|
2200
|
+
function shutdownForDetachedUpdate() {
|
|
2201
|
+
try {
|
|
2202
|
+
wss.clients.forEach((client) => client.close());
|
|
2203
|
+
}
|
|
2204
|
+
catch {
|
|
2205
|
+
/* noop */
|
|
2206
|
+
}
|
|
2207
|
+
server.close(() => {
|
|
2208
|
+
process.exit(0);
|
|
2209
|
+
});
|
|
2210
|
+
setTimeout(() => process.exit(0), 3000);
|
|
2211
|
+
}
|
|
2190
2212
|
app.post("/api/restart", async (_req, res) => {
|
|
2191
2213
|
res.json({ ok: true, message: "服务正在重启..." });
|
|
2192
2214
|
wsManager.emitEvent({
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DetachedUpdateOptions {
|
|
2
|
+
installSpec: string;
|
|
3
|
+
configPath: string;
|
|
4
|
+
parentPid: number;
|
|
5
|
+
cliArgs: string[];
|
|
6
|
+
cwd: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DetachedUpdateResult {
|
|
11
|
+
started: boolean;
|
|
12
|
+
scriptPath: string;
|
|
13
|
+
logPath: string;
|
|
14
|
+
pid?: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function startDetachedUpdateHelper(opts: DetachedUpdateOptions): DetachedUpdateResult;
|
|
18
|
+
export declare function canUseDetachedUpdateHelper(): boolean;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { detectInstalledScope } from "./tui/commands.js";
|
|
7
|
+
function shellQuote(value) {
|
|
8
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9
|
+
}
|
|
10
|
+
function shellArray(values) {
|
|
11
|
+
return values.map((value) => shellQuote(value)).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function boolShell(value) {
|
|
14
|
+
return value ? "1" : "0";
|
|
15
|
+
}
|
|
16
|
+
function resolveBashPath() {
|
|
17
|
+
return existsSync("/bin/bash") ? "/bin/bash" : "bash";
|
|
18
|
+
}
|
|
19
|
+
function detectServiceScope() {
|
|
20
|
+
try {
|
|
21
|
+
return detectInstalledScope();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function buildHelperScript(opts, logPath, serviceScope) {
|
|
28
|
+
const isSystemdManaged = !!process.env.INVOCATION_ID;
|
|
29
|
+
const isLaunchdManaged = !!process.env.LAUNCHD_SOCKET || !!process.env.XPC_SERVICE_NAME;
|
|
30
|
+
const shouldStartService = serviceScope !== null;
|
|
31
|
+
const shouldRestartStandalone = !shouldStartService;
|
|
32
|
+
const npmCache = opts.env.npm_config_cache || path.join(os.homedir(), ".npm");
|
|
33
|
+
const timeoutSec = Math.max(30, Math.ceil((opts.timeoutMs ?? 300_000) / 1000));
|
|
34
|
+
const cliArgArray = opts.cliArgs;
|
|
35
|
+
return `#!/usr/bin/env bash
|
|
36
|
+
set -u
|
|
37
|
+
LOG=${shellQuote(logPath)}
|
|
38
|
+
exec >>"$LOG" 2>&1
|
|
39
|
+
echo "[wand-update] started at $(date -Is)"
|
|
40
|
+
echo "[wand-update] parent pid: ${opts.parentPid}"
|
|
41
|
+
echo "[wand-update] install spec: ${opts.installSpec}"
|
|
42
|
+
|
|
43
|
+
export PATH=${shellQuote(opts.env.PATH || process.env.PATH || "")}
|
|
44
|
+
export HOME=${shellQuote(opts.env.HOME || os.homedir())}
|
|
45
|
+
export npm_config_cache=${shellQuote(npmCache)}
|
|
46
|
+
CONFIG_PATH=${shellQuote(opts.configPath)}
|
|
47
|
+
INSTALL_SPEC=${shellQuote(opts.installSpec)}
|
|
48
|
+
PARENT_PID=${opts.parentPid}
|
|
49
|
+
SERVICE_SCOPE=${shellQuote(serviceScope ?? "")}
|
|
50
|
+
SHOULD_START_SERVICE=${boolShell(shouldStartService)}
|
|
51
|
+
SHOULD_RESTART_STANDALONE=${boolShell(shouldRestartStandalone)}
|
|
52
|
+
TIMEOUT_SEC=${timeoutSec}
|
|
53
|
+
CLI_ARGS=(${shellArray(cliArgArray)})
|
|
54
|
+
|
|
55
|
+
run() {
|
|
56
|
+
echo "+ $*"
|
|
57
|
+
"$@"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run_best_effort() {
|
|
61
|
+
echo "+ $*"
|
|
62
|
+
"$@" || true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
wait_for_parent_exit() {
|
|
66
|
+
local pid="$1"
|
|
67
|
+
local i=0
|
|
68
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
69
|
+
i=$((i + 1))
|
|
70
|
+
if [ "$i" -ge 120 ]; then
|
|
71
|
+
echo "[wand-update] parent still alive after 60s; continuing"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
sleep 0.5
|
|
75
|
+
done
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clean_leftovers() {
|
|
79
|
+
local npm_root
|
|
80
|
+
npm_root="$(npm root -g 2>/dev/null || true)"
|
|
81
|
+
if [ -n "$npm_root" ] && [ -d "$npm_root/@co0ontty" ]; then
|
|
82
|
+
find "$npm_root/@co0ontty" -maxdepth 1 -name ".wand-*" -type d -print -exec rm -rf {} + 2>/dev/null || true
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
npm_install_wand() {
|
|
87
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
88
|
+
run timeout "$TIMEOUT_SEC" npm install -g "$INSTALL_SPEC"
|
|
89
|
+
else
|
|
90
|
+
run npm install -g "$INSTALL_SPEC"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
npm_install_wand_force() {
|
|
95
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
96
|
+
run timeout "$TIMEOUT_SEC" npm install -g --force "$INSTALL_SPEC"
|
|
97
|
+
else
|
|
98
|
+
run npm install -g --force "$INSTALL_SPEC"
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
restart_or_start_service() {
|
|
103
|
+
if [ "$SHOULD_START_SERVICE" = "1" ]; then
|
|
104
|
+
if [ "$SERVICE_SCOPE" = "user" ]; then
|
|
105
|
+
run_best_effort wand service:install --user -c "$CONFIG_PATH"
|
|
106
|
+
run_best_effort systemctl --user restart wand.service
|
|
107
|
+
else
|
|
108
|
+
run_best_effort wand service:install -c "$CONFIG_PATH"
|
|
109
|
+
run_best_effort systemctl restart wand.service
|
|
110
|
+
fi
|
|
111
|
+
return
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if [ "$SHOULD_RESTART_STANDALONE" = "1" ]; then
|
|
115
|
+
echo "[wand-update] starting standalone process"
|
|
116
|
+
cd ${shellQuote(opts.cwd)}
|
|
117
|
+
local global_cli
|
|
118
|
+
global_cli="$(npm root -g 2>/dev/null)/@co0ontty/wand/dist/cli.js"
|
|
119
|
+
if [ -f "$global_cli" ]; then
|
|
120
|
+
nohup ${shellQuote(process.execPath)} "$global_cli" "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
|
|
121
|
+
else
|
|
122
|
+
nohup wand "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main() {
|
|
128
|
+
if [ "$SERVICE_SCOPE" = "user" ]; then
|
|
129
|
+
run_best_effort systemctl --user stop wand.service
|
|
130
|
+
elif [ "$SERVICE_SCOPE" = "system" ]; then
|
|
131
|
+
run_best_effort systemctl stop wand.service
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
wait_for_parent_exit "$PARENT_PID"
|
|
135
|
+
clean_leftovers
|
|
136
|
+
|
|
137
|
+
echo "[wand-update] installing"
|
|
138
|
+
if ! npm_install_wand; then
|
|
139
|
+
echo "[wand-update] first install failed; retrying with uninstall + force install"
|
|
140
|
+
clean_leftovers
|
|
141
|
+
run_best_effort npm uninstall -g @co0ontty/wand
|
|
142
|
+
clean_leftovers
|
|
143
|
+
npm_install_wand_force
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
run wand init -c "$CONFIG_PATH"
|
|
147
|
+
restart_or_start_service
|
|
148
|
+
echo "[wand-update] completed at $(date -Is)"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
function spawnDetached(scriptPath, _logPath, env, serviceScope) {
|
|
155
|
+
const baseEnv = { ...env };
|
|
156
|
+
const trySpawn = (cmd, args, method) => {
|
|
157
|
+
try {
|
|
158
|
+
const child = spawn(cmd, args, {
|
|
159
|
+
detached: true,
|
|
160
|
+
stdio: "ignore",
|
|
161
|
+
env: baseEnv,
|
|
162
|
+
});
|
|
163
|
+
child.unref();
|
|
164
|
+
return { pid: child.pid, method };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
if (process.platform === "linux") {
|
|
171
|
+
const unitName = `wand-update-${process.pid}-${Date.now()}`;
|
|
172
|
+
const bashPath = resolveBashPath();
|
|
173
|
+
const runTransient = (args, method) => {
|
|
174
|
+
const res = spawnSync("systemd-run", args, {
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 5000,
|
|
177
|
+
env: baseEnv,
|
|
178
|
+
stdio: "ignore",
|
|
179
|
+
});
|
|
180
|
+
return res.status === 0 ? { method } : null;
|
|
181
|
+
};
|
|
182
|
+
const userService = runTransient(["--user", `--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run --user") ??
|
|
183
|
+
runTransient(["--user", `--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run --user");
|
|
184
|
+
if (userService)
|
|
185
|
+
return userService;
|
|
186
|
+
const systemService = runTransient([`--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run") ??
|
|
187
|
+
runTransient([`--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run");
|
|
188
|
+
if (systemService)
|
|
189
|
+
return systemService;
|
|
190
|
+
if (serviceScope) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const setsid = spawnSync("setsid", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
194
|
+
if (setsid.status === 0) {
|
|
195
|
+
const result = trySpawn("setsid", [bashPath, scriptPath], "setsid");
|
|
196
|
+
if (result)
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (process.platform === "darwin") {
|
|
201
|
+
const result = trySpawn("nohup", [resolveBashPath(), scriptPath], "nohup");
|
|
202
|
+
if (result)
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
return trySpawn(resolveBashPath(), [scriptPath], "bash detached");
|
|
206
|
+
}
|
|
207
|
+
export function startDetachedUpdateHelper(opts) {
|
|
208
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "wand-update-"));
|
|
209
|
+
const scriptPath = path.join(dir, "update.sh");
|
|
210
|
+
const logPath = path.join(dir, "update.log");
|
|
211
|
+
const serviceScope = detectServiceScope();
|
|
212
|
+
writeFileSync(scriptPath, buildHelperScript(opts, logPath, serviceScope), { encoding: "utf8", mode: 0o700 });
|
|
213
|
+
chmodSync(scriptPath, 0o700);
|
|
214
|
+
const spawned = spawnDetached(scriptPath, logPath, opts.env, serviceScope);
|
|
215
|
+
if (!spawned) {
|
|
216
|
+
return {
|
|
217
|
+
started: false,
|
|
218
|
+
scriptPath,
|
|
219
|
+
logPath,
|
|
220
|
+
message: "无法启动独立更新 helper。",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
started: true,
|
|
225
|
+
scriptPath,
|
|
226
|
+
logPath,
|
|
227
|
+
pid: spawned.pid,
|
|
228
|
+
message: `独立更新 helper 已启动 (${spawned.method})。日志: ${logPath}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export function canUseDetachedUpdateHelper() {
|
|
232
|
+
if (process.platform === "win32")
|
|
233
|
+
return false;
|
|
234
|
+
if (existsSync("/bin/bash"))
|
|
235
|
+
return true;
|
|
236
|
+
const res = spawnSync("bash", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
237
|
+
return res.status === 0;
|
|
238
|
+
}
|