zen-gitsync 2.17.5 → 2.17.7

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 (71) hide show
  1. package/README.md +35 -2
  2. package/package.json +1 -1
  3. package/scripts/release.js +139 -48
  4. package/src/aiCommit.js +6 -3
  5. package/src/cli/ai/agent.js +106 -311
  6. package/src/cli/ai/cli.integration.test.js +99 -0
  7. package/src/cli/ai/context.js +90 -0
  8. package/src/cli/ai/modelSetup.js +44 -8
  9. package/src/cli/ai/runtime.test.js +152 -0
  10. package/src/cli/ai/safety.js +27 -4
  11. package/src/cli/ai/safety.test.js +13 -0
  12. package/src/cli/ai/telemetry.js +43 -0
  13. package/src/cli/ai/termui.js +154 -41
  14. package/src/cli/ai/termui.test.js +63 -14
  15. package/src/cli/ai/tools.js +27 -5
  16. package/src/cli/ai/tools.test.js +23 -0
  17. package/src/cli/ai/transport.js +89 -0
  18. package/src/cli/ai/turn.js +126 -0
  19. package/src/config.js +88 -5
  20. package/src/ui/public/assets/{AgentView-DJOdvJEV.js → AgentView-CZc191Xm.js} +3 -3
  21. package/src/ui/public/assets/AgentView-CzUeyQfR.css +1 -0
  22. package/src/ui/public/assets/{AppVersionBadge-D2Li5_R-.js → AppVersionBadge-Gs5lUl7P.js} +2 -2
  23. package/src/ui/public/assets/{BranchSelector-CSd5uaVm.js → BranchSelector-IjaFk0S-.js} +1 -1
  24. package/src/ui/public/assets/{CommitForm-Ckg9-zay.js → CommitForm-C-sowOXH.js} +3 -3
  25. package/src/ui/public/assets/{CommonDialog-B8XOXtz_.js → CommonDialog-QyptFD1R.js} +1 -1
  26. package/src/ui/public/assets/{EditorView-Crj8S2TI.js → EditorView-CvvluFCY.js} +1 -1
  27. package/src/ui/public/assets/{FileDiffViewer-DL07IMz5.js → FileDiffViewer-BXVsdCxE.js} +1 -1
  28. package/src/ui/public/assets/{FlowExecutionViewer-DvVT19Qv.js → FlowExecutionViewer-CRxipXru.js} +1 -1
  29. package/src/ui/public/assets/{FlowOrchestrationWorkspace-kRhK9IyL.js → FlowOrchestrationWorkspace-AE1ZHTKF.js} +1 -1
  30. package/src/ui/public/assets/{IconButton-pqSYKRT8.js → IconButton-BmSueF_H.js} +1 -1
  31. package/src/ui/public/assets/{LogList-BrN6qtFb.js → LogList-iaVA-_aC.js} +1 -1
  32. package/src/ui/public/assets/{MindmapView-CSsT0NDZ.js → MindmapView-DcG4jpCh.js} +1 -1
  33. package/src/ui/public/assets/{MonitorView-edRyBybQ.js → MonitorView-DO59syvl.js} +1 -1
  34. package/src/ui/public/assets/{ProjectStartupButton-DZ8VgU79.js → ProjectStartupButton-DYppceyw.js} +1 -1
  35. package/src/ui/public/assets/{RemoteManagerDialog-pu9szuYB.js → RemoteManagerDialog-CDbxG6pZ.js} +1 -1
  36. package/src/ui/public/assets/{RemoteRepoCard-BSFehgY5.js → RemoteRepoCard-Cko2KB8w.js} +1 -1
  37. package/src/ui/public/assets/{SourceMapView-DbEydLSU.js → SourceMapView-CwWPVdz-.js} +1 -1
  38. package/src/ui/public/assets/{SvgIcon-BVCbrPws.js → SvgIcon-Cf7nOKc8.js} +1 -1
  39. package/src/ui/public/assets/{TemplateManager-DPyYaGCE.js → TemplateManager-DWPDjWxM.js} +1 -1
  40. package/src/ui/public/assets/{UserInputNode-DXHeCjzG.js → UserInputNode-YOziN-5A.js} +1 -1
  41. package/src/ui/public/assets/WorkbenchView-DNnEpIYy.js +13 -0
  42. package/src/ui/public/assets/WorkbenchView-v3QFn6Ln.css +1 -0
  43. package/src/ui/public/assets/_plugin-vue_export-helper-CQjg8MXm.js +19 -0
  44. package/src/ui/public/assets/configStore-C7HOx5S1.js +1 -0
  45. package/src/ui/public/assets/{gitStore-BJbNtjxA.js → gitStore-DIsBd6n4.js} +1 -1
  46. package/src/ui/public/assets/index-FOe1cHkh.css +1 -0
  47. package/src/ui/public/assets/index-MmcnT2Dn.js +24 -0
  48. package/src/ui/public/assets/{previewSandbox-BZV6Eqhv.js → previewSandbox-TKucqLxt.js} +0 -0
  49. package/src/ui/public/assets/{toolsStore-Bu8Ujw_A.js → toolsStore-BiC2mmC3.js} +1 -1
  50. package/src/ui/public/assets/{vendor-CwZs6S_i.js → vendor-92fKgtnz.js} +7 -7
  51. package/src/ui/public/assets/{vendor-_6pgW9Z2.css → vendor-B1UrVTTI.css} +1 -1
  52. package/src/ui/public/index.html +14 -14
  53. package/src/ui/server/routes/codeAnalysis.js +6 -3
  54. package/src/ui/server/routes/config.js +40 -9
  55. package/src/ui/server/routes/fileOpen.js +48 -2
  56. package/src/ui/server/routes/gAiLauncher.test.js +70 -0
  57. package/src/ui/server/routes/workbench/agentChat.js +34 -8
  58. package/src/ui/server/routes/workbench/index.js +37 -6
  59. package/src/ui/server/routes/workbench/jobStore.js +4 -0
  60. package/src/ui/server/routes/workbench/llmClient.js +15 -8
  61. package/src/ui/server/routes/workbench/taskRunner.js +276 -96
  62. package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +126 -0
  63. package/src/utils/aiEndpoint.js +231 -0
  64. package/src/utils/aiEndpoint.test.js +175 -0
  65. package/src/ui/public/assets/AgentView-BsN9y4Kg.css +0 -1
  66. package/src/ui/public/assets/WorkbenchView-CKXtZAJn.css +0 -1
  67. package/src/ui/public/assets/WorkbenchView-DGcirla3.js +0 -13
  68. package/src/ui/public/assets/_plugin-vue_export-helper-C-YBnnP5.js +0 -19
  69. package/src/ui/public/assets/configStore-DoobmT7N.js +0 -1
  70. package/src/ui/public/assets/index-ClqvC4kJ.css +0 -1
  71. package/src/ui/public/assets/index-ndDaGoRO.js +0 -24
