@co0ontty/wand 4.93.0 → 4.94.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.
Files changed (172) hide show
  1. package/README.md +25 -30
  2. package/dist/ai-text-runner.d.ts +2 -10
  3. package/dist/ai-text-runner.js +13 -74
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli.js +47 -9
  6. package/dist/config.d.ts +2 -3
  7. package/dist/config.js +26 -5
  8. package/dist/core-status-cli.d.ts +1 -1
  9. package/dist/core-status-cli.js +3 -1
  10. package/dist/daemon-connection.d.ts +10 -0
  11. package/dist/daemon-connection.js +28 -0
  12. package/dist/diagnostic-logs.d.ts +10 -0
  13. package/dist/diagnostic-logs.js +121 -0
  14. package/dist/employee-text.js +1 -3
  15. package/dist/git-quick-commit.d.ts +1 -1
  16. package/dist/git-quick-commit.js +2 -9
  17. package/dist/layout-tree.d.ts +3 -1
  18. package/dist/layout-tree.js +8 -0
  19. package/dist/local-model-process.js +20 -0
  20. package/dist/local-model-setup.js +5 -3
  21. package/dist/managed-temp.d.ts +32 -0
  22. package/dist/managed-temp.js +176 -0
  23. package/dist/middleware/api-scope.js +1 -0
  24. package/dist/models/shared.d.ts +0 -6
  25. package/dist/models/shared.js +0 -12
  26. package/dist/models/thinking.d.ts +0 -2
  27. package/dist/models/thinking.js +0 -2
  28. package/dist/models.d.ts +0 -1
  29. package/dist/models.js +3 -11
  30. package/dist/native-session-title.d.ts +1 -1
  31. package/dist/native-session-title.js +1 -1
  32. package/dist/pi-model-discovery.js +5 -17
  33. package/dist/pi-resource-recommendation.d.ts +1 -1
  34. package/dist/pi-resource-recommendation.js +129 -42
  35. package/dist/process-manager-native-history.js +1 -6
  36. package/dist/process-manager.d.ts +0 -1
  37. package/dist/process-manager.js +5 -18
  38. package/dist/provider-catalog.d.ts +1 -2
  39. package/dist/provider-catalog.js +1 -6
  40. package/dist/provider-cli-updater.js +1 -9
  41. package/dist/provider-history-scanner.d.ts +0 -3
  42. package/dist/provider-history-scanner.js +0 -26
  43. package/dist/render-daemon-client.js +6 -29
  44. package/dist/render-structured-client.js +2 -7
  45. package/dist/retention.js +1 -6
  46. package/dist/run-command.d.ts +5 -0
  47. package/dist/run-command.js +14 -1
  48. package/dist/server-session-lifecycle-routes.js +1 -1
  49. package/dist/server-storage-cleanup-routes.d.ts +3 -0
  50. package/dist/server-storage-cleanup-routes.js +19 -0
  51. package/dist/server-workspace-routes.d.ts +4 -1
  52. package/dist/server-workspace-routes.js +75 -9
  53. package/dist/server.js +21 -16
  54. package/dist/service-command-privilege.d.ts +17 -0
  55. package/dist/service-command-privilege.js +55 -0
  56. package/dist/service-manager/index.d.ts +3 -0
  57. package/dist/service-manager/index.js +3 -0
  58. package/dist/service-manager/launchd.d.ts +1 -0
  59. package/dist/service-manager/launchd.js +15 -0
  60. package/dist/service-manager/systemd.d.ts +1 -0
  61. package/dist/service-manager/systemd.js +11 -0
  62. package/dist/session-ai-context.js +17 -6
  63. package/dist/session-completion-state.d.ts +2 -0
  64. package/dist/session-completion-state.js +6 -2
  65. package/dist/session-logger.js +7 -1
  66. package/dist/session-model.d.ts +4 -0
  67. package/dist/session-model.js +13 -4
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/session-registry.js +0 -3
  70. package/dist/silicon-employee-draft.js +0 -1
  71. package/dist/speech-service.js +18 -8
  72. package/dist/storage/mappers.js +6 -10
  73. package/dist/storage/migrations.js +14 -0
  74. package/dist/storage/uploads-repository.d.ts +36 -0
  75. package/dist/storage/uploads-repository.js +98 -0
  76. package/dist/storage-cleanup.d.ts +50 -0
  77. package/dist/storage-cleanup.js +344 -0
  78. package/dist/storage.d.ts +3 -1
  79. package/dist/storage.js +4 -0
  80. package/dist/structured-failure.d.ts +2 -0
  81. package/dist/structured-failure.js +9 -0
  82. package/dist/structured-grok-adapter.js +4 -4
  83. package/dist/structured-opencode-adapter.js +4 -9
  84. package/dist/structured-provider-common.d.ts +1 -2
  85. package/dist/structured-provider-common.js +3 -6
  86. package/dist/structured-session-manager-replay.js +2 -6
  87. package/dist/structured-session-manager.d.ts +1 -2
  88. package/dist/structured-session-manager.js +32 -25
  89. package/dist/task-organize-contract.d.ts +41 -0
  90. package/dist/task-organize-contract.js +4 -0
  91. package/dist/task-title-organize-service.d.ts +21 -0
  92. package/dist/task-title-organize-service.js +168 -0
  93. package/dist/task-title-organize.d.ts +77 -0
  94. package/dist/task-title-organize.js +346 -0
  95. package/dist/terminal-daemon-client.js +10 -48
  96. package/dist/time-utils.d.ts +2 -0
  97. package/dist/time-utils.js +7 -0
  98. package/dist/tui/attach.js +6 -5
  99. package/dist/tui/commands.d.ts +4 -1
  100. package/dist/tui/commands.js +36 -25
  101. package/dist/tui/index.js +2 -2
  102. package/dist/tui/service-actions.js +5 -4
  103. package/dist/tui/service-command.d.ts +6 -0
  104. package/dist/tui/service-command.js +40 -0
  105. package/dist/tui/service-panel.js +9 -8
  106. package/dist/types.d.ts +4 -4
  107. package/dist/upload-reference-guard.d.ts +4 -0
  108. package/dist/upload-reference-guard.js +29 -0
  109. package/dist/upload-routes.d.ts +4 -0
  110. package/dist/upload-routes.js +121 -21
  111. package/dist/user-profile.d.ts +1 -1
  112. package/dist/user-profile.js +8 -3
  113. package/dist/web-ui/content/ai-teams.js +9 -9
  114. package/dist/web-ui/content/module-manifest.json +1 -1
  115. package/dist/web-ui/content/modules/{2548dbe0d0783577/chat-render-EODYMQF6.js → 0051bd3d55f10ab7/chat-render-RY2I2CF3.js} +1 -1
  116. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-ZTEQSXPA.js → 0051bd3d55f10ab7/chunk-2F6J2MKX.js} +1 -1
  117. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-A33YPCY6.js → 0051bd3d55f10ab7/chunk-7AFM245W.js} +1 -1
  118. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-QNUWUMKW.js → 0051bd3d55f10ab7/chunk-MHX2QXIS.js} +1 -1
  119. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-WGADUEIT.js → 0051bd3d55f10ab7/chunk-RAAPETNH.js} +1 -1
  120. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-I3UYDQLS.js → 0051bd3d55f10ab7/chunk-RXKBHYUR.js} +2 -2
  121. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-3TCUG766.js → 0051bd3d55f10ab7/chunk-U4EDP4TI.js} +1 -1
  122. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-HK7RBU3M.js → 0051bd3d55f10ab7/chunk-WHXQR7JT.js} +1 -1
  123. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +1 -0
  124. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-2PU6TV2I.js → 0051bd3d55f10ab7/host-2IVIZ2YC.js} +1 -1
  125. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-BPYNMKFC.js → 0051bd3d55f10ab7/host-AZXNFCRW.js} +1 -1
  126. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HZIKPWYY.js → 0051bd3d55f10ab7/host-EPEQX22R.js} +1 -1
  127. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-YELXZID4.js → 0051bd3d55f10ab7/host-FZYIMOLC.js} +1 -1
  128. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OGD6AWHL.js → 0051bd3d55f10ab7/host-G6GL4SNL.js} +1 -1
  129. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-E3SCPOIU.js → 0051bd3d55f10ab7/host-PEILC5NU.js} +1 -1
  130. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OWD32FZ3.js → 0051bd3d55f10ab7/host-PHHNGVNL.js} +2 -2
  131. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-CXDAFMXX.js → 0051bd3d55f10ab7/host-Q524TMSR.js} +1 -1
  132. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +69 -0
  133. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J5XRAQFP.js → 0051bd3d55f10ab7/host-WOQRK6NG.js} +1 -1
  134. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HKXE2DOT.js → 0051bd3d55f10ab7/host-XNIR3NTJ.js} +1 -1
  135. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-4E4H4JZJ.js → 0051bd3d55f10ab7/host-XRVOIEP6.js} +1 -1
  136. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +1 -0
  137. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +1 -0
  138. package/dist/web-ui/content/modules/{2548dbe0d0783577/library-bridge-Y3Y24CPQ.js → 0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js} +1 -1
  139. package/dist/web-ui/content/modules/{2548dbe0d0783577/page-GQBUU7T7.js → 0051bd3d55f10ab7/page-PY4JUL4W.js} +1 -1
  140. package/dist/web-ui/content/modules/{2548dbe0d0783577/panel-PG662OC5.js → 0051bd3d55f10ab7/panel-BBQFC55Q.js} +1 -1
  141. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +974 -0
  142. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +2 -0
  143. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +1 -0
  144. package/dist/web-ui/content/modules/{2548dbe0d0783577/workspace-window-HV4T3WKR.js → 0051bd3d55f10ab7/workspace-window-MNMKSOBG.js} +1 -1
  145. package/dist/web-ui/content/styles.css +1 -1
  146. package/dist/web-ui/embedded-assets.d.ts +27 -27
  147. package/dist/web-ui/embedded-assets.js +4 -4
  148. package/dist/web-ui/provider-identity.d.ts +4 -5
  149. package/dist/web-ui/provider-identity.js +4 -10
  150. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +39 -0
  151. package/dist/web-ui/react/workspaces/sidebar-manage.js +149 -0
  152. package/dist/web-ui/react/workspaces/types.d.ts +408 -0
  153. package/dist/web-ui/react/workspaces/types.js +16 -0
  154. package/dist/web-ui/session-activity.js +1 -1
  155. package/package.json +2 -2
  156. package/dist/structured-gemini-adapter.d.ts +0 -40
  157. package/dist/structured-gemini-adapter.js +0 -208
  158. package/dist/web-ui/content/modules/2548dbe0d0783577/chunk-XGVT4ZWT.js +0 -1
  159. package/dist/web-ui/content/modules/2548dbe0d0783577/employee-card-LJ3TX72B.js +0 -1
  160. package/dist/web-ui/content/modules/2548dbe0d0783577/host-ITV2QZ4A.js +0 -1
  161. package/dist/web-ui/content/modules/2548dbe0d0783577/host-XNWC7KPR.js +0 -69
  162. package/dist/web-ui/content/modules/2548dbe0d0783577/host-bridge-G5HEVADT.js +0 -1
  163. package/dist/web-ui/content/modules/2548dbe0d0783577/runtime.js +0 -946
  164. package/dist/web-ui/content/modules/2548dbe0d0783577/task-board-host-RAN2Z2KR.js +0 -2
  165. package/dist/web-ui/content/modules/2548dbe0d0783577/workspace-agent-picker-FXLH6N5K.js +0 -1
  166. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-YX475LXO.js → 0051bd3d55f10ab7/chunk-6C6A2RHG.js} +0 -0
  167. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-NP35GDQ3.js → 0051bd3d55f10ab7/chunk-ECWYA3U5.js} +0 -0
  168. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-47EVOSJS.js → 0051bd3d55f10ab7/chunk-T35D57FW.js} +0 -0
  169. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/chunk-YOXXNXOX.js +0 -0
  170. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-C3K34WJD.js → 0051bd3d55f10ab7/chunk-YQJ6XYV7.js} +0 -0
  171. /package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J6HBDK4A.js → 0051bd3d55f10ab7/host-2AQAIUNO.js} +0 -0
  172. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/sha256-ANXV72UC.js +0 -0
