zen-gitsync 2.16.1 → 2.16.2
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/README.md +28 -3
- package/package.json +1 -1
- package/src/cli/ai/agent.js +173 -15
- package/src/cli/ai/agent.test.js +77 -1
- package/src/cli/ai/modelSetup.js +594 -0
- package/src/cli/ai/modelSetup.test.js +496 -0
- package/src/cli/ai/platformGuard.js +200 -0
- package/src/cli/ai/platformGuard.test.js +253 -0
- package/src/cli/ai/termui.js +81 -6
- package/src/cli/ai/termui.test.js +38 -0
- package/src/cli/ai/tools.js +8 -0
- package/src/gitCommit.js +484 -484
- package/src/ui/public/assets/AppVersionBadge-BTznOVSJ.js +2 -0
- package/src/ui/public/assets/BranchSelector-BISqgXoZ.js +1 -0
- package/src/ui/public/assets/CommitForm-C7FmI0Sl.js +6 -0
- package/src/ui/public/assets/CommonDialog-BA4xg5l3.js +1 -0
- package/src/ui/public/assets/EditorView-CSWy4q7G.js +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-CPblwVA1.js → FlowExecutionViewer-CZdF1f1e.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BECFOAcQ.js +21 -0
- package/src/ui/public/assets/LogList-DYoGtOou.js +15 -0
- package/src/ui/public/assets/{MindmapView-C_9P5ijq.js → MindmapView-CU3tXgbR.js} +1 -1
- package/src/ui/public/assets/MonitorView-CSAqU7oV.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-2By0-fuq.js +1 -0
- package/src/ui/public/assets/{RemoteRepoCard-C_cBtHcN.js → RemoteRepoCard-DHsJwTEX.js} +2 -2
- package/src/ui/public/assets/SourceMapView-DskfUia5.js +3 -0
- package/src/ui/public/assets/{SvgIcon-C9YQSvGU.js → SvgIcon-BqWp2WS8.js} +1 -1
- package/src/ui/public/assets/UserInputNode-Ccnh0e-0.js +1 -0
- package/src/ui/public/assets/WorkbenchView-1q0xaqyV.js +28 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-CHQqCylN.js → _plugin-vue_export-helper-BQRl7WAH.js} +1 -1
- package/src/ui/public/assets/configStore-BkxDLXGa.js +1 -0
- package/src/ui/public/assets/{css.worker-CvXBzhp8.js → css.worker-BKx7BC4h.js} +7 -7
- package/src/ui/public/assets/dagre-CTEVx1fc.js +1 -0
- package/src/ui/public/assets/editor.worker-DQJwLooG.js +26 -0
- package/src/ui/public/assets/element-plus-CQrjg9us.css +1 -0
- package/src/ui/public/assets/element-plus-DNnePjco.js +32 -0
- package/src/ui/public/assets/{flow-mindmap-CUvJaYn2.js → flow-mindmap-DRmzYE4F.js} +6 -6
- package/src/ui/public/assets/{html.worker-BO6WuOEO.js → html.worker-ChRLLxCa.js} +4 -4
- package/src/ui/public/assets/index-Br2VZQBT.js +68 -0
- package/src/ui/public/assets/{json.worker-BkJRGcCJ.js → json.worker-DR52qCex.js} +5 -5
- package/src/ui/public/assets/{monaco-BFLoT2VD.js → monaco-BvgtFLbw.js} +30 -30
- package/src/ui/public/assets/rolldown-runtime-DAXXjFlN.js +1 -0
- package/src/ui/public/assets/{socket-io-BSgn8Jx2.js → socket-io-pmu5tRPo.js} +1 -1
- package/src/ui/public/assets/{ts.worker-B0J26iPs.js → ts.worker-Cqvg7802.js} +18 -18
- package/src/ui/public/assets/{vendor-CU5NYUKJ.js → vendor-CcK8n8T4.js} +10 -10
- package/src/ui/public/assets/vue-flow-pAz--378.js +39 -0
- package/src/ui/public/favicon.svg +75 -75
- package/src/ui/public/index.html +30 -30
- package/src/ui/public/logo.svg +74 -74
- package/src/ui/server/.claude/codediff.txt +8 -0
- package/src/ui/server/middleware/requestLogger.js +53 -53
- package/src/ui/server/routes/branchStatus.js +105 -105
- package/src/ui/server/routes/fs.js +791 -791
- package/src/ui/server/routes/git/diff.js +426 -426
- package/src/ui/server/routes/git/diffUtils.js +149 -149
- package/src/ui/server/routes/git/stash.js +550 -550
- package/src/ui/server/routes/git/tags.js +176 -176
- package/src/ui/server/routes/git.js +181 -181
- package/src/ui/server/routes/gitOps.js +1255 -1255
- package/src/ui/server/routes/process.js +86 -86
- package/src/ui/server/routes/status.js +67 -67
- package/src/ui/server/routes/workbench/pdfText.js +145 -145
- package/src/ui/server/utils/createSavePortToFile.js +48 -48
- package/src/ui/server/utils/instanceRegistry.js +282 -282
- package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
- package/src/utils/index.js +1173 -1173
- package/src/ui/public/assets/AppVersionBadge-DJyfXR_Q.js +0 -2
- package/src/ui/public/assets/BranchSelector-ClnVPC0Q.js +0 -1
- package/src/ui/public/assets/CommitForm-BihM26_4.js +0 -6
- package/src/ui/public/assets/CommonDialog-CYoIF2fg.js +0 -1
- package/src/ui/public/assets/EditorView-DYJQT-Xj.js +0 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-Cc-Tnhng.js +0 -21
- package/src/ui/public/assets/LogList-va6ANtbL.js +0 -15
- package/src/ui/public/assets/MonitorView-DRPECOVW.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DSovbKPE.js +0 -1
- package/src/ui/public/assets/SourceMapView-ZiCL8mS8.js +0 -3
- package/src/ui/public/assets/UserInputNode-CR6o8zJG.js +0 -1
- package/src/ui/public/assets/WorkbenchView-CJJrBqBH.js +0 -28
- package/src/ui/public/assets/configStore-Bw9U4AHx.js +0 -1
- package/src/ui/public/assets/dagre-CWIGcX9d.js +0 -1
- package/src/ui/public/assets/editor.worker-Cn2oRESe.js +0 -26
- package/src/ui/public/assets/element-plus-CyGhXd7X.js +0 -25
- package/src/ui/public/assets/element-plus-D3Dn7nX0.css +0 -1
- package/src/ui/public/assets/index-BzNpkvm5.js +0 -68
- package/src/ui/public/assets/rolldown-runtime-CMxvf4Kt.js +0 -1
- package/src/ui/public/assets/vue-flow-CWmfpfPN.js +0 -39
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# zen-gitsync
|
|
2
2
|
|
|
3
3
|
[English](#zen-gitsync) | [中文](#zh)
|
|
4
4
|
|
|
@@ -410,7 +410,9 @@ rm -f package-lock.json # when running on pnpm
|
|
|
410
410
|
|
|
411
411
|
### AI coding agent (terminal):
|
|
412
412
|
Launch an interactive AI agent that writes code, runs commands, and commits for you.
|
|
413
|
-
It uses the default model configured in `g ui` (Settings → AI models).
|
|
413
|
+
It uses the default model configured in `g ui` (Settings → AI models). If no model is
|
|
414
|
+
configured yet, `g ai` launches an interactive setup wizard — pick a provider, choose a
|
|
415
|
+
model, enter your API key, test the connection, and you're ready to go.
|
|
414
416
|
|
|
415
417
|
```bash
|
|
416
418
|
$ g ai # interactive REPL
|
|
@@ -418,7 +420,7 @@ $ g ai "fix the failing test" # one-shot task, then exit
|
|
|
418
420
|
$ g ai --model=2 # pick the 2nd configured model (index or name)
|
|
419
421
|
```
|
|
420
422
|
|
|
421
|
-
In-session commands: `/help`, `/model`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
|
|
423
|
+
In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
|
|
422
424
|
|
|
423
425
|
Images: press `Alt+V` in the REPL to paste a clipboard image (screenshot), or attach a
|
|
424
426
|
local file with `/image <path>`; images are sent as multimodal `image_url` parts with your
|
|
@@ -919,6 +921,29 @@ git add --renormalize .
|
|
|
919
921
|
|
|
920
922
|
## 命令行
|
|
921
923
|
|
|
924
|
+
### AI 编码智能体(终端):
|
|
925
|
+
启动交互式 AI 智能体,自动写代码、跑命令、提交代码。
|
|
926
|
+
默认使用 `g ui` 中配置的模型(设置 → AI 模型)。如果尚未配置任何模型,`g ai` 会启动
|
|
927
|
+
交互式配置向导 —— 选择服务商、选择模型、输入 API Key、测试连接,完成后即可直接使用。
|
|
928
|
+
|
|
929
|
+
```bash
|
|
930
|
+
$ g ai # 交互式 REPL
|
|
931
|
+
$ g ai "修复失败的测试" # 单发模式:执行一轮后退出
|
|
932
|
+
$ g ai --model=2 # 使用第 2 个已配置的模型(序号或名称)
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/clear`、`/exit`。
|
|
936
|
+
|
|
937
|
+
图片:在 REPL 中按 `Alt+V` 粘贴剪贴板图片(截图),或用 `/image <路径>` 附加本地图片;
|
|
938
|
+
图片以多模态 `image_url` 部件随下一条消息发送(需视觉模型)。单独 `/image` 查看待发送图片,
|
|
939
|
+
`/image clear` 清除。
|
|
940
|
+
|
|
941
|
+
终端 UI 对标 Codex / Claude Code 风格:盒式输入框、等待 spinner、灰斜体流式思考、
|
|
942
|
+
`⏺` 工具块 + 智能参数摘要、轻量 Markdown 渲染(加粗、行内代码、标题、代码块)。
|
|
943
|
+
|
|
944
|
+
权限模型:启动目录内所有操作直接执行;其他目录同样可读写;仅系统级破坏命令
|
|
945
|
+
(格式化磁盘、`rm -rf /`、关机等)由内置安全守卫硬拦截。
|
|
946
|
+
|
|
922
947
|
#### 交互式提交:
|
|
923
948
|
```bash
|
|
924
949
|
$ g
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-gitsync",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.2",
|
|
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",
|
package/src/cli/ai/agent.js
CHANGED
|
@@ -49,11 +49,13 @@ import { TOOL_DEFINITIONS, executeTool } from './tools.js'
|
|
|
49
49
|
import { createThinkFilter } from './streamFilter.js'
|
|
50
50
|
import {
|
|
51
51
|
printBanner, printHelpPanel,
|
|
52
|
+
filterSlashCommands, renderSlashHintBody,
|
|
52
53
|
startSpinner, createAssistantWriter,
|
|
53
54
|
summarizeToolArgs, printToolHeader, printToolResult,
|
|
54
55
|
printOk, printWarn, printError, printDim,
|
|
55
56
|
} from './termui.js'
|
|
56
57
|
import { readClipboardImage, checkImageFile, imageToDataUrl, formatBytes } from './images.js'
|
|
58
|
+
import { runModelSetup, collectModelInput, buildModelConfig } from './modelSetup.js'
|
|
57
59
|
|
|
58
60
|
// truncateDisplay 已迁移到 termui.js;这里 re-export 保持既有测试/外部引用不断
|
|
59
61
|
export { truncateDisplay } from './termui.js'
|
|
@@ -104,6 +106,10 @@ const STRINGS = {
|
|
|
104
106
|
imageListEmpty: '当前没有待发送的图片(Alt+V 粘贴或 /image <路径> 附加)',
|
|
105
107
|
imageCleared: '已清除待发送的图片',
|
|
106
108
|
imageSending: (n) => `📎 附带 ${n} 张图片`,
|
|
109
|
+
addModelTitle: '添加模型配置',
|
|
110
|
+
addModelSaved: (name) => `✓ 模型 "${name}" 已添加并切换为当前模型`,
|
|
111
|
+
addModelCancelled: '已取消添加模型',
|
|
112
|
+
addModelSaveError: (msg) => `保存配置失败: ${msg}`,
|
|
107
113
|
},
|
|
108
114
|
en: {
|
|
109
115
|
noModel: 'No AI model configured. Add one in GUI settings first (run `g ui`).',
|
|
@@ -140,6 +146,10 @@ const STRINGS = {
|
|
|
140
146
|
imageListEmpty: 'No pending images (Alt+V to paste, or /image <path>)',
|
|
141
147
|
imageCleared: 'Pending images cleared',
|
|
142
148
|
imageSending: (n) => `📎 ${n} image(s) attached`,
|
|
149
|
+
addModelTitle: 'Add Model Configuration',
|
|
150
|
+
addModelSaved: (name) => `✓ Model "${name}" added and switched to`,
|
|
151
|
+
addModelCancelled: 'Add model cancelled',
|
|
152
|
+
addModelSaveError: (msg) => `Failed to save: ${msg}`,
|
|
143
153
|
},
|
|
144
154
|
}
|
|
145
155
|
|
|
@@ -164,13 +174,24 @@ function buildSystemPrompt({ cwd, locale, shellDesc }) {
|
|
|
164
174
|
|
|
165
175
|
# 运行环境
|
|
166
176
|
- 操作系统: ${process.platform}
|
|
167
|
-
- Shell: ${shellDesc}
|
|
168
|
-
- 注意:Windows cmd 没有 head/grep/ls/sed/cat/tail 等 Unix 命令,直接跑会报"不是内部或外部命令"。
|
|
169
|
-
列目录用 list_files、搜内容用 search_text、看文件用 read_file,优先用工具而不是 shell;
|
|
170
|
-
必须跑 shell 时优先跨平台写法(如 node -e "..."),别用 Unix 专属命令` : ''}
|
|
177
|
+
- Shell: ${shellDesc}
|
|
171
178
|
- 当前工作目录: ${cwd}(用户在此启动 g ai,也是所有相对路径的基准)
|
|
172
179
|
- 当前时间: ${now}
|
|
173
180
|
|
|
181
|
+
# 平台兼容性(重要!)
|
|
182
|
+
- 必须使用与当前 Shell 兼容的命令,禁止盲套 Unix 写法
|
|
183
|
+
${isWin ? `- 当前是 Windows cmd.exe,以下 Unix 命令**不存在**,用了必定报"不是内部或外部命令":
|
|
184
|
+
tail / head / cat / grep / ls / sed / awk / wc / cut / uniq / xargs / which / touch
|
|
185
|
+
平台守卫会在执行前拦截这些命令,但请主动避免,不要浪费一轮调用
|
|
186
|
+
- 跨平台替代方案:
|
|
187
|
+
· 列目录 → list_files 工具 或 cmd 的 dir
|
|
188
|
+
· 搜内容 → search_text 工具 或 cmd 的 findstr
|
|
189
|
+
· 看文件 → read_file 工具 或 cmd 的 type
|
|
190
|
+
· 看输出末尾 → PowerShell "命令 | Select-Object -Last N"
|
|
191
|
+
· 文本处理 → node -e "..." 或 PowerShell
|
|
192
|
+
· 查命令路径 → cmd 的 where(不是 which)
|
|
193
|
+
- 必须跑 shell 时优先跨平台写法(如 node -e "..."),别用 Unix 专属命令` : `- 当前是 POSIX 环境,Unix 命令可用`}
|
|
194
|
+
|
|
174
195
|
# 权限(用户已明确授权,无需反复征求同意)
|
|
175
196
|
- 工作目录内:读写文件、执行命令等所有操作直接执行
|
|
176
197
|
- 其他目录:同样可以读取和修改
|
|
@@ -203,13 +224,24 @@ function buildSystemPrompt({ cwd, locale, shellDesc }) {
|
|
|
203
224
|
|
|
204
225
|
# Environment
|
|
205
226
|
- OS: ${process.platform}
|
|
206
|
-
- Shell: ${shellDesc}
|
|
207
|
-
- Note: Windows cmd has NO Unix commands (head/grep/ls/sed/cat/tail); running them fails with "not recognized".
|
|
208
|
-
Use the tools instead: list_files for directories, search_text for content, read_file for files.
|
|
209
|
-
For shell one-offs, prefer cross-platform forms like node -e "..."` : ''}
|
|
227
|
+
- Shell: ${shellDesc}
|
|
210
228
|
- Working directory: ${cwd} (where the user launched g ai; base for all relative paths)
|
|
211
229
|
- Current time: ${now}
|
|
212
230
|
|
|
231
|
+
# Platform compatibility (important!)
|
|
232
|
+
- You MUST use commands compatible with the current shell. Do NOT blindly copy Unix patterns.
|
|
233
|
+
${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist here and will fail with "not recognized":
|
|
234
|
+
tail / head / cat / grep / ls / sed / awk / wc / cut / uniq / xargs / which / touch
|
|
235
|
+
A platform guard will block these before execution, but avoid them proactively — don't waste a turn.
|
|
236
|
+
- Cross-platform alternatives:
|
|
237
|
+
· List dirs → list_files tool, or cmd's dir
|
|
238
|
+
· Search content → search_text tool, or cmd's findstr
|
|
239
|
+
· Read files → read_file tool, or cmd's type
|
|
240
|
+
· Tail output → PowerShell "command | Select-Object -Last N"
|
|
241
|
+
· Text processing → node -e "..." or PowerShell
|
|
242
|
+
· Find executable → cmd's where (not which)
|
|
243
|
+
- For shell one-offs, prefer cross-platform forms like node -e "..."` : `- POSIX environment: Unix commands are available`}
|
|
244
|
+
|
|
213
245
|
# Permissions (explicitly granted by the user — do not keep asking)
|
|
214
246
|
- Inside the working directory: read/write files and run commands directly
|
|
215
247
|
- Other directories: may also be read and modified
|
|
@@ -334,6 +366,33 @@ function trimHistory(messages) {
|
|
|
334
366
|
messages.splice(1, cut - 1)
|
|
335
367
|
}
|
|
336
368
|
|
|
369
|
+
// ──────────────────────────────────────────────
|
|
370
|
+
// 消息消毒:确保发给 LLM 的消息数组里没有空内容。
|
|
371
|
+
//
|
|
372
|
+
// 背景:部分 LLM provider(如 Moonshot/Kimi、智谱、火山引擎等)对 assistant
|
|
373
|
+
// 消息的 content 字段校验严格 —— 当模型只返回 tool_calls 而没有文本时,
|
|
374
|
+
// content 为空字符串 "" 会被拒绝并报 "chat content is empty (2013)"。
|
|
375
|
+
// OpenAI 官方规范允许 assistant 消息在带 tool_calls 时 content 为 null,
|
|
376
|
+
// 本函数将空字符串统一转为 null,并对 tool/user 消息做兜底防止空内容。
|
|
377
|
+
// ──────────────────────────────────────────────
|
|
378
|
+
export function sanitizeMessages(messages) {
|
|
379
|
+
for (const m of messages) {
|
|
380
|
+
if (typeof m.content !== 'string') continue
|
|
381
|
+
if (m.content === '') {
|
|
382
|
+
if (m.role === 'assistant') {
|
|
383
|
+
// assistant 带 tool_calls 时 content 用 null(OpenAI 规范);
|
|
384
|
+
// 不带 tool_calls 的空内容也用 null,provider 通常可接受
|
|
385
|
+
m.content = null
|
|
386
|
+
} else if (m.role === 'tool') {
|
|
387
|
+
m.content = '(no output)'
|
|
388
|
+
} else if (m.role === 'user') {
|
|
389
|
+
m.content = ' '
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return messages
|
|
394
|
+
}
|
|
395
|
+
|
|
337
396
|
// ──────────────────────────────────────────────
|
|
338
397
|
// 多模态历史:base64 图片很占上下文,只保留"最近一条带图消息"里的图片,
|
|
339
398
|
// 更早消息里的 image_url 部件降级为文字占位(模型仍知道这里曾有图)
|
|
@@ -404,6 +463,9 @@ async function runAgentTurn(state, userText, t, images = []) {
|
|
|
404
463
|
else writer.writeContent(text)
|
|
405
464
|
}
|
|
406
465
|
|
|
466
|
+
// 发送前消毒:确保历史里没有空 content 的消息(部分 provider 报 2013)
|
|
467
|
+
sanitizeMessages(state.messages)
|
|
468
|
+
|
|
407
469
|
let result
|
|
408
470
|
try {
|
|
409
471
|
result = await streamChatOnce({
|
|
@@ -440,12 +502,13 @@ async function runAgentTurn(state, userText, t, images = []) {
|
|
|
440
502
|
|
|
441
503
|
// 无工具调用:本轮结束,assistant 文本入历史
|
|
442
504
|
if (toolCalls.length === 0) {
|
|
443
|
-
state.messages.push({ role: 'assistant', content: content ||
|
|
505
|
+
state.messages.push({ role: 'assistant', content: content || null })
|
|
444
506
|
return
|
|
445
507
|
}
|
|
446
508
|
|
|
447
509
|
// 有工具调用:assistant(带 tool_calls)入历史,然后逐个执行
|
|
448
|
-
|
|
510
|
+
// content 为 null 而非空字符串:部分 provider 拒绝空字符串内容(2013 错误)
|
|
511
|
+
state.messages.push({ role: 'assistant', content: content || null, tool_calls: toolCalls })
|
|
449
512
|
|
|
450
513
|
for (const tc of toolCalls) {
|
|
451
514
|
const name = tc.function?.name || ''
|
|
@@ -491,6 +554,7 @@ function printSlashHelp(t, locale) {
|
|
|
491
554
|
const lines = zh ? [
|
|
492
555
|
' /help 显示本帮助',
|
|
493
556
|
' /model 列出可用模型;/model <序号> 切换模型',
|
|
557
|
+
' /addmodel 添加新的模型配置(交互式向导)',
|
|
494
558
|
' /cd <路径> 切换智能体工作目录',
|
|
495
559
|
' /image [路径] 查看待发送图片;/image <路径> 附加本地图片;/image clear 清除',
|
|
496
560
|
' /think 开关思考过程显示',
|
|
@@ -504,6 +568,7 @@ function printSlashHelp(t, locale) {
|
|
|
504
568
|
] : [
|
|
505
569
|
' /help Show this help',
|
|
506
570
|
' /model List models; /model <n> to switch',
|
|
571
|
+
' /addmodel Add a new model (interactive wizard)',
|
|
507
572
|
' /cd <path> Change agent working directory',
|
|
508
573
|
' /image [path] List pending images; attach a file; /image clear to reset',
|
|
509
574
|
' /think Toggle thinking display',
|
|
@@ -582,6 +647,47 @@ async function handleSlashCommand(state, input, t) {
|
|
|
582
647
|
printOk(t.modelSwitched(modelLabel(state.model)))
|
|
583
648
|
return 'ok'
|
|
584
649
|
}
|
|
650
|
+
if (cmd === '/addmodel') {
|
|
651
|
+
// 智能体执行中不允许启动向导:向导的 rl.question 会与流式输出交错,体验混乱
|
|
652
|
+
if (state.busy) {
|
|
653
|
+
printDim(t.busy)
|
|
654
|
+
return 'ok'
|
|
655
|
+
}
|
|
656
|
+
state.inWizard = true
|
|
657
|
+
try {
|
|
658
|
+
console.log(chalk.cyan.bold('\n' + t.addModelTitle))
|
|
659
|
+
// 复用 modelSetup 的交互式收集逻辑(服务商 → 接口 → 模型名 → Key → 显示名 → 测试)
|
|
660
|
+
const collected = await collectModelInput({
|
|
661
|
+
locale: state.locale,
|
|
662
|
+
rl: state.rl,
|
|
663
|
+
cancelMessage: t.addModelCancelled,
|
|
664
|
+
})
|
|
665
|
+
if (!collected) return 'ok'
|
|
666
|
+
const { baseURL, model, apiKey, displayName } = collected
|
|
667
|
+
// 构建模型配置(isDefault=false:/addmodel 只是追加,不改默认模型)
|
|
668
|
+
const newModel = buildModelConfig({ baseURL, model, apiKey, name: displayName, isDefault: false })
|
|
669
|
+
// 持久化到 ~/.git-commit-tool.json 顶层 models 数组
|
|
670
|
+
const cfg = await config.loadConfig()
|
|
671
|
+
const models = Array.isArray(cfg.models) ? cfg.models : []
|
|
672
|
+
models.push(newModel)
|
|
673
|
+
cfg.models = models
|
|
674
|
+
try {
|
|
675
|
+
await config.saveConfig(cfg)
|
|
676
|
+
} catch (err) {
|
|
677
|
+
printError(t.addModelSaveError(err.message))
|
|
678
|
+
return 'ok'
|
|
679
|
+
}
|
|
680
|
+
// 同步 REPL 状态:更新模型列表 + 自动切换到新模型(用户刚添加,大概率想立刻用)
|
|
681
|
+
state.models = models
|
|
682
|
+
state.model = newModel
|
|
683
|
+
printOk(t.addModelSaved(displayName || model))
|
|
684
|
+
} catch (err) {
|
|
685
|
+
printError(err.message)
|
|
686
|
+
} finally {
|
|
687
|
+
state.inWizard = false
|
|
688
|
+
}
|
|
689
|
+
return 'ok'
|
|
690
|
+
}
|
|
585
691
|
if (cmd === '/cd') {
|
|
586
692
|
if (!arg) { console.log(state.ctx.cwd); return 'ok' }
|
|
587
693
|
const { resolve } = await import('node:path')
|
|
@@ -617,11 +723,15 @@ export async function runAiAgent(argv = []) {
|
|
|
617
723
|
return
|
|
618
724
|
}
|
|
619
725
|
|
|
620
|
-
|
|
726
|
+
let models = Array.isArray(cfg.models) ? cfg.models : []
|
|
621
727
|
if (models.length === 0) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
728
|
+
// 未配置模型:启动交互式配置向导,参照 g ui 设置里的"添加模型"
|
|
729
|
+
const setupResult = await runModelSetup({ locale })
|
|
730
|
+
if (!setupResult) {
|
|
731
|
+
process.exitCode = 1
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
models = setupResult.models
|
|
625
735
|
}
|
|
626
736
|
|
|
627
737
|
// --model=<序号|名称> 选择模型;默认 isDefault,否则第一个
|
|
@@ -664,6 +774,8 @@ export async function runAiAgent(argv = []) {
|
|
|
664
774
|
showThinking: true, // /think 切换:是否回显模型的思考过程
|
|
665
775
|
pendingImages: [], // Alt+V / /image 附加的待发送图片 [{path, bytes}]
|
|
666
776
|
pasting: false, // 剪贴板读取进行中(防 Alt+V 连打并发)
|
|
777
|
+
inWizard: false, // /addmodel 交互式向导进行中:忽略 REPL 的 line 事件
|
|
778
|
+
rl: null, // REPL readline 引用(供 /addmodel 复用做 rl.question)
|
|
667
779
|
}
|
|
668
780
|
|
|
669
781
|
// SIGINT 时中止进行中的 LLM 请求 + 正在跑的子命令
|
|
@@ -684,7 +796,7 @@ export async function runAiAgent(argv = []) {
|
|
|
684
796
|
|
|
685
797
|
// ── 交互模式(REPL)──
|
|
686
798
|
printBanner({
|
|
687
|
-
title: '
|
|
799
|
+
title: 'Zen GitSync — AI Agent',
|
|
688
800
|
modelLabel: modelLabel(model),
|
|
689
801
|
baseURL: model.baseURL || '',
|
|
690
802
|
cwd,
|
|
@@ -699,6 +811,7 @@ export async function runAiAgent(argv = []) {
|
|
|
699
811
|
prompt: chalk.cyan.bold(t.prompt),
|
|
700
812
|
historySize: 200,
|
|
701
813
|
})
|
|
814
|
+
state.rl = rl // 供 /addmodel 等需要 rl.question 的斜杠命令复用
|
|
702
815
|
|
|
703
816
|
// 输入提示符管理。
|
|
704
817
|
//
|
|
@@ -756,6 +869,35 @@ export async function runAiAgent(argv = []) {
|
|
|
756
869
|
if (!state.busy) safeShowPrompt()
|
|
757
870
|
}
|
|
758
871
|
|
|
872
|
+
// ── 斜杠命令即时提示 ──
|
|
873
|
+
// 在提示符下方浮现匹配到的命令,随输入过滤;不移动光标(用 DECSC/DECRC 保存-恢复)。
|
|
874
|
+
// slashHintRows 记录当前提示占了几行,下次刷新/提交时据此擦除,避免残影。
|
|
875
|
+
let slashHintRows = 0
|
|
876
|
+
const eraseSlashHint = () => {
|
|
877
|
+
if (!process.stdout.isTTY || slashHintRows === 0) return
|
|
878
|
+
// 保存光标 → 下移到提示区逐行清空 → 恢复光标
|
|
879
|
+
let seq = '\x1b7'
|
|
880
|
+
for (let i = 0; i < slashHintRows; i++) seq += '\x1b[1B\x1b[2K'
|
|
881
|
+
seq += '\x1b8'
|
|
882
|
+
process.stdout.write(seq)
|
|
883
|
+
slashHintRows = 0
|
|
884
|
+
}
|
|
885
|
+
const renderSlashHint = () => {
|
|
886
|
+
if (!process.stdout.isTTY || state.busy || state.inWizard) { return }
|
|
887
|
+
const matches = filterSlashCommands(rl.line, state.locale)
|
|
888
|
+
const body = renderSlashHintBody(matches)
|
|
889
|
+
// 先擦掉旧提示(行数可能变化),再画新的
|
|
890
|
+
eraseSlashHint()
|
|
891
|
+
if (!body) return
|
|
892
|
+
const rows = body.split('\n')
|
|
893
|
+
// 保存光标,逐行下移打印(每行先清行防止与旧内容叠字),最后恢复光标回输入行
|
|
894
|
+
let seq = '\x1b7'
|
|
895
|
+
for (const r of rows) seq += '\x1b[1B\x1b[2K\r' + r
|
|
896
|
+
seq += '\x1b8'
|
|
897
|
+
process.stdout.write(seq)
|
|
898
|
+
slashHintRows = rows.length
|
|
899
|
+
}
|
|
900
|
+
|
|
759
901
|
// Alt+V 粘贴剪贴板图片(node 把 ESC+v 解析为 meta+v;部分终端不发 Alt,可用 /image 兜底)
|
|
760
902
|
const pasteFromClipboard = async () => {
|
|
761
903
|
if (state.pasting) return
|
|
@@ -777,6 +919,13 @@ export async function runAiAgent(argv = []) {
|
|
|
777
919
|
}
|
|
778
920
|
}
|
|
779
921
|
rl.input.on('keypress', (ch, key) => {
|
|
922
|
+
// Enter 提交由 line 处理器负责擦除提示,这里不重画(此刻 readline 已换行,
|
|
923
|
+
// 光标不在输入行,重画会错位)
|
|
924
|
+
if (!(key && (key.name === 'return' || key.name === 'enter'))) {
|
|
925
|
+
// 每次按键后即时刷新斜杠命令提示(readline 已先处理完本次按键,rl.line 为最新值)。
|
|
926
|
+
// 非 slash / 无匹配时 renderSlashHint 会把上一次的提示擦掉。
|
|
927
|
+
renderSlashHint()
|
|
928
|
+
}
|
|
780
929
|
if (!key) return
|
|
781
930
|
const isAltV = (key.meta && key.name === 'v')
|
|
782
931
|
|| (typeof key.sequence === 'string' && key.sequence.length === 2
|
|
@@ -789,6 +938,15 @@ export async function runAiAgent(argv = []) {
|
|
|
789
938
|
rl.prompt()
|
|
790
939
|
|
|
791
940
|
rl.on('line', async (line) => {
|
|
941
|
+
// 提交时光标已落到提示符下一行(即提示区起点),直接清到屏幕末尾擦掉整块提示,
|
|
942
|
+
// 比 DECSC/DECRC 相对定位更稳(此时光标不在输入行)
|
|
943
|
+
if (process.stdout.isTTY && slashHintRows > 0) {
|
|
944
|
+
process.stdout.write('\x1b[0J')
|
|
945
|
+
slashHintRows = 0
|
|
946
|
+
}
|
|
947
|
+
// /addmodel 等交互式向导进行中时,用户的回答由向导自身的 rl.question 处理,
|
|
948
|
+
// 不应进入 REPL 的正常输入流程(否则会把向导的答案当成命令/消息发给模型)
|
|
949
|
+
if (state.inWizard) return
|
|
792
950
|
const input = line.trim()
|
|
793
951
|
|
|
794
952
|
if (input.startsWith('/')) {
|
package/src/cli/ai/agent.test.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// 且切口把 "src/cli/ai/agent.js" 从中间切断显示成 "rc/cli/ai/agent.js"。
|
|
18
18
|
import { test } from 'node:test'
|
|
19
19
|
import assert from 'node:assert/strict'
|
|
20
|
-
import { truncateDisplay, stripStaleImages } from './agent.js'
|
|
20
|
+
import { truncateDisplay, stripStaleImages, sanitizeMessages } from './agent.js'
|
|
21
21
|
|
|
22
22
|
const LIMIT = 600
|
|
23
23
|
|
|
@@ -112,3 +112,79 @@ test('stripStaleImages: en locale 用英文占位符', () => {
|
|
|
112
112
|
assert.equal(messages[0].content[0].text, '[image omitted from history]')
|
|
113
113
|
assert.equal(messages[1].content[0].type, 'image_url')
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
// ── sanitizeMessages:空 content 消毒(防止 provider 报 2013) ──
|
|
117
|
+
test('sanitizeMessages: assistant 空 content 转 null(带 tool_calls 场景)', () => {
|
|
118
|
+
const messages = [
|
|
119
|
+
{ role: 'system', content: 'sys' },
|
|
120
|
+
{ role: 'user', content: 'hi' },
|
|
121
|
+
{ role: 'assistant', content: '', tool_calls: [{ id: 'tc1', type: 'function', function: { name: 'read_file', arguments: '{}' } }] },
|
|
122
|
+
{ role: 'tool', tool_call_id: 'tc1', name: 'read_file', content: 'file contents...' },
|
|
123
|
+
]
|
|
124
|
+
sanitizeMessages(messages)
|
|
125
|
+
assert.equal(messages[2].content, null, 'assistant 空 content 应转为 null')
|
|
126
|
+
assert.ok(messages[2].tool_calls, 'tool_calls 应保留不动')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('sanitizeMessages: assistant 空 content 转 null(无 tool_calls)', () => {
|
|
130
|
+
const messages = [
|
|
131
|
+
{ role: 'assistant', content: '' },
|
|
132
|
+
]
|
|
133
|
+
sanitizeMessages(messages)
|
|
134
|
+
assert.equal(messages[0].content, null)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('sanitizeMessages: tool 空 content 兜底为 (no output)', () => {
|
|
138
|
+
const messages = [
|
|
139
|
+
{ role: 'tool', tool_call_id: 'tc1', name: 'run_command', content: '' },
|
|
140
|
+
]
|
|
141
|
+
sanitizeMessages(messages)
|
|
142
|
+
assert.equal(messages[0].content, '(no output)')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('sanitizeMessages: user 空 content 兜底为空格', () => {
|
|
146
|
+
const messages = [
|
|
147
|
+
{ role: 'user', content: '' },
|
|
148
|
+
]
|
|
149
|
+
sanitizeMessages(messages)
|
|
150
|
+
assert.equal(messages[0].content, ' ')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('sanitizeMessages: 非空 content 不受影响', () => {
|
|
154
|
+
const messages = [
|
|
155
|
+
{ role: 'system', content: 'sys' },
|
|
156
|
+
{ role: 'user', content: 'hello' },
|
|
157
|
+
{ role: 'assistant', content: 'world' },
|
|
158
|
+
{ role: 'tool', tool_call_id: 'tc1', name: 'read_file', content: 'output' },
|
|
159
|
+
]
|
|
160
|
+
const snapshot = JSON.parse(JSON.stringify(messages))
|
|
161
|
+
sanitizeMessages(messages)
|
|
162
|
+
assert.deepEqual(messages, snapshot)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('sanitizeMessages: 数组 content(多模态)不受影响', () => {
|
|
166
|
+
const messages = [
|
|
167
|
+
{ role: 'user', content: [{ type: 'text', text: 'hi' }, { type: 'image_url', image_url: { url: 'data:...' } }] },
|
|
168
|
+
]
|
|
169
|
+
const snapshot = JSON.parse(JSON.stringify(messages))
|
|
170
|
+
sanitizeMessages(messages)
|
|
171
|
+
assert.deepEqual(messages, snapshot)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('sanitizeMessages: 模拟真实工具调用流程后消息数组无空 content', () => {
|
|
175
|
+
// 模拟:模型只返回 tool_calls 没有文本 → 工具执行 → 下次请求
|
|
176
|
+
const messages = [
|
|
177
|
+
{ role: 'system', content: 'sys prompt' },
|
|
178
|
+
{ role: 'user', content: '帮我读一下文件' },
|
|
179
|
+
{ role: 'assistant', content: '', tool_calls: [{ id: 'tc1', type: 'function', function: { name: 'read_file', arguments: '{"path":"a.js"}' } }] },
|
|
180
|
+
{ role: 'tool', tool_call_id: 'tc1', name: 'read_file', content: '1→hello' },
|
|
181
|
+
]
|
|
182
|
+
sanitizeMessages(messages)
|
|
183
|
+
// 消毒后所有消息的 content 都不应是空字符串
|
|
184
|
+
for (const m of messages) {
|
|
185
|
+
if (typeof m.content === 'string') {
|
|
186
|
+
assert.ok(m.content.length > 0, `role=${m.role} 的 content 不应为空字符串`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
assert.equal(messages[2].content, null, 'assistant 空 content 应为 null')
|
|
190
|
+
})
|