@co0ontty/wand 1.25.3 → 1.26.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/server.js CHANGED
@@ -21,6 +21,7 @@ import { SessionLogger } from "./session-logger.js";
21
21
  import { StructuredSessionManager } from "./structured-session-manager.js";
22
22
  import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
23
23
  import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
24
+ import { installPackageGloballyAsync } from "./npm-update-utils.js";
24
25
  import { registerUploadRoutes } from "./upload-routes.js";
25
26
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
26
27
  import { resolveDatabasePath, WandStorage } from "./storage.js";
@@ -1063,23 +1064,13 @@ export async function startServer(config, configPath) {
1063
1064
  res.status(500).json({ error: getErrorMessage(error, "保存证书失败。") });
1064
1065
  }
1065
1066
  });
1066
- // ── Global npm install with ENOTEMPTY fallback ──
1067
+ // ── Global npm install (with leftover cleanup + ENOTEMPTY fallback) ──
1068
+ // 把所有恢复逻辑下沉到 ./npm-update-utils,TUI 和 server 共用,确保自动更新、
1069
+ // /api/update、tui installUpdate 三处行为一致。
1067
1070
  async function npmInstallGlobal(pkg, timeoutMs) {
1068
- try {
1069
- await execAsync(`npm install -g ${pkg}`, { timeout: timeoutMs });
1070
- }
1071
- catch (error) {
1072
- const msg = getErrorMessage(error, "");
1073
- if (msg.includes("ENOTEMPTY")) {
1074
- // Running process holds files in the install dir; uninstall first, then reinstall with --force.
1075
- process.stdout.write(`[wand] npm install 遇到 ENOTEMPTY,尝试先卸载再安装...\n`);
1076
- await execAsync(`npm uninstall -g ${pkg}`, { timeout: timeoutMs });
1077
- await execAsync(`npm install -g --force ${pkg}`, { timeout: timeoutMs });
1078
- }
1079
- else {
1080
- throw error;
1081
- }
1082
- }
1071
+ await installPackageGloballyAsync(pkg, timeoutMs, (line) => {
1072
+ process.stdout.write(`${line}\n`);
1073
+ });
1083
1074
  }