package/README.md CHANGED
@@ -384,6 +384,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
384
384
  | Streaming chat | SSE-based real-time streaming with thinking process, content, tool calls, and tool results rendered inline |
385
385
  | Tool call display | Each tool invocation (run_command, read_file, edit_file, list_files, search_text, write_file) is shown as a collapsible card with arguments preview and execution result |
386
386
  | Session persistence | All conversations are saved to `~/.zen-gitsync/agent-sessions/` as JSON files; the CLI agent (`g ai`) writes to the same directory so Web and CLI sessions are unified |
387
+ | Per-turn tool limit | A single message may trigger up to N tool calls in a row (default **200**, range 1–2000). Configurable in **Settings → AI models → Agent Runtime**; hitting the limit ends the turn and asks you to send another message. The same setting drives the CLI agent |
387
388
  | Preset questions | Quick-start buttons on the welcome screen for common tasks (view project structure, analyze code quality, write tests, check git status) |
388
389
  | Stop generation | A floating stop button appears during streaming; aborts the LLM request and any running child processes |
389
390
  | Theme sync | The chat area follows the GUI's current theme (light / dark / auto) |
@@ -452,7 +453,24 @@ selection + Enter to confirm** (typing a number also jumps directly; `0` selects
452
453
  "custom / manual input" entry). Non-TTY environments (CI, piped input) automatically fall back