package/dist/tui/index.js CHANGED
@@ -110,9 +110,9 @@ export function startTui(deps) {
110
110
  layout,
111
111
  updateConfirmBody: ({ current, latest, channelLabel }) => `通道 ${channelLabel}:当前 ${current} → 最新 ${latest},是否立即升级?`,
112
112
  installServiceBody: (isRoot) => process.platform === "linux"
113
- ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}\n不想要 root?退出 TUI 跑 wand service:install --user (登出会被回收)。`
113
+ ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "需要管理员权限,将自动申请 sudo,请在终端输入密码。"}\n不想要 root?退出 TUI 跑 wand service:install --user (登出会被回收)。`
114
114
  : process.platform === "darwin"
115
- ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}`
115
+ ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "需要管理员权限,将自动申请 sudo,请在终端输入密码。"}`
116
116
  : "当前平台暂不支持。",
117
117
  onServiceChanged: refreshAll,
118
118
  });
@@ -5,9 +5,10 @@
5
5
  * - 升级确认弹窗文案(attach 模式要提醒按 R 重启服务)
6
6
  * - 注册系统服务的说明正文(attach 模式没有本地会话列表,装卸后不刷新界面)
7
7
  */
8
- import { checkUpdate, installService, installUpdate, isServiceInstalled, readUpdateChannel, uninstallService, } from "./commands.js";
8
+ import { checkUpdate, installUpdate, isServiceInstalled, readUpdateChannel, } from "./commands.js";
9
9
  import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
