@co0ontty/wand 1.41.3 → 1.42.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/npm-update-utils.d.ts +2 -2
- package/dist/npm-update-utils.js +191 -58
- package/dist/pwa.js +2 -22
- package/dist/server.js +49 -27
- package/dist/structured-session-manager.d.ts +3 -3
- package/dist/structured-session-manager.js +9 -7
- package/dist/types.d.ts +1 -1
- package/dist/update-helper.d.ts +18 -0
- package/dist/update-helper.js +238 -0
- package/dist/web-ui/content/scripts.js +9 -3
- package/dist/web-ui/embedded-assets.d.ts +23 -0
- package/dist/web-ui/embedded-assets.js +27 -0
- package/dist/web-ui/index.d.ts +2 -0
- package/dist/web-ui/index.js +3 -26
- package/dist/web-ui/scripts.js +5 -6
- package/dist/web-ui/styles.js +4 -5
- package/package.json +5 -4
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-05-
|
|
4
|
-
"version": "1.
|
|
2
|
+
"commit": "c94bcd87ab8e081c2d72f7d9c35130991379e5e0",
|
|
3
|
+
"builtAt": "2026-05-31T01:26:56.891Z",
|
|
4
|
+
"version": "1.42.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* 如果安装中途失败,这个备份目录会留下,之后每次 npm install 都会因为目标 dest 已存在
|
|
9
9
|
* 报 `ENOTEMPTY: directory not empty, rename ...`。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* 我们的策略:安装前备份当前全局包,补齐 npm 子进程 PATH,清掉
|
|
12
|
+
* `@co0ontty/.wand-*` 残留目录;失败时恢复备份,避免运行中的服务被半成品安装拆掉。
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
15
|
* 解析当前 `npm root -g` 的目录。失败返回 null。
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -8,24 +8,64 @@
|
|
|
8
8
|
* 如果安装中途失败,这个备份目录会留下,之后每次 npm install 都会因为目标 dest 已存在
|
|
9
9
|
* 报 `ENOTEMPTY: directory not empty, rename ...`。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* 我们的策略:安装前备份当前全局包,补齐 npm 子进程 PATH,清掉
|
|
12
|
+
* `@co0ontty/.wand-*` 残留目录;失败时恢复备份,避免运行中的服务被半成品安装拆掉。
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import { chmodSync, existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
14
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
15
|
+
import { chmodSync, cpSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
import process from "node:process";
|
|
18
19
|
import { promisify } from "node:util";
|
|
19
|
-
const
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
20
21
|
const PACKAGE_NAME = "@co0ontty/wand";
|
|
21
22
|
const PACKAGE_SCOPE = "@co0ontty";
|
|
22
23
|
const PACKAGE_BASENAME = "wand";
|
|
24
|
+
const NPM_BIN = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
25
|
+
const COMMON_UNIX_PATHS = ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"];
|
|
26
|
+
const INSTALL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
27
|
+
function getChildEnv() {
|
|
28
|
+
const entries = [
|
|
29
|
+
path.dirname(process.execPath),
|
|
30
|
+
...(process.env.PATH || "").split(path.delimiter),
|
|
31
|
+
...(process.platform === "win32" ? [] : COMMON_UNIX_PATHS),
|
|
32
|
+
];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const pathEntries = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry || seen.has(entry))
|
|
37
|
+
continue;
|
|
38
|
+
seen.add(entry);
|
|
39
|
+
pathEntries.push(entry);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...process.env,
|
|
43
|
+
PATH: pathEntries.join(path.delimiter),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function runNpmSync(args, timeoutMs) {
|
|
47
|
+
const options = {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
timeout: timeoutMs,
|
|
50
|
+
env: getChildEnv(),
|
|
51
|
+
maxBuffer: INSTALL_MAX_BUFFER,
|
|
52
|
+
};
|
|
53
|
+
return spawnSync(NPM_BIN, args, options);
|
|
54
|
+
}
|
|
55
|
+
async function runNpmAsync(args, timeoutMs) {
|
|
56
|
+
const options = {
|
|
57
|
+
timeout: timeoutMs,
|
|
58
|
+
env: getChildEnv(),
|
|
59
|
+
maxBuffer: INSTALL_MAX_BUFFER,
|
|
60
|
+
};
|
|
61
|
+
await execFileAsync(NPM_BIN, args, options);
|
|
62
|
+
}
|
|
23
63
|
/**
|
|
24
64
|
* 解析当前 `npm root -g` 的目录。失败返回 null。
|
|
25
65
|
*/
|
|
26
66
|
export function getNpmGlobalRoot() {
|
|
27
67
|
try {
|
|
28
|
-
const res =
|
|
68
|
+
const res = runNpmSync(["root", "-g"], 10_000);
|
|
29
69
|
if (res.status !== 0)
|
|
30
70
|
return null;
|
|
31
71
|
const out = (res.stdout || "").trim();
|
|
@@ -82,6 +122,7 @@ const REQUIRED_RUNTIME_FILES = [
|
|
|
82
122
|
path.join("dist", "cli.js"),
|
|
83
123
|
path.join("dist", "server.js"),
|
|
84
124
|
path.join("dist", "web-ui", "index.js"),
|
|
125
|
+
path.join("dist", "web-ui", "embedded-assets.js"),
|
|
85
126
|
path.join("dist", "web-ui", "scripts.js"),
|
|
86
127
|
path.join("dist", "web-ui", "styles.js"),
|
|
87
128
|
path.join("dist", "web-ui", "content", "scripts.js"),
|
|
@@ -139,6 +180,60 @@ function assertGlobalWandInstallComplete() {
|
|
|
139
180
|
throw new Error(result.message);
|
|
140
181
|
}
|
|
141
182
|
}
|
|
183
|
+
function createGlobalInstallBackup(note) {
|
|
184
|
+
const packageDir = getGlobalPackageDir();
|
|
185
|
+
if (!packageDir) {
|
|
186
|
+
return { packageDir: "", backupDir: null };
|
|
187
|
+
}
|
|
188
|
+
if (!existsSync(packageDir)) {
|
|
189
|
+
return { packageDir, backupDir: null };
|
|
190
|
+
}
|
|
191
|
+
const backupRoot = mkdtempSync(path.join(os.tmpdir(), "wand-global-backup-"));
|
|
192
|
+
const backupDir = path.join(backupRoot, PACKAGE_BASENAME);
|
|
193
|
+
try {
|
|
194
|
+
cpSync(packageDir, backupDir, {
|
|
195
|
+
recursive: true,
|
|
196
|
+
dereference: false,
|
|
197
|
+
verbatimSymlinks: true,
|
|
198
|
+
});
|
|
199
|
+
note?.(`[wand] 已备份当前全局安装: ${backupDir}`);
|
|
200
|
+
return { packageDir, backupDir };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
rmSync(backupRoot, { recursive: true, force: true });
|
|
204
|
+
note?.(`[wand] 全局安装备份失败,继续尝试更新: ${err instanceof Error ? err.message : String(err)}`);
|
|
205
|
+
return { packageDir, backupDir: null };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function cleanupGlobalInstallBackup(backup) {
|
|
209
|
+
if (!backup.backupDir)
|
|
210
|
+
return;
|
|
211
|
+
rmSync(path.dirname(backup.backupDir), { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
function restoreGlobalInstallBackup(backup, note) {
|
|
214
|
+
if (!backup.packageDir || !backup.backupDir || !existsSync(backup.backupDir))
|
|
215
|
+
return false;
|
|
216
|
+
try {
|
|
217
|
+
rmSync(backup.packageDir, { recursive: true, force: true });
|
|
218
|
+
cpSync(backup.backupDir, backup.packageDir, {
|
|
219
|
+
recursive: true,
|
|
220
|
+
dereference: false,
|
|
221
|
+
verbatimSymlinks: true,
|
|
222
|
+
});
|
|
223
|
+
note?.(`[wand] 已恢复更新前的全局安装: ${backup.packageDir}`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
note?.(`[wand] 恢复更新前安装失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function npmInstallGlobalAsync(pkg, timeoutMs, extra = []) {
|
|
232
|
+
await runNpmAsync(["install", "-g", ...extra, pkg], timeoutMs);
|
|
233
|
+
}
|
|
234
|
+
function isRecoverableInstallError(message) {
|
|
235
|
+
return /ENOTEMPTY|EEXIST|全局 wand 安装不完整|无法解析 npm 全局安装目录|全局 wand CLI 无法设置执行权限/.test(message);
|
|
236
|
+
}
|
|
142
237
|
/**
|
|
143
238
|
* 异步版本的全局安装:
|
|
144
239
|
* 1. 清理残留
|
|
@@ -155,54 +250,67 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
|
|
|
155
250
|
if (log)
|
|
156
251
|
log(line);
|
|
157
252
|
};
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
note(`[wand] 清理 npm 残留目录: ${cleanup.removed.join(", ")}`);
|
|
161
|
-
}
|
|
253
|
+
const backup = createGlobalInstallBackup(note);
|
|
254
|
+
let success = false;
|
|
162
255
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
256
|
+
const cleanup = cleanupNpmLeftovers();
|
|
257
|
+
if (cleanup.removed.length > 0) {
|
|
258
|
+
note(`[wand] 清理 npm 残留目录: ${cleanup.removed.join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await npmInstallGlobalAsync(pkg, timeoutMs);
|
|
262
|
+
assertGlobalWandInstallComplete();
|
|
263
|
+
success = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
268
|
+
if (!isRecoverableInstallError(msg)) {
|
|
174
269
|
throw error;
|
|
175
270
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
note(`[wand] npm install 遇到 ENOTEMPTY/EEXIST,清理后重试一次...`);
|
|
179
|
-
cleanupNpmLeftovers();
|
|
180
|
-
try {
|
|
181
|
-
await execAsync(`npm install -g ${pkg}`, { timeout: timeoutMs });
|
|
182
|
-
assertGlobalWandInstallComplete();
|
|
183
|
-
return;
|
|
271
|
+
if (/全局 wand 安装不完整|无法解析 npm 全局安装目录|全局 wand CLI 无法设置执行权限/.test(msg)) {
|
|
272
|
+
note(`[wand] npm install 后安装目录不完整,尝试强制重装...`);
|
|
184
273
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
274
|
+
else {
|
|
275
|
+
note(`[wand] npm install 遇到 ENOTEMPTY/EEXIST,清理后重试一次...`);
|
|
276
|
+
cleanupNpmLeftovers();
|
|
277
|
+
try {
|
|
278
|
+
await npmInstallGlobalAsync(pkg, timeoutMs);
|
|
279
|
+
assertGlobalWandInstallComplete();
|
|
280
|
+
success = true;
|
|
281
|
+
return;
|
|
189
282
|
}
|
|
283
|
+
catch (retryError) {
|
|
284
|
+
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
285
|
+
if (!isRecoverableInstallError(retryMsg)) {
|
|
286
|
+
throw retryError;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
note(`[wand] 重试仍失败,尝试先卸载再强制安装...`);
|
|
190
290
|
}
|
|
191
|
-
note(`[wand] 重试仍失败,尝试先卸载再强制安装...`);
|
|
192
291
|
}
|
|
292
|
+
// 终极兜底:uninstall + force install
|
|
293
|
+
// 卸载用固定包名 PACKAGE_NAME,而不是从 install spec 反推:spec 可能是 git
|
|
294
|
+
// 形式(`github:co0ontty/wand#beta`),用正则 strip @tag 反推会得到错误的卸载目标。
|
|
295
|
+
try {
|
|
296
|
+
await runNpmAsync(["uninstall", "-g", PACKAGE_NAME], timeoutMs);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* 卸载失败也继续,下一步 --force 可能仍然能装上 */
|
|
300
|
+
}
|
|
301
|
+
cleanupNpmLeftovers();
|
|
302
|
+
await npmInstallGlobalAsync(pkg, timeoutMs, ["--force"]);
|
|
303
|
+
assertGlobalWandInstallComplete();
|
|
304
|
+
success = true;
|
|
193
305
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/* 卸载失败也继续,下一步 --force 可能仍然能装上 */
|
|
306
|
+
finally {
|
|
307
|
+
if (!success) {
|
|
308
|
+
if (restoreGlobalInstallBackup(backup, note)) {
|
|
309
|
+
cleanupNpmLeftovers();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
cleanupGlobalInstallBackup(backup);
|
|
202
313
|
}
|
|
203
|
-
cleanupNpmLeftovers();
|
|
204
|
-
await execAsync(`npm install -g --force ${pkg}`, { timeout: timeoutMs });
|
|
205
|
-
assertGlobalWandInstallComplete();
|
|
206
314
|
}
|
|
207
315
|
/**
|
|
208
316
|
* 同步版本,给 TUI installUpdate 用。
|
|
@@ -211,6 +319,8 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
|
|
|
211
319
|
*/
|
|
212
320
|
export function installPackageGloballySync(pkg, timeoutMs) {
|
|
213
321
|
const attempts = [];
|
|
322
|
+
const backupNotes = [];
|
|
323
|
+
const backup = createGlobalInstallBackup((line) => backupNotes.push(line));
|
|
214
324
|
const withValidation = (res) => {
|
|
215
325
|
if (res.status !== 0)
|
|
216
326
|
return res;
|
|
@@ -226,32 +336,55 @@ export function installPackageGloballySync(pkg, timeoutMs) {
|
|
|
226
336
|
const tryInstall = (extra) => {
|
|
227
337
|
const args = ["install", "-g", ...extra, pkg];
|
|
228
338
|
attempts.push(`npm ${args.join(" ")}`);
|
|
229
|
-
const r =
|
|
339
|
+
const r = runNpmSync(args, timeoutMs);
|
|
230
340
|
return withValidation({
|
|
231
341
|
status: r.status,
|
|
232
342
|
stdout: r.stdout || "",
|
|
233
343
|
stderr: r.stderr || "",
|
|
234
344
|
});
|
|
235
345
|
};
|
|
346
|
+
const withBackupNotes = (res) => ({
|
|
347
|
+
...res,
|
|
348
|
+
stderr: [res.stderr, ...backupNotes].filter(Boolean).join("\n"),
|
|
349
|
+
attempts,
|
|
350
|
+
});
|
|
351
|
+
const finishSuccess = (res) => {
|
|
352
|
+
cleanupGlobalInstallBackup(backup);
|
|
353
|
+
return withBackupNotes(res);
|
|
354
|
+
};
|
|
355
|
+
const finishFailure = (res) => {
|
|
356
|
+
if (restoreGlobalInstallBackup(backup, (line) => backupNotes.push(line))) {
|
|
357
|
+
cleanupNpmLeftovers();
|
|
358
|
+
}
|
|
359
|
+
cleanupGlobalInstallBackup(backup);
|
|
360
|
+
return withBackupNotes(res);
|
|
361
|
+
};
|
|
236
362
|
cleanupNpmLeftovers();
|
|
237
363
|
let res = tryInstall([]);
|
|
238
|
-
if (res.status === 0)
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
364
|
+
if (res.status === 0) {
|
|
365
|
+
return finishSuccess(res);
|
|
366
|
+
}
|
|
367
|
+
const hitRecoverableInstallError = (r) => isRecoverableInstallError(r.stdout + r.stderr);
|
|
368
|
+
if (!hitRecoverableInstallError(res)) {
|
|
369
|
+
return finishFailure(res);
|
|
370
|
+
}
|
|
243
371
|
cleanupNpmLeftovers();
|
|
244
372
|
res = tryInstall([]);
|
|
245
|
-
if (res.status === 0)
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
373
|
+
if (res.status === 0) {
|
|
374
|
+
return finishSuccess(res);
|
|
375
|
+
}
|
|
376
|
+
if (!hitRecoverableInstallError(res)) {
|
|
377
|
+
return finishFailure(res);
|
|
378
|
+
}
|
|
249
379
|
// 终极兜底(卸载用固定包名,兼容 git spec,见 async 版同样注释)
|
|
250
380
|
attempts.push(`npm uninstall -g ${PACKAGE_NAME}`);
|
|
251
|
-
|
|
381
|
+
runNpmSync(["uninstall", "-g", PACKAGE_NAME], timeoutMs);
|
|
252
382
|
cleanupNpmLeftovers();
|
|
253
383
|
res = tryInstall(["--force"]);
|
|
254
|
-
|
|
384
|
+
if (res.status === 0) {
|
|
385
|
+
return finishSuccess(res);
|
|
386
|
+
}
|
|
387
|
+
return finishFailure(res);
|
|
255
388
|
}
|
|
256
389
|
/**
|
|
257
390
|
* 解析「刚装好的全局 wand CLI 入口」(dist/cli.js) 的绝对路径。
|
|
@@ -276,7 +409,7 @@ export function resolveGlobalWandCli() {
|
|
|
276
409
|
}
|
|
277
410
|
try {
|
|
278
411
|
const tool = process.platform === "win32" ? "where" : "which";
|
|
279
|
-
const r = spawnSync(tool, ["wand"], { encoding: "utf8", timeout: 10_000 });
|
|
412
|
+
const r = spawnSync(tool, ["wand"], { encoding: "utf8", timeout: 10_000, env: getChildEnv() });
|
|
280
413
|
if (r.status === 0) {
|
|
281
414
|
const first = (r.stdout || "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
282
415
|
if (first)
|
package/dist/pwa.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PWA manifest and Service Worker generation.
|
|
3
3
|
*/
|
|
4
|
-
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
10
|
-
const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0";
|
|
5
|
+
import { EMBEDDED_WEB_ASSET_VERSION } from "./web-ui/embedded-assets.js";
|
|
11
6
|
/** Cache version: package version + content fingerprint.
|
|
12
7
|
*
|
|
13
8
|
* 之前只用 pkgVersion 派生,本地 dev 时同一个 1.36.0 下改了几次 CSS / scripts,
|
|
@@ -18,22 +13,7 @@ const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0";
|
|
|
18
13
|
* 正式发版时由于 pkgVersion 也会变,效果叠加,无副作用。
|
|
19
14
|
*/
|
|
20
15
|
function buildCacheVersion() {
|
|
21
|
-
const h = createHash("md5").update(
|
|
22
|
-
const fingerprintTargets = [
|
|
23
|
-
path.join(__dirname, "web-ui", "content", "scripts.js"),
|
|
24
|
-
path.join(__dirname, "web-ui", "content", "styles.css"),
|
|
25
|
-
];
|
|
26
|
-
for (const p of fingerprintTargets) {
|
|
27
|
-
try {
|
|
28
|
-
if (existsSync(p)) {
|
|
29
|
-
const s = statSync(p);
|
|
30
|
-
h.update(":").update(p).update(":").update(String(s.mtimeMs)).update(":").update(String(s.size));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// best effort — fingerprint 只是为了 bust 缓存,失败就退化成 pkg-only
|
|
35
|
-
}
|
|
36
|
-
}
|
|
16
|
+
const h = createHash("md5").update(EMBEDDED_WEB_ASSET_VERSION);
|
|
37
17
|
return h.digest("hex").slice(0, 8);
|
|
38
18
|
}
|
|
39
19
|
// 不 freeze 进模块加载时——SW JS 是每次请求 generateServiceWorker() 现拼的,
|
package/dist/server.js
CHANGED
|
@@ -25,11 +25,13 @@ 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";
|
|
31
32
|
import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
|
|
32
33
|
import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
34
|
+
import { EMBEDDED_WEB_ASSETS } from "./web-ui/embedded-assets.js";
|
|
33
35
|
import { renderApp } from "./web-ui/index.js";
|
|
34
36
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
35
37
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
@@ -930,12 +932,14 @@ export async function startServer(config, configPath) {
|
|
|
930
932
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
931
933
|
app.use(express.json({ limit: "1mb" }));
|
|
932
934
|
app.use(compression({ threshold: 1024 }));
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
app.
|
|
935
|
+
const sendEmbeddedVendorAsset = (assetPath, _req, res) => {
|
|
936
|
+
const asset = EMBEDDED_WEB_ASSETS.vendor[assetPath];
|
|
937
|
+
res.setHeader("Cache-Control", "public, max-age=604800, immutable");
|
|
938
|
+
res.type(asset.contentType).send(asset.content);
|
|
939
|
+
};
|
|
940
|
+
app.get("/vendor/wterm/wterm.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/wterm/wterm.bundle.js", req, res));
|
|
941
|
+
app.get("/vendor/wterm/terminal.css", (req, res) => sendEmbeddedVendorAsset("/vendor/wterm/terminal.css", req, res));
|
|
942
|
+
app.get("/vendor/qrcode/qrcode.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/qrcode/qrcode.bundle.js", req, res));
|
|
939
943
|
// ── Web UI and PWA endpoints ──
|
|
940
944
|
app.get("/", (_req, res) => {
|
|
941
945
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -994,9 +998,6 @@ export async function startServer(config, configPath) {
|
|
|
994
998
|
res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, size));
|
|
995
999
|
});
|
|
996
1000
|
}
|
|
997
|
-
const iconsDir = path.resolve(existsSync(path.join(SERVER_MODULE_DIR, "web-ui", "content"))
|
|
998
|
-
? path.join(SERVER_MODULE_DIR, "web-ui", "content")
|
|
999
|
-
: path.join(RUNTIME_ROOT_DIR, "src", "web-ui", "content"));
|
|
1000
1001
|
app.get("/sw.js", (_req, res) => {
|
|
1001
1002
|
res.setHeader("Content-Type", "application/javascript");
|
|
1002
1003
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -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({
|
|
@@ -32,8 +32,8 @@ export declare function thinkingEffortToSdkBudget(effort: SessionSnapshot["think
|
|
|
32
32
|
* off → 原 prompt 不变。
|
|
33
33
|
*/
|
|
34
34
|
export declare function applyThinkingEffortToPrompt(prompt: string, effort: SessionSnapshot["thinkingEffort"]): string;
|
|
35
|
-
/** Codex CLI 用:把 thinkingEffort 映射到
|
|
36
|
-
export declare function
|
|
35
|
+
/** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → minimal。 */
|
|
36
|
+
export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
37
37
|
export declare class StructuredSessionManager {
|
|
38
38
|
private readonly storage;
|
|
39
39
|
private readonly config;
|
|
@@ -110,7 +110,7 @@ export declare class StructuredSessionManager {
|
|
|
110
110
|
/**
|
|
111
111
|
* Update the thinking-effort level for a structured session. Takes effect on
|
|
112
112
|
* the next spawn / next message (SDK runner injects `thinking`, CLI runner
|
|
113
|
-
* prepends magic words, codex runner
|
|
113
|
+
* prepends magic words, codex runner overrides `model_reasoning_effort`).
|
|
114
114
|
*/
|
|
115
115
|
setSessionThinkingEffort(sessionId: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot;
|
|
116
116
|
/** Toggle auto-approve for the session. */
|
|
@@ -74,8 +74,8 @@ export function applyThinkingEffortToPrompt(prompt, effort) {
|
|
|
74
74
|
return prompt;
|
|
75
75
|
return prefix + trimmed;
|
|
76
76
|
}
|
|
77
|
-
/** Codex CLI 用:把 thinkingEffort 映射到
|
|
78
|
-
export function
|
|
77
|
+
/** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → minimal。 */
|
|
78
|
+
export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
79
79
|
switch (effort) {
|
|
80
80
|
case "standard": return "low";
|
|
81
81
|
case "deep": return "medium";
|
|
@@ -862,7 +862,7 @@ export class StructuredSessionManager {
|
|
|
862
862
|
/**
|
|
863
863
|
* Update the thinking-effort level for a structured session. Takes effect on
|
|
864
864
|
* the next spawn / next message (SDK runner injects `thinking`, CLI runner
|
|
865
|
-
* prepends magic words, codex runner
|
|
865
|
+
* prepends magic words, codex runner overrides `model_reasoning_effort`).
|
|
866
866
|
*/
|
|
867
867
|
setSessionThinkingEffort(sessionId, effort) {
|
|
868
868
|
const session = this.requireSession(sessionId);
|
|
@@ -1117,10 +1117,12 @@ export class StructuredSessionManager {
|
|
|
1117
1117
|
if (modelChoice && modelChoice !== "default") {
|
|
1118
1118
|
args.push("--model", modelChoice);
|
|
1119
1119
|
}
|
|
1120
|
-
// 思考深度 →
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1120
|
+
// 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max → high)
|
|
1121
|
+
// Newer Codex CLI versions removed the old dedicated exec flag, but still
|
|
1122
|
+
// accept config overrides through `-c`.
|
|
1123
|
+
const reasoningEffort = thinkingEffortToCodexReasoningEffort(session.thinkingEffort);
|
|
1124
|
+
if (reasoningEffort) {
|
|
1125
|
+
args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
|
|
1124
1126
|
}
|
|
1125
1127
|
if (session.claudeSessionId) {
|
|
1126
1128
|
args.push("resume", session.claudeSessionId, "-");
|
package/dist/types.d.ts
CHANGED
|
@@ -440,7 +440,7 @@ export interface SessionSnapshot {
|
|
|
440
440
|
selectedModel?: string | null;
|
|
441
441
|
/**
|
|
442
442
|
* 用户选定的思考深度。
|
|
443
|
-
* - off: 不启用思考(SDK: 不传 thinking;CLI: 不插魔法词;Codex:
|
|
443
|
+
* - off: 不启用思考(SDK: 不传 thinking;CLI: 不插魔法词;Codex: model_reasoning_effort minimal)
|
|
444
444
|
* - standard: 标准(SDK: budget 4096;CLI: think;Codex: low)
|
|
445
445
|
* - deep: 深度(SDK: budget 16000;CLI: think hard;Codex: medium)
|
|
446
446
|
* - max: 最深(SDK: budget 31999;CLI: ultrathink;Codex: high)
|
|
@@ -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;
|