453
454
  to numeric input. `Esc` or `Ctrl+C` cancels the wizard cleanly.
454
455
 
455
- In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
456
+ In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/tools`, `/stats`, `/new`, `/resume`, `/clear`, `/exit`.
457
+
458
+ Reasoning, tool calls and answers have separate visual sections. Reasoning defaults to a short preview;
459
+ `/think full` shows subsequent reasoning in full, `/think off` hides it, and `/think compact` restores the preview.
460
+ Tool output defaults to a few head/tail lines; `/tools full` shows subsequent tool results in full and
461
+ `/tools compact` restores compact output. These display settings do not reduce model token usage.
462
+
463
+ Each turn ends with completion time, total duration, first-token latency (including reasoning), first-answer
464
+ latency, model/tool durations and provider-reported input/output token usage across all model calls.
465
+ Cache and reasoning tokens are shown as subsets when reported. Missing or partial usage is labelled explicitly;
466
+ `/stats` also shows session totals. `Ctrl+C` stops an active task while keeping the conversation open.
467
+ Progress is saved after each tool result; `/resume` restores the working directory and reported usage.
468
+
469
+ Tool-call budget: one message may trigger up to N tool calls in a row before the turn is
470
+ force-ended with a "max tool iterations reached" notice (send another message to continue).
471
+ N defaults to **200** and is configurable in **Settings → AI models → Agent Runtime**
472
+ (`aiMaxToolIterations` in `~/.zen-gitsync/config.json`, range 1–2000) — the Web agent shares
473
+ the same value.
456
474
 
457
475
  Images: press `Alt+V` in the REPL to paste a clipboard image (screenshot), or attach a
458
476
  local file with `/image <path>`; images are sent as multimodal `image_url` parts with your
@@ -973,6 +991,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
973
991
  | 流式对话 | 基于 SSE 的实时流式输出,包含思考过程、正文内容、工具调用和工具结果的内联渲染 |
974
992
  | 工具调用展示 | 每次工具调用(run_command、read_file、edit_file、list_files、search_text、write_file)以可折叠卡片形式展示,含参数预览和执行结果 |
975
993
  | 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
994
+ | 单轮工具调用上限 | 一条消息内智能体最多连续调用多少次工具(默认 **200**,可调范围 1–2000)。在 **设置 → AI 模型配置 → 智能体运行时** 中修改;达到上限本轮会被强制结束并提示再发一条消息继续。CLI 智能体共用同一项设置 |
976
995
  | 预设问题 | 开场界面提供快捷按钮(查看项目结构、分析代码质量、写测试、Git 状态检查)|
977
996
  | 停止生成 | 流式输出期间出现浮动停止按钮;中止 LLM 请求及正在运行的子进程 |
978
997
  | 主题同步 | 对话区域跟随 GUI 当前主题(浅色 / 深色 / 自动)|
@@ -1036,7 +1055,21 @@ $ g ai --model=2 # 使用第 2 个已配置的模型(序号或
1036
1055
  直接输入数字跳转,`0` = 列表底部的"自定义 / 手动输入");非 TTY 环境下自动回退为数字输入。
1037
1056
  `Esc` 或 `Ctrl+C` 一键取消整个向导。
1038
1057
 
1039
- 会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/clear`、`/exit`。
1058
+ 会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/tools`、`/stats`、`/new`、`/resume`、`/clear`、`/exit`。
1059
+
1060
+ 思考、工具调用和回答分区展示。默认只显示简短思考预览,`/think full` 显示后续完整思考,
1061
+ `/think off` 隐藏思考,`/think compact` 恢复预览。工具结果默认保留头尾几行,
1062
+ `/tools full` 显示后续完整工具结果,`/tools compact` 恢复精简。这些显示设置不会减少模型的 Token 消耗。
1063
+
1064
+ 每轮结束显示完成时间、总耗时、首响应(含思考首字)、正文等待、模型与工具耗时,以及本轮所有模型调用的
1065
+ 输入 / 输出 Token 总量;服务端提供时还显示其中的缓存与推理 Token。用量来自服务端真实返回,
1066
+ 缺失或不完整时明确标注;`/stats` 还可查看会话累计用量。执行中按 `Ctrl+C` 停止当前任务并保留会话。
1067
+ 每个工具结果后保存进度,`/resume` 同时恢复工作目录和用量统计。
1068
+
1069
+ 工具调用预算:一条消息内智能体最多连续调用 N 次工具,触顶后本轮被强制结束并提示
1070
+ "已达单轮最大工具调用次数",再发一条消息即可继续。N 默认 **200**,可在
1071
+ **设置 → AI 模型配置 → 智能体运行时** 修改(即 `~/.zen-gitsync/config.json` 的
1072
+ `aiMaxToolIterations`,范围 1–2000),Web 端智能体共用同一项设置。
1040
1073
 
1041
1074
  图片:在 REPL 中按 `Alt+V` 粘贴剪贴板图片(截图),或用 `/image <路径>` 附加本地图片;
1042
1075
  图片以多模态 `image_url` 部件随下一条消息发送(需视觉模型)。单独 `/image` 查看待发送图片,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.17.5",
3
+ "version": "2.17.7",
4
4
  "description": "Auto commit, scheduled sync, and visual GUI for Git. Run `g` in any repo for one-key commit & push, AI commit messages, scheduled background sync, and a drag-and-drop workflow builder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -28,9 +28,10 @@
28
28
  * npm run release -- --dry-run # 只打印计划,不真正改 package.json / commit / publish
29
29
  * npm run release -- --poll-interval=20 --poll-timeout=600 # 调自更新重试节奏(秒)
30
30
  *
31
- * 发布后自更新:反复尝试 `npm install -g zen-gitsync@<版本>` 并校验全局版本;
32
- * 精确版本号解析不到(ETARGET)时改用 tarball URL 直连兜底。
33
- * 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于 packument 立即可见。
31
+ * 发布后自更新:每轮先探 tarball 能不能取,能取到才调 npm;两条路(`pkg@<版本>` /
32
+ * tarball URL 直连)都试,装上后校验全局版本,失败把原因打出来。
33
+ * 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于
34
+ * packument 立即可见,也不等于 tarball 立即可取(两种先后顺序都实测出现过)。
34
35
  */
35
36
 
36
37
  import fs from 'node:fs'
@@ -74,19 +75,81 @@ function readNumberArg(name, fallback) {
74
75
  // `npm view dist-tags` 和 `npm install pkg@ver` 读的是**同一份 packument**:
75
76
  // 等它、用它,走的是同一条被缓存的路。只把超时 600s 往上加,只是把失败往后推。
76
77
  //
77
- // 兜底:tarball URL 是不可变资源,发布完成即可取,且不经过 packument 的那层缓存
78
- // (见 tarballUrl 注释)。所以自更新不再把安装卡在 dist-tags 后面,而是
79
- // "反复尝试安装 + 校验全局版本",精确版本号解析不到时自动改用 tarball 直连。
78
+ // 兜底:tarball URL 是不可变资源,且不经过 packument 那层缓存(见 tarballUrl 注释)。
79
+ // 所以自更新不再把安装卡在 dist-tags 后面,而是"反复尝试安装 + 校验全局版本",
80
+ // 精确版本号解析不到时自动改用 tarball 直连。
81
+ //
82
+ // 又实测(2026-09-20, v2.17.6):tarball 也可能**晚于** packument 就绪 ——
83
+ // dist-tags.latest 已经是 2.17.6,而那个 tarball 的 Last-Modified 晚了 2 分钟,
84
+ // 这 2 分钟里两条路一起失败(对象还不存在),之后又有几分钟边缘缓存回 404。
85
+ // 于是 600s 眼看要耗尽、终端上 25 轮只有"→ npm install"没有任何原因。
86
+ // 对策两条:① 每轮先自己探一次 tarball(isTarballFetchable,GET 而非 HEAD),
87
+ // 取不到就跳过这一轮的 npm 调用并把状态打出来;② 失败原因压一行打出来。
88
+ // 另外单次 npm 调用必须有时间上限(见 INSTALL_TIMEOUT_MS),否则一次网络卡死
89
+ // 就能吃掉整个预算,外面那个 15s 一轮的循环根本没机会重试。
90
+ //
80
91
  // 默认 15s 一轮、上限 600s;可用 `--poll-interval=<秒>` / `--poll-timeout=<秒>` 调。
81
92
  const POLL_INTERVAL_MS = readNumberArg('--poll-interval', 15) * 1000
82
93
  const POLL_TIMEOUT_MS = readNumberArg('--poll-timeout', 600) * 1000
83
94
 
84
- // 发布物 tarball 的地址。npm publish 是先把 tarball 推进 registry、再更新 packument;
85
- // tarball 按 URL 寻址、内容不可变,所以它比 packument 更早对外可用,也不会被
86
- // max-age 那层缓存挂在旧内容上 —— "新版本已发布、但 packument 还是旧的"时,
87
- // 这是唯一能立刻装上的路径。
88
- function tarballUrl(version) {
89
- return `${NPM_REGISTRY.replace(/\/$/, '')}/${PKG_NAME}/-/${PKG_NAME}-${version}.tgz`
95
+ // 发布物 tarball 的地址。
96
+ //
97
+ // 为什么值得单独走这条路:tarball 按 URL 寻址、内容不可变,不像 packument 那样被
98
+ // max-age 挂在旧内容上 —— "新版本已发布、但 packument 还是旧的"时,它是唯一能立刻装上的路径。
99
+ //
100
+ // 但**两种先后顺序都实测出现过**,别押任何一边:
101
+ // - v2.17.4:packument 滞后 >600s,tarball 早就可取;
102
+ // - v2.17.6:反过来 —— dist-tags 已经是 2.17.6 了,tarball 的 Last-Modified 却晚 2 分钟,
103
+ // 那 2 分钟里 `npm install -g pkg@2.17.6` 和直连 tarball 一起失败(取不到那个对象)。
104
+ // 所以自更新每轮**两条路都试**,并且用 isTarballFetchable() 自己先探一次。
105
+ //
106
+ // `cacheBust=true` 时带一个时间戳查询串:目标 URL 在被发布出来的头几分钟里,
107
+ // CDN 边缘可能还挂着"这个路径 404"的负缓存(实测同一分钟内 HEAD 404 / GET 200),
108
+ // 换一个 URL 就等于绕开那份缓存。
109
+ function tarballUrl(version, cacheBust = false) {
110
+ const base = `${NPM_REGISTRY.replace(/\/$/, '')}/${PKG_NAME}/-/${PKG_NAME}-${version}.tgz`
111
+ return cacheBust ? `${base}?t=${Date.now()}` : base
112
+ }
113
+
114
+ // 探一次 tarball 到底能不能取到 —— 这比"packument 里有没有这个版本"更接近真相。
115
+ //
116
+ // 三个刻意的选择:
117
+ // ① 用 GET 而不是 HEAD:实测同一个 URL 在同一分钟内 HEAD 返回 404、GET 返回 200,
118
+ // npm 真正下载用的是 GET,所以只认 GET 的结果。
119
+ // ② 带 `Range: bytes=0-0`,只取 1 个字节(服务端支持 Accept-Ranges);万一服务端忽略 Range
120
+ // 回了整包,读完后 cancel() 掉,不会真把 7MB 拖下来。
121
+ // ③ 探不到不算致命:调用方据此跳过这一轮的 npm 调用(省下一次注定失败的 10~40s),
122
+ // 但拿不到 fetch(老 Node)时返回 unknown,让调用方照旧去试。
123
+ async function isTarballFetchable(version, cacheBust) {
124
+ if (typeof fetch !== 'function') return { ok: true, unknown: true }
125
+ try {
126
+ const res = await fetch(tarballUrl(version, cacheBust), {
127
+ headers: { Range: 'bytes=0-0' },
128
+ redirect: 'follow',
129
+ })
130
+ const status = res.status
131
+ try {
132
+ await res.body?.cancel()
133
+ } catch {
134
+ /* 流已结束,忽略 */
135
+ }
136
+ return { ok: status === 200 || status === 206, status }
137
+ } catch (err) {
138
+ return { ok: false, status: 0, error: String(err?.message || err) }
139
+ }
140
+ }
141
+
142
+ // 把 npm 的一大坨输出压成一行,用来在每次失败时给出"为什么",而不是只报"重试中"。
143
+ // 例:`E404 Not Found - GET https://... - Not found`
144
+ function summarizeInstallError(output) {
145
+ if (!output) return '未知错误'
146
+ const lines = String(output)
147
+ .split(/\r?\n/)
148
+ .map((l) => l.trim())
149
+ .filter((l) => l.startsWith('npm error') || l.startsWith('npm ERR!'))
150
+ .map((l) => l.replace(/^npm (error|ERR!)\s*/, ''))
151
+ if (lines.length === 0) return String(output).split(/\r?\n/)[0].slice(0, 160)
152
+ return lines.slice(0, 2).join(' / ').slice(0, 200)
90
153
  }