10
10
  import { runOffMicrotask } from "./runtime-utils.js";
11
+ import { runTuiServiceCommand } from "./service-command.js";
11
12
  export function createServiceActions(options) {
12
13
  const { layout } = options;
13
14
  async function handleUpdate() {
@@ -51,7 +52,7 @@ export function createServiceActions(options) {
51
52
  layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
52
53
  return;
53
54
  }
54
- // 默认装 system-wide。非 root 时 installService 会返回明确错误,toast 自然展示。
55
+ // 默认装 system-wide;确认后让独立 CLI 自动申请 sudo。
55
56
  const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
56
57
  const ok = await layout.confirm({
57
58
  title: "注册为系统服务",
@@ -59,7 +60,7 @@ export function createServiceActions(options) {
59
60
  });
60
61
  if (!ok)
61
62
  return;
62
- const r = await runOffMicrotask(() => installService({ configPath: options.configPath }));
63
+ const r = await runTuiServiceCommand(layout, "service:install", options.configPath);
63
64
  layout.showToast(r.message, r.ok ? "success" : "error", 5000);
64
65
  if (r.detail)
65
66
  layout.showDetail(r.ok ? "服务安装详情" : "服务安装失败", r.detail);
@@ -76,7 +77,7 @@ export function createServiceActions(options) {
76
77
  });
77
78
  if (!ok)
78
79
  return;
79
- const r = await runOffMicrotask(() => uninstallService());
80
+ const r = await runTuiServiceCommand(layout, "service:uninstall", options.configPath);
80
81
  layout.showToast(r.message, r.ok ? "success" : "error", 4000);
81
82
  if (r.detail)
82
83
  layout.showDetail(r.ok ? "服务卸载详情" : "服务卸载失败", r.detail);
@@ -0,0 +1,6 @@
1
+ import type { CommandResult } from "./commands.js";
2
+ import type { LayoutHandle } from "./layout.js";
3
+ /** Give sudo the real terminal, never route password keystrokes through TUI.
4
+ * Run a separate CLI asynchronously so an embedded server can still drain Core.
5
+ */
6
+ export declare function runTuiServiceCommand(layout: LayoutHandle, command: string, configPath: string): Promise<CommandResult>;
@@ -0,0 +1,40 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { runCommandInteractive } from "../run-command.js";
4
+ import { getErrorMessage } from "../error-utils.js";
5
+ const activeActions = new WeakSet();
6
+ /** Give sudo the real terminal, never route password keystrokes through TUI.
7
+ * Run a separate CLI asynchronously so an embedded server can still drain Core.
8
+ */
9
+ export async function runTuiServiceCommand(layout, command, configPath) {
10
+ if (activeActions.has(layout))
11
+ return { ok: false, message: "服务操作正在进行,请等待完成。" };
12
+ activeActions.add(layout);
13
+ let resume;
14
+ // Ctrl+C in cooked mode reaches the foreground process group, including
15
+ // sudo. Keep blessed from exiting the parent TUI while the child cancels.
16
+ const onInterrupt = () => { };
17
+ process.on("SIGINT", onInterrupt);
18
+ try {
19
+ resume = layout.screen.program.pause();
20
+ const compiled = fileURLToPath(new URL("../cli.js", import.meta.url));
21
+ const cli = existsSync(compiled) ? compiled : fileURLToPath(new URL("../cli.ts", import.meta.url));
22
+ const code = await runCommandInteractive(process.execPath, [
23
+ ...process.execArgv, cli, command, "-c", configPath, "--verbose",
24
+ ]);
25
+ return { ok: code === 0, message: code === 0 ? `${command} 已完成` : `${command} 未完成 (exit ${code})` };
26
+ }
27
+ catch (error) {
28
+ return { ok: false, message: "服务操作失败", detail: getErrorMessage(error) };
29
+ }
30
+ finally {
31
+ process.off("SIGINT", onInterrupt);
32
+ try {
33
+ resume?.();
34
+ }
35
+ finally {
36
+ activeActions.delete(layout);
37
+ layout.screen.render();
38
+ }
39
+ }
40
+ }
@@ -11,7 +11,8 @@
11
11
  * l — 查看最近日志
12
12
  * Esc / q — 关闭面板
13
13
  */
14
- import { installService, isServiceInstalled, serviceLogs, serviceRestart, serviceStart, serviceStatusAsync, serviceStop, uninstallService, } from "./commands.js";
14
+ import { isServiceInstalled, serviceLogs, serviceStatusAsync, } from "./commands.js";
15
+ import { runTuiServiceCommand } from "./service-command.js";
15
16
  const panelOpenings = new WeakMap();
16
17
  export function openServicePanel(deps) {
17
18
  const { layout, configPath } = deps;
@@ -49,8 +50,8 @@ export function openServicePanel(deps) {
49
50
  setLastAction(`${label}: ${result.message}`);
50
51
  }
51
52
  layout.openServicePanel({
52
- onStart: () => {
53
- handleResult("start", serviceStart());
53
+ onStart: async () => {
54
+ handleResult("start", await runTuiServiceCommand(layout, "service:start", configPath));
54
55
  },
55
56
  onStop: async () => {
56
57
  const ok = await layout.confirm({
@@ -59,7 +60,7 @@ export function openServicePanel(deps) {
59
60
  });
60
61
  if (!ok)
61
62
  return;
62
- handleResult("stop", serviceStop());
63
+ handleResult("stop", await runTuiServiceCommand(layout, "service:stop", configPath));
63
64
  },
64
65
  onRestart: async () => {
65
66
  const ok = await layout.confirm({
@@ -68,7 +69,7 @@ export function openServicePanel(deps) {
68
69
  });
69
70
  if (!ok)
70
71
  return;
71
- handleResult("restart", serviceRestart());
72
+ handleResult("restart", await runTuiServiceCommand(layout, "service:restart", configPath));
72
73
  },
73
74
  onInstall: async () => {
74
75
  if (isServiceInstalled()) {
@@ -76,11 +77,11 @@ export function openServicePanel(deps) {
76
77
  return;
77
78
  }
78
79
  const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
79
- const body = `默认装 system-wide(/etc/systemd/system/ 或 /Library/LaunchDaemons/),开机自启、登出不停。${isRoot ? "" : "\n⚠ 需要 root,如果失败请退出 TUI 跑 sudo wand service:install。"}`;
80
+ const body = `默认装 system-wide(/etc/systemd/system/ 或 /Library/LaunchDaemons/),开机自启、登出不停。${isRoot ? "" : "\n需要管理员权限,将自动申请 sudo,请在终端输入密码。"}`;
80
81
  const ok = await layout.confirm({ title: "注册服务", body });
81
82
  if (!ok)
82
83
  return;
83
- handleResult("install", installService({ configPath }));
84
+ handleResult("install", await runTuiServiceCommand(layout, "service:install", configPath));
84
85
  },
85
86
  onUninstall: async () => {
86
87
  if (!isServiceInstalled()) {
@@ -93,7 +94,7 @@ export function openServicePanel(deps) {
93
94
  });
94
95
  if (!ok)
95
96
  return;
96
- handleResult("uninstall", uninstallService());
97
+ handleResult("uninstall", await runTuiServiceCommand(layout, "service:uninstall", configPath));
97
98
  },
98
99
  onLogs: () => {
99
100
  const r = serviceLogs(80);
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@ export type SessionKind = "pty" | "structured";
3
3
  export type { SessionProvider } from "./provider-catalog.js";
4
4
  import type { SessionProvider } from "./provider-catalog.js";
5
5
  /** "claude-sdk" 是历史值:SDK 执行路径已移除,读旧会话时归一为 claude-cli-print。 */
6
- export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "qoder-cli-print" | "pi-cli-json" | "gemini-cli-json" | "pty";
6
+ export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "qoder-cli-print" | "pi-cli-json" | "pty";
7
7
  export type SessionSource = "interactive" | "automation" | "startup";
8
8
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
9
9
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -115,6 +115,8 @@ export interface IosIpaConfig {
115
115
  currentIpaFile?: string;
116
116
  }
117
117
  export interface WandConfig {
118
+ /** Background maintenance for owned temporary files, orphan uploads and diagnostic archives. Deployment-owned. */
119
+ autoCleanupTemporaryFiles?: boolean;
118
120
  host: string;
119
121
  port: number;
120
122
  /** Enable HTTPS with self-signed certificate (default: false) */
@@ -203,8 +205,6 @@ export interface WandConfig {
203
205
  defaultQoderModel?: string;
204
206
  /** 新建 Pi 会话时默认使用的 provider/model pattern。 */
205
207
  defaultPiModel?: string;
206
- /** 新建 Gemini 会话时默认使用的模型。留空则不传 --model,由 gemini 自行决定。 */
207
- defaultGeminiModel?: string;
208
208
  /** 每工具的命名模型优先链。SQLite 偏好,稳定分组 ID 可用于会话/员工/默认值。 */
209
209
  modelGroups?: import("./model-groups.js").ModelGroup[];
210
210
  /** 快捷提交生成 commit message / tag 时使用的 CLI。 */
@@ -282,7 +282,7 @@ interface ReasoningEffortInfo {
282
282
  * 避免 `max`(旧值,各家含义不同)和 CLI 新增的原生档位冲突。
283
283
  */
284
284
  export type { SiliconEmployee } from "./ai-team-types.js";
285
- export type ThinkingEffort = "off" | "standard" | "deep" | "max" | `${"claude" | "codex" | "opencode" | "grok" | "qoder" | "pi" | "gemini"}:${string}`;
285
+ export type ThinkingEffort = "off" | "standard" | "deep" | "max" | `${"claude" | "codex" | "opencode" | "grok" | "qoder" | "pi"}:${string}`;
286
286
  export interface WorktreeInfo {
287
287
  branch: string;
288
288
  path: string;
@@ -0,0 +1,4 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { UploadRepository } from "./storage/uploads-repository.js";
3
+ /** Runs after authentication, before either input owner starts dispatch. */
4
+ export declare function uploadReferenceGuard(uploads: Pick<UploadRepository, "retainManagedUploadReferences">): RequestHandler;
@@ -0,0 +1,29 @@
1
+ /** Runs after authentication, before either input owner starts dispatch. */
2
+ export function uploadReferenceGuard(uploads) {
3
+ return (req, _res, next) => {
4
+ if (req.method !== "POST" || !req.body) {
5
+ next();
6
+ return;
7
+ }
8
+ const match = req.path.match(/^\/(?:sessions|structured-sessions)\/([^/]+)\/(?:input|messages)$/);
9
+ if (!match) {
10
+ next();
11
+ return;
12
+ }
13
+ let sessionId;
14
+ try {
15
+ sessionId = decodeURIComponent(match[1]);
16
+ }
17
+ catch {
18
+ next();
19
+ return;
20
+ }
21
+ try {
22
+ uploads.retainManagedUploadReferences(sessionId, JSON.stringify(req.body));
23
+ next();
24
+ }
25
+ catch (error) {
26
+ next(error);
27
+ }
28
+ };
29
+ }
@@ -1,7 +1,11 @@
1
1
  import type { Express } from "express";
2
2
  import type { SessionRegistry } from "./session-registry.js";
3
+ import type { UploadRepository } from "./storage/uploads-repository.js";
4
+ export declare function invalidateUploadUsage(cwd: string): void;
3
5
  export interface UploadRouteOptions {
4
6
  /** Test seam: per-directory byte ceiling. */
5
7
  maxDirBytes?: number;
8
+ uploads?: Pick<UploadRepository, "recordManagedUploads">;
9
+ temporaryRoot?: string;
6
10
  }
7
11
  export declare function registerUploadRoutes(app: Express, sessions: Pick<SessionRegistry, "getLatest">, options?: UploadRouteOptions): void;
@@ -1,9 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { readdir, stat, unlink, mkdir } from "node:fs/promises";
2
+ import { readdir, stat, lstat, realpath, unlink, mkdir, open } from "node:fs/promises";
3
+ import { createReadStream, createWriteStream } from "node:fs";
4
+ import { pipeline } from "node:stream/promises";
3
5
  import path from "node:path";
4
6
  import multer, { MulterError } from "multer";
5
7
  import { asyncRoute } from "./express-async.js";
6
8
  import { HttpError } from "./http-error.js";
9
+ import { assertPlainTempPath, createManagedTemporary } from "./managed-temp.js";
7
10
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
8
11
  const MAX_FILES = 5;
9
12
  /** Per working directory ceiling for everything ever uploaded there (in-process accounting). */
@@ -18,6 +21,19 @@ function sanitizeFilename(name) {
18
21
  * needs to grow; deleting files outside the server simply makes the cap conservative.
19
22
  */
20
23
  const uploadUsage = new Map();
24
+ export function invalidateUploadUsage(cwd) { uploadUsage.delete(path.join(cwd, UPLOAD_DIR_NAME)); }
25
+ async function reserveUploadUsage(dir, incoming, maximum) {
26
+ for (;;) {
27
+ const pending = seedUploadUsage(dir);
28
+ const current = await pending;
29
+ if (uploadUsage.get(dir) !== pending)
30
+ continue;
31
+ if (current + incoming > maximum)
32
+ return false;
33
+ uploadUsage.set(dir, Promise.resolve(current + incoming));
34
+ return true;
35
+ }
36
+ }
21
37
  function seedUploadUsage(dir) {
22
38
  let usage = uploadUsage.get(dir);
23
39
  if (!usage) {
@@ -52,6 +68,8 @@ async function removeFiles(paths) {
52
68
  }
53
69
  /** multer 的限额错误改成稳定的 413,其他错误原样交给统一错误处理。 */
54
70
  function uploadErrorHandler(error, _req, res, next) {
71
+ if (_req.aborted)
72
+ return;
55
73
  if (error instanceof MulterError && (error.code === "LIMIT_FILE_SIZE" || error.code === "LIMIT_FILE_COUNT" || error.code === "LIMIT_UNEXPECTED_FILE")) {
56
74
  next(new HttpError(413, "payload_too_large", error.code === "LIMIT_FILE_SIZE" ? "单个文件不能超过 10 MB。" : `一次最多上传 ${MAX_FILES} 个文件。`));
57
75
  return;
@@ -60,29 +78,64 @@ function uploadErrorHandler(error, _req, res, next) {
60
78
  }
61
79
  export function registerUploadRoutes(app, sessions, options = {}) {
62
80
  const maxDirBytes = options.maxDirBytes ?? MAX_UPLOAD_DIR_BYTES;
63
- const storage = multer.diskStorage({
64
- destination(req, _file, cb) {
65
- const cwd = req.uploadCwd;
66
- if (!cwd) {
67
- cb(new Error("会话不存在。"), "");
68
- return;
69
- }
70
- const uploadDir = path.join(cwd, UPLOAD_DIR_NAME);
71
- // Async mkdir keeps the event loop free while a large upload is being accepted.
72
- mkdir(uploadDir, { recursive: true }).then(() => cb(null, uploadDir), (error) => cb(error instanceof Error ? error : new Error(String(error)), ""));
73
- },
74
- filename(_req, file, cb) {
75
- const ts = Date.now();
76
- const rand = randomBytes(4).toString("hex");
77
- const safe = sanitizeFilename(file.originalname);
78
- cb(null, `${ts}-${rand}-${safe}`);
81
+ const storage = {
82
+ _handleFile(req, file, cb) {
83
+ void (async () => {
84
+ const request = req;
85
+ const cwd = request.uploadCwd;
86
+ if (!cwd)
87
+ throw new Error("会话不存在。");
88
+ const uploadDir = path.join(await realpath(cwd), UPLOAD_DIR_NAME);
89
+ assertPlainTempPath(uploadDir);
90
+ await mkdir(uploadDir, { recursive: true, mode: 0o700 });
91
+ assertPlainTempPath(uploadDir);
92
+ if (request.uploadClosed || req.aborted)
93
+ throw new Error("Upload closed before receiving files.");
94
+ if (options.temporaryRoot)
95
+ request.uploadTemporary ??= createManagedTemporary("upload-stage", options.temporaryRoot);
96
+ const destination = request.uploadTemporary?.directory ?? path.join(cwd, UPLOAD_DIR_NAME);
97
+ const filename = `${Date.now()}-${randomBytes(4).toString("hex")}-${sanitizeFilename(file.originalname)}`;
98
+ const filePath = path.join(destination, filename);
99
+ const output = createWriteStream(filePath, { flags: "wx", mode: 0o600 });
100
+ const writers = request.uploadWriters ??= new Set();
101
+ writers.add(output);
102
+ let opened = false;
103
+ output.once("open", () => { opened = true; });
104
+ output.once("close", () => writers.delete(output));
105
+ try {
106
+ // pipeline closes the disk descriptor when the multipart source fails or disconnects.
107
+ await pipeline(file.stream, output);
108
+ }
109
+ catch (error) {
110
+ if (opened)
111
+ await unlink(filePath).catch(() => undefined);
112
+ throw error;
113
+ }
114
+ return { destination, filename, path: filePath, size: output.bytesWritten };
115
+ })().then(info => cb(null, info), error => cb(error instanceof Error ? error : new Error(String(error))));
79
116
  },
80
- });
117
+ _removeFile(_req, file, cb) { unlink(file.path).then(() => cb(null), error => cb(error.code === "ENOENT" ? null : error)); },
118
+ };
81
119
  const upload = multer({
82
120
  storage,
83
121
  limits: { fileSize: MAX_FILE_SIZE, files: MAX_FILES },
84
122
  });
85
123
  function requireUploadSession(req, res, next) {
124
+ const discardStage = () => {
125
+ const request = req;
126
+ if (request.uploadClosed)
127
+ return;
128
+ request.uploadClosed = true;
129
+ const closed = [...(request.uploadWriters ?? [])].map(writer => new Promise(resolve => {
130
+ writer.once("close", resolve);
131
+ writer.destroy();
132
+ }));
133
+ // Wait for descriptors to close before unlinking; deleted-but-open files still consume disk.
134
+ void Promise.all(closed).then(() => request.uploadTemporary?.closeAsync());
135
+ };
136
+ res.once("finish", discardStage);
137
+ res.once("close", discardStage);
138
+ req.once("aborted", discardStage);
86
139
  const sessionId = req.params.id;
87
140
  // Group chats are structured relay sessions; ProcessManager does not own them.
88
141
  // Resolve through the same registry used by the rest of the session API so
@@ -108,12 +161,59 @@ export function registerUploadRoutes(app, sessions, options = {}) {
108
161
  }
109
162
  const dir = path.join(req.uploadCwd, UPLOAD_DIR_NAME);
110
163
  const incoming = files.reduce((total, file) => total + file.size, 0);
111
- const current = await seedUploadUsage(dir);
112
- if (current + incoming > maxDirBytes) {
164
+ if (!await reserveUploadUsage(dir, incoming, maxDirBytes)) {
113
165
  await removeFiles(files.map((file) => file.path));
114
166
  throw new HttpError(413, "payload_too_large", "该会话的上传空间已满,请清理 .wand-uploads 目录后重试。");
115
167
  }
116
- uploadUsage.set(dir, Promise.resolve(current + incoming));
168
+ if (req.uploadTemporary) {
169
+ const promoted = [];
170
+ try {
171
+ const targetDir = path.join(await realpath(req.uploadCwd), UPLOAD_DIR_NAME);
172
+ assertPlainTempPath(targetDir);
173
+ for (const file of files) {
174
+ const destination = path.join(targetDir, path.basename(file.path));
175
+ // Exclusive creation never overwrites a collision or user-created redirect.
176
+ const output = await open(destination, "wx", 0o600);
177
+ promoted.push(destination);
178
+ try {
179
+ for await (const chunk of createReadStream(file.path))
180
+ await output.writeFile(chunk);
181
+ }
182
+ finally {
183
+ await output.close();
184
+ }
185
+ await unlink(file.path);
186
+ file.path = path.join(req.uploadCwd, UPLOAD_DIR_NAME, path.basename(destination));
187
+ }
188
+ }
189
+ catch (error) {
190
+ await removeFiles(promoted);
191
+ invalidateUploadUsage(req.uploadCwd);
192
+ throw error;
193
+ }
194
+ }
195
+ if (options.uploads) {
196
+ try {
197
+ const canonicalCwd = await realpath(req.uploadCwd);
198
+ const receipts = [];
199
+ for (const file of files) {
200
+ const canonicalPath = path.join(canonicalCwd, UPLOAD_DIR_NAME, path.basename(file.path));
201
+ assertPlainTempPath(canonicalPath);
202
+ const info = await lstat(canonicalPath);
203
+ if (!info.isFile())
204
+ throw new Error("Upload is not a regular file.");
205
+ receipts.push({ filePath: file.path, canonicalPath, cwd: req.uploadCwd,
206
+ canonicalCwd, sessionId: req.params.id, device: info.dev, inode: info.ino, size: info.size,
207
+ mtimeMs: info.mtimeMs, ctimeMs: info.ctimeMs, createdAt: Date.now() });
208
+ }
209
+ options.uploads.recordManagedUploads(receipts);
210
+ }
211
+ catch (error) {
212
+ await removeFiles(files.map(file => file.path));
213
+ invalidateUploadUsage(req.uploadCwd);
214
+ throw error;
215
+ }
216
+ }
117
217
  const result = files.map((f) => ({
118
218
  originalName: f.originalname,
119
219
  savedPath: f.path,
@@ -8,7 +8,7 @@ import type { ConversationAuthor, WandConfig } from "./types.js";
8
8
  export interface UserProfileConfig {
9
9
  /** 会话里的署名。留空回落 `DEFAULT_USER_DISPLAY_NAME`,不做多套默认。 */
10
10
  name?: string;
11
- /** `""`(按名字生成)、`"cat:<n>"`(像素猫毛色)或 `data:image/*;base64,…`(上传图)。 */
11
+ /** 与员工共用头像格式;空值按稳定 user ID 生成,保留旧像素猫与上传图。 */
12
12
  avatar?: string;
13
13
  }
14
14
  /** 未设置资料时的唯一默认署名。历史数据、客户端兜底与文案都用这一个值。 */
@@ -1,3 +1,4 @@
1
+ import { parsePlushAvatar, parsePlushCatAvatar, parsePlushDogAvatar } from "./plush-avatar.js";
1
2
  /** 未设置资料时的唯一默认署名。历史数据、客户端兜底与文案都用这一个值。 */
2
3
  export const DEFAULT_USER_DISPLAY_NAME = "我";
3
4
  /** 与硅基员工头像同一条白名单与上限,避免两套大小口径。 */
@@ -8,10 +9,14 @@ export const USER_PROFILE_NAME_MAX = 24;
8
9
  function clean(value) {
9
10
  return typeof value === "string" ? value.trim() : "";
10
11
  }
11
- /** 只接受像素猫毛色与白名单 data URL;其他写法当没配,不猜也不静默改写。 */
12
+ function isAvatar(avatar) {
13
+ return AVATAR_CAT.test(avatar) || AVATAR_DATA_URL.test(avatar)
14
+ || !!(parsePlushAvatar(avatar) ?? parsePlushCatAvatar(avatar) ?? parsePlushDogAvatar(avatar));
15
+ }
16
+ /** 与员工共享版本化毛绒格式;仍拒绝外部 URL 和未知版本。 */
12
17
  function normalizeAvatar(value) {
13
18
  const avatar = clean(value);
14
- if (!avatar || AVATAR_CAT.test(avatar) || (avatar.length <= USER_AVATAR_MAX_CHARS && AVATAR_DATA_URL.test(avatar))) {
19
+ if (!avatar || (avatar.length <= USER_AVATAR_MAX_CHARS && isAvatar(avatar))) {
15
20
  return avatar;
16
21
  }
17
22
  return "";
@@ -41,7 +46,7 @@ export function parseUserProfile(input) {
41
46
  const avatarInput = raw.avatar;
42
47
  const avatar = clean(avatarInput);
43
48
  if (avatar) {
44
- if (!AVATAR_CAT.test(avatar) && !AVATAR_DATA_URL.test(avatar)) {
49
+ if (!isAvatar(avatar)) {
45
50
  throw new Error("头像格式无效,请重新选择或上传图片。");
46
51
  }
47
52
  if (avatar.length > USER_AVATAR_MAX_CHARS)