1084
1075
  app.get("/api/check-update", async (_req, res) => {
1085
1076
  try {
@@ -1104,7 +1095,13 @@ export async function startServer(config, configPath) {
1104
1095
  return;
1105
1096
  }
1106
1097
  await npmInstallGlobal(`${PKG_NAME}@latest`, 120000);
1107
- res.json({ ok: true, message: `已更新到 ${latest},请重启 wand 服务以生效。` });
1098
+ // 装包成功后告知前端可以发起重启;前端会随即调用 /api/restart 完成自动重启。
1099
+ res.json({
1100
+ ok: true,
1101
+ message: `已更新到 ${latest}`,
1102
+ restartRequired: true,
1103
+ version: latest,
1104
+ });
1108
1105
  }
1109
1106
  catch (error) {
1110
1107
  res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
@@ -1177,10 +1177,66 @@ export class StructuredSessionManager {
1177
1177
  const blocksByKey = new Map();
1178
1178
  const keyOrder = [];
1179
1179
  let toolResultSeq = 0;
1180
+ // 估算单个 ContentBlock 的"信息体积"——文字 / thinking / tool input 长度之和。
1181
+ // 用于 upsertBlocks 的防御性合并:同一 message.id 重发时,按位置取信息量更大的
1182
+ // 那个版本,保证已经吐出的文字 / tool_use input 不会被一条更短的同 id 事件
1183
+ // 整段覆盖。
1184
+ const blockVolume = (b) => {
1185
+ if (!b)
1186
+ return 0;
1187
+ const anyB = b;
1188
+ let total = 0;
1189
+ if (typeof anyB.text === "string")
1190
+ total += anyB.text.length;
1191
+ if (typeof anyB.thinking === "string")
1192
+ total += anyB.thinking.length;
1193
+ if (typeof anyB.content === "string")
1194
+ total += anyB.content.length;
1195
+ if (anyB.input) {
1196
+ try {
1197
+ total += JSON.stringify(anyB.input).length;
1198
+ }
1199
+ catch (_e) { /* ignore */ }
1200
+ }
1201
+ return total;
1202
+ };
1180
1203
  const upsertBlocks = (key, blocks) => {
1181
- if (!blocksByKey.has(key))
1204
+ const prev = blocksByKey.get(key);
1205
+ if (!prev) {
1182
1206
  keyOrder.push(key);
1183
- blocksByKey.set(key, blocks);
1207
+ blocksByKey.set(key, blocks);
1208
+ return;
1209
+ }
1210
+ // claude -p 在同一 message.id 的多次 assistant 事件里**应当**每次发出累加内容。
1211
+ // 但偶发会观察到某次重发只带其中一部分 block(典型场景:text 后又开始一段
1212
+ // tool_use,下一帧 text 字段意外为空字符串),导致先前已渲染的文本被覆盖
1213
+ // 为空——刷新页面后从持久化又恢复出来,就是用户反馈的"显示了又消失"。
1214
+ // 这里按 index 逐块取较"重"的版本,类型一致则取信息量大者,否则信任 incoming
1215
+ // 但**绝不**让 incoming 比 prev 的总块数少(短数组拼接 prev 的尾部)。
1216
+ const merged = [];
1217
+ const maxLen = Math.max(prev.length, blocks.length);
1218
+ for (let i = 0; i < maxLen; i++) {
1219
+ const a = prev[i];
1220
+ const b = blocks[i];
1221
+ if (a && !b) {
1222
+ merged.push(a);
1223
+ continue;
1224
+ }
1225
+ if (!a && b) {
1226
+ merged.push(b);
1227
+ continue;
1228
+ }
1229
+ if (a && b) {
1230
+ if (a.type === b.type) {
1231
+ merged.push(blockVolume(b) >= blockVolume(a) ? b : a);
1232
+ }
1233
+ else {
1234
+ // 类型变了(极少见,多半是上游 bug)——保留信息量大者,不丢字。
1235
+ merged.push(blockVolume(b) >= blockVolume(a) ? b : a);
1236
+ }
1237
+ }
1238
+ }
1239
+ blocksByKey.set(key, merged);
1184
1240
  };
1185
1241
  const rebuildTurnBlocks = () => {
1186
1242
  const flat = [];
@@ -26,7 +26,11 @@ export interface UpdateInfo {
26
26
  export declare function checkUpdate(currentVersion: string): UpdateInfo;
27
27
  /**
28
28
  * 执行 `npm install -g @co0ontty/wand@latest`。
29
+ *
29
30
  * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
31
+ * 通过 npm-update-utils 自动处理 `.wand-XXXXXX` 残留目录和 ENOTEMPTY 回退,
32
+ * 行为与 server.ts 的 /api/update / performAutoUpdate 保持一致。
33
+ *
30
34
  * 返回 npm 输出供调试。
31
35
  */
32
36
  export declare function installUpdate(): CommandResult;
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
11
  import process from "node:process";
12
+ import { installPackageGloballySync } from "../npm-update-utils.js";
12
13
  const PACKAGE_NAME = "@co0ontty/wand";
13
14
  // ─── 重启 ────────────────────────────────────────────────────────────────
14
15
  /**
@@ -52,26 +53,30 @@ export function checkUpdate(currentVersion) {
52
53
  }
53
54
  /**
54
55
  * 执行 `npm install -g @co0ontty/wand@latest`。
56
+ *
55
57
  * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
58
+ * 通过 npm-update-utils 自动处理 `.wand-XXXXXX` 残留目录和 ENOTEMPTY 回退,
59
+ * 行为与 server.ts 的 /api/update / performAutoUpdate 保持一致。
60
+ *
56
61
  * 返回 npm 输出供调试。
57
62
  */
58
63
  export function installUpdate() {
59
- const res = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
60
- encoding: "utf8",
61
- timeout: 180_000,
62
- });
64
+ const res = installPackageGloballySync(`${PACKAGE_NAME}@latest`, 180_000);
63
65
  const out = (res.stdout || "") + (res.stderr ? "\n" + res.stderr : "");
66
+ const trail = res.attempts.length > 1
67
+ ? `\n\n尝试过的命令:\n ${res.attempts.join("\n ")}`
68
+ : "";
64
69
  if (res.status === 0) {
65
70
  return {
66
71
  ok: true,
67
72
  message: "更新已安装。按 [R] 重启以生效。",
68
- detail: out.trim(),
73
+ detail: (out.trim() + trail).trim(),
69
74
  };
70
75
  }
71
76
  return {
72
77
  ok: false,
73
78
  message: `npm install 失败 (exit ${res.status})`,
74
- detail: out.trim(),
79
+ detail: (out.trim() + trail).trim(),
75
80
  };
76
81
  }
77
82
  // ─── 打开浏览器 ─────────────────────────────────────────────────────────
@@ -358,6 +363,10 @@ function installSystemdUserService(ctx) {
358
363
  const unitPath = servicePath();
359
364
  const wandBin = resolveWandBin(ctx);
360
365
  const nodeBin = process.execPath;
366
+ // Restart=always 是关键:自动更新 / /api/restart 走的是 "spawn detached + exit 0",
367
+ // 在 systemd 用户服务的 cgroup 下,detached 子进程会随父进程一起被清理(默认
368
+ // KillMode=control-group),只有 Restart=always 能让 systemd 在 exit 0 后拉起
369
+ // 新进程,把更新真正生效。
361
370
  const unit = [
362
371
  "[Unit]",
363
372
  "Description=wand web console",
@@ -367,7 +376,7 @@ function installSystemdUserService(ctx) {
367
376
  "Type=simple",
368
377
  `ExecStart=${nodeBin} ${wandBin} web -c ${ctx.configPath}`,
369
378
  `Environment=WAND_NO_TUI=1`,
370
- "Restart=on-failure",
379
+ "Restart=always",
371
380
  "RestartSec=3",
372
381
  "",
373
382
  "[Install]",
package/dist/types.d.ts CHANGED
@@ -169,6 +169,22 @@ export interface GitStatusResult {
169
169
  repoRoot?: string;
170
170
  /** Truthy when the repo has no commits yet (initial state). */
171
171
  initialCommit?: boolean;
172
+ /** Whether current branch has an upstream tracking branch. */
173
+ hasUpstream?: boolean;
174
+ /** Upstream branch identifier (e.g. `origin/main`). Only set when `hasUpstream` is true. */
175
+ upstream?: string;
176
+ /** Number of local commits not yet on upstream. Only meaningful when `hasUpstream` is true. */
177
+ ahead?: number;
178
+ /** Number of upstream commits not yet locally. Only meaningful when `hasUpstream` is true. */
179
+ behind?: number;
180
+ /** HEAD commit subject + short hash (handy for "tag the current commit" UX). */
181
+ lastCommit?: {
182
+ hash: string;
183
+ shortHash: string;
184
+ subject: string;
185
+ };
186
+ /** Number of local tags that don't exist on the remote (best-effort, may be undefined if not reachable). */
187
+ unpushedTagCount?: number;
172
188
  error?: string;
173
189
  }
174
190
  export interface QuickCommitResult {
@@ -184,6 +200,22 @@ export interface QuickCommitResult {
184
200
  /** commit 已成功但 push 失败时填入;前端用它显示"已提交但 push 失败"。 */
185
201
  pushError?: string;
186
202
  }
203
+ export interface TagHeadResult {
204
+ ok: boolean;
205
+ tag: {
206
+ name: string;
207
+ commit: string;
208
+ };
209
+ pushed?: boolean;
210
+ pushError?: string;
211
+ }
212
+ export interface PushResult {
213
+ ok: boolean;
214
+ pushedCommits: boolean;
215
+ pushedTags: boolean;
216
+ /** Either operation failed — the other may still have succeeded. */
217
+ error?: string;
218
+ }
187
219
  export interface CommandRequest {
188
220
  command: string;
189
221
  provider?: SessionProvider;