91
154
 
92
155
  // 跨平台 sleep:Node 原生,避免 `sleep N || ping` 的 Windows 兜底 hack
@@ -549,18 +612,33 @@ function readGlobalInstalledVersion() {
549
612
 
550
613
  // 执行一次全局安装。退出码 0 才返回 ok。
551
614
  // 输出先兜住不打印:重试期间每失败一次就刷一屏 `npm error` 太吵,
552
- // 只有整轮尝试全部失败时,调用方才回放最后一份输出。
615
+ // 失败时由调用方压成一行打出来(summarizeInstallError),整轮失败才回放全文。
616
+ //
617
+ // 时间必须**有界**:npm 自己的 fetch-timeout 是 5 分钟、还要重试 2 次,
618
+ // 网络卡住时单次 `npm install` 能吃掉十几分钟 —— 实测挂在一个坏代理上,
619
+ // 一次调用就耗掉了整个 600s 预算,外面那个"每 15s 重试一轮"的循环根本没有机会跑第二轮。
620
+ // 所以这里把 fetch 重试收紧、再给 execSync 一个硬上限:让每次尝试**快速失败**,
621
+ // 把"重试"这件事交给外层循环(它本来就在做,而且间隔只有 15s)。
622
+ const INSTALL_TIMEOUT_MS = 180000
553
623
  function tryInstallGlobal(spec) {
554
624
  console.log(chalk.gray(` → npm install -g ${spec}`))
555
625
  try {
556
- execSync(`npm install -g ${spec} --registry=${NPM_REGISTRY} --prefer-online`, {
557
- stdio: ['ignore', 'pipe', 'pipe'],
558
- maxBuffer: 8 * 1024 * 1024,
559
- timeout: 300000,
560
- })
626
+ execSync(
627
+ `npm install -g ${spec} --registry=${NPM_REGISTRY} --prefer-online`
628
+ + ' --fetch-retries=1 --fetch-retry-mintimeout=3000 --fetch-retry-maxtimeout=10000',
629
+ {
630
+ stdio: ['ignore', 'pipe', 'pipe'],
631
+ maxBuffer: 8 * 1024 * 1024,
632
+ timeout: INSTALL_TIMEOUT_MS,
633
+ }
634
+ )
561
635
  return { ok: true }
562
636
  } catch (err) {
637
+ // 超时会被 execSync 杀掉(带 SIGTERM),stderr 可能是空的 —— 补一句可读的原因
563
638
  const out = `${err.stdout ?? ''}${err.stderr ?? ''}`.trim()
639
+ if (!out && (err.killed || err.signal)) {
640
+ return { ok: false, output: `安装超时(${INSTALL_TIMEOUT_MS / 1000}s 到点被终止)` }
641
+ }
564
642
  return { ok: false, output: out || String(err.message || err) }
565
643
  }
566
644
  }
@@ -572,13 +650,18 @@ function tryInstallGlobal(spec) {
572
650
  // packument 被 CDN 缓存(max-age=300,实测还能更久)时两边一起滞后 ——
573
651
  // 等待阶段白等 600s,超时后再装照样 ETARGET,等于什么都没解决。
574
652
  //
575
- // 所以顺序反过来:先"尝试装",装不上才等。每轮按两条路试,任一条装上且校验通过就结束:
576
- // ① `pkg@<version>` —— 常规路径,packument 已同步时一次命中;
577
- // ② tarball URL 直连 —— packument 还是旧的时候的唯一出路(见 tarballUrl 注释)。
653
+ // 所以顺序反过来:先"尝试装",装不上才等。每轮:
654
+ // ① 先探 tarball 能不能取(isTarballFetchable)—— 取不到就别浪费一次 npm 调用,
655
+ // 并且把"还取不到 HTTP 404"如实打出来:这是"到底卡在哪"的唯一线索;
656
+ // ② 取得到就按两条路试,任一条装上且校验通过即结束:
657
+ // `pkg@<version>`(packument 已同步时的常规路径)/ tarball URL 直连;
658
+ // ③ 失败原因压成一行打出来(summarizeInstallError)。**别再默默重试**——
659
+ // v2.17.6 那次用户看到的就是"一直在执行安装命令"、25 轮里一句原因都没有,
660
+ // 而真实原因是那几分钟 tarball 还没对外可取(见 tarballUrl 注释)。
578
661
  async function selfUpdateGlobal(version) {
579
662
  console.log(chalk.blue('\n=== 发布后自更新全局版本 ==='))
580
- console.log(chalk.gray('publish 成功 ≠ packument 立即可见(registry 处理 + CDN 缓存)。'))
581
- console.log(chalk.gray('不空等 dist-tags,直接反复尝试安装;精确版本解析不到就 tarball 直连兜底。'))
663
+ console.log(chalk.gray('publish 成功 ≠ packument 立即可见、也 ≠ tarball 立即可取(两者先后顺序随机)。'))
664
+ console.log(chalk.gray('不空等 dist-tags:每轮先探 tarball,能取到才调 npm,失败会打印原因。'))
582
665
  console.log(chalk.gray(
583
666
  `间隔 ${POLL_INTERVAL_MS / 1000}s,上限 ${POLL_TIMEOUT_MS / 1000}s`
584
667
  + '(可用 --poll-interval / --poll-timeout 调)。'
@@ -586,39 +669,47 @@ async function selfUpdateGlobal(version) {
586
669
 
587
670
  const startedAt = Date.now()
588
671
  const deadline = startedAt + POLL_TIMEOUT_MS
589
- const tarball = tarballUrl(version)
590
672
  let attempt = 0
591
673
  let lastOutput = ''
592
674
 
593
675
  for (;;) {
594
676
  attempt += 1
595
677
  const packumentSynced = readLatestDistTag() === version
596
- // packument 已同步 → 先走常规精确版本;还没同步 → tarball 优先,争取一轮命中
597
- const targets = packumentSynced
598
- ? [`${PKG_NAME}@${version}`, tarball]
599
- : [tarball, `${PKG_NAME}@${version}`]
678
+ // 第 2 轮起给 tarball 地址加时间戳,绕开 CDN 可能挂着的"这条路径 404"负缓存
679
+ const bust = attempt > 1
680
+ const tarball = tarballUrl(version, bust)
681
+ const probe = await isTarballFetchable(version, bust)
600
682
 
601
683
  console.log(chalk.gray(
602
- `第 ${attempt} 次尝试(dist-tags.latest${packumentSynced ? '已同步' : '仍是旧版本'})...`
684
+ `第 ${attempt} 次尝试(dist-tags.latest${packumentSynced ? '已同步' : '仍是旧版本'},`
685
+ + ` tarball ${probe.ok ? '已可取' : `还取不到${probe.status ? ` HTTP ${probe.status}` : ''}`})...`
603
686
  ))
604
687
 
605
- for (const spec of targets) {
606
- const res = tryInstallGlobal(spec)
607
- if (!res.ok) {
608
- lastOutput = res.output
609
- continue
610
- }
611
- const installed = readGlobalInstalledVersion()
612
- if (installed === version) {
613
- const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
614
- console.log(chalk.green(
615
- `全局已更新到 ${PKG_NAME}@${version}(第 ${attempt} 次尝试,耗时 ${cost}s)`
616
- ))
617
- return
688
+ if (probe.ok) {
689
+ // packument 已同步 → 先走常规精确版本;还没同步 → tarball 优先,争取一轮命中
690
+ const targets = packumentSynced
691
+ ? [`${PKG_NAME}@${version}`, tarball]
692
+ : [tarball, `${PKG_NAME}@${version}`]
693
+
694
+ for (const spec of targets) {
695
+ const res = tryInstallGlobal(spec)
696
+ if (!res.ok) {
697
+ lastOutput = res.output
698
+ console.log(chalk.yellow(` ✗ ${summarizeInstallError(res.output)}`))
699
+ continue
700
+ }
701
+ const installed = readGlobalInstalledVersion()
702
+ if (installed === version) {
703
+ const cost = ((Date.now() - startedAt) / 1000).toFixed(1)
704
+ console.log(chalk.green(
705
+ `全局已更新到 ${PKG_NAME}@${version}(第 ${attempt} 次尝试,耗时 ${cost}s)`
706
+ ))
707
+ return
708
+ }
709
+ // 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
710
+ lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
711
+ console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
618
712
  }
619
- // 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
620
- lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
621
- console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
622
713
  }
623
714
 
624
715
  const remain = deadline - Date.now()
@@ -628,8 +719,8 @@ async function selfUpdateGlobal(version) {
628
719
  console.error(chalk.gray(
629
720
  '可稍后手动重试:\n'
630
721
  + ` npm install -g ${PKG_NAME}@${version}\n`
631
- + '若仍报 ETARGET(packument 缓存滞后),直连 tarball:\n'
632
- + ` npm install -g ${tarball}`
722
+ + '若报 ETARGET / E404(registry 元数据与 tarball 还没对齐),直连 tarball:\n'
723
+ + ` npm install -g ${tarballUrl(version)}`
633
724
  ))
634
725
  return
635
726
  }
@@ -646,7 +737,7 @@ async function publishToNpm(version) {
646
737
  if (DRY_RUN) {
647
738
  console.log(chalk.yellow(`[dry-run] npm publish --registry=${NPM_REGISTRY}`))
648
739
  console.log(chalk.yellow(
649
- `[dry-run] 轮询 registry 直到 ${version} 可见,再 npm install -g ${PKG_NAME}@${version}`
740
+ `[dry-run] 反复探测并安装,直到全局 ${PKG_NAME}@${version} 校验通过(见 selfUpdateGlobal)`
650
741
  ))
651
742
  return
652
743
  }
package/src/aiCommit.js CHANGED
@@ -19,6 +19,7 @@ import chalk from 'chalk';
19
19
  import ora from 'ora';
20
20
  import config from './config.js';
21
21
  import { execGitCommand } from './utils/index.js';
22
+ import { buildAiChatRequest } from './utils/aiEndpoint.js';
22
23
 
23
24
  // ──────────────────────────────────────────────
24
25
  // 跳过的产物/资源/lock 文件(与 GUI config.js SKIP_FILE_PATTERNS 一致)
@@ -276,9 +277,11 @@ ${diffText || '(no staged content, please infer from the file list)'}`;
276
277
  // 4. 调用 LLM
277
278
  spinner.start('AI 正在生成提交信息...');
278
279
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
279
- const url = `${defaultModel.baseURL.replace(/\/$/, '')}/chat/completions`;
280
- const headers = { 'Content-Type': 'application/json' };
281
- if (defaultModel.apiKey) headers['Authorization'] = `Bearer ${defaultModel.apiKey}`;
280
+ const { url, headers } = buildAiChatRequest({
281
+ baseURL: defaultModel.baseURL,
282
+ model: defaultModel.model,
283
+ apiKey: defaultModel.apiKey
284
+ });
282
285
  const body = JSON.stringify({
283
286
  model: defaultModel.model,
284
287
  messages: [{ role: 'user', content: prompt }],