zen-gitsync 2.17.8 → 2.17.10
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 +6 -0
- package/package.json +2 -1
- package/src/cli/ai/agent.js +210 -58
- package/src/cli/ai/context.js +58 -0
- package/src/cli/ai/context.test.js +117 -0
- package/src/cli/ai/extensions.js +131 -0
- package/src/cli/ai/extensions.test.js +158 -0
- package/src/cli/ai/mcp.js +498 -0
- package/src/cli/ai/mcp.test.js +275 -0
- package/src/cli/ai/runtime.test.js +32 -0
- package/src/cli/ai/skills.js +207 -0
- package/src/cli/ai/skills.test.js +126 -0
- package/src/cli/ai/termui.js +1 -0
- package/src/cli/ai/tools.js +94 -3
- package/src/cli/ai/tools.test.js +82 -0
- package/src/cli/ai/transport.js +18 -4
- package/src/cli/ai/turn.js +21 -5
- package/src/config.js +27 -3
- package/src/paths.js +11 -0
- package/src/ui/public/assets/AgentView-BV15UtEF.css +1 -0
- package/src/ui/public/assets/AgentView-C-AJo7KS.js +5 -0
- package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +2 -0
- package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +1 -0
- package/src/ui/public/assets/CommitForm-DpXuFG-F.js +6 -0
- package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +1 -0
- package/src/ui/public/assets/EditorView-B1OnoXXf.js +1 -0
- package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +14 -0
- package/src/ui/public/assets/{FlowExecutionViewer-uXrzZjsW.js → FlowExecutionViewer-BgEt5_Hz.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +21 -0
- package/src/ui/public/assets/IconButton-aM3vaSIN.js +1 -0
- package/src/ui/public/assets/LogList-UjoVFZ-H.js +7 -0
- package/src/ui/public/assets/MindmapView-D_04v0n4.js +1 -0
- package/src/ui/public/assets/MonitorView-BD1_uzq7.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +1 -0
- package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +3 -0
- package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +1 -0
- package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +1 -0
- package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +1 -0
- package/src/ui/public/assets/{WorkbenchView-v3QFn6Ln.css → WorkbenchView-C_Gmzvj1.css} +1 -1
- package/src/ui/public/assets/WorkbenchView-UGC334JL.js +13 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +19 -0
- package/src/ui/public/assets/configStore-BLRcYJdf.js +1 -0
- package/src/ui/public/assets/{element-plus-KTmRV-so.js → element-plus-DcZ-TVp6.js} +3 -3
- package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +139 -0
- package/src/ui/public/assets/{gitStore-CpcS-MzM.js → gitStore-DMEDMZMk.js} +4 -4
- package/src/ui/public/assets/index-CW3xs-af.css +1 -0
- package/src/ui/public/assets/index-S0_gRyiO.js +26 -0
- package/src/ui/public/assets/{office-docx-BfSBpdBv.js → office-docx-C0g5Oepv.js} +5 -5
- package/src/ui/public/assets/{office-excel-S95W2mv4.js → office-excel-D-uYLKaA.js} +1 -1
- package/src/ui/public/assets/{office-pptx-DFFarGrk.js → office-pptx-De3fnc6M.js} +1 -1
- package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
- package/src/ui/public/assets/toolsStore-CpImRUhI.js +1 -0
- package/src/ui/public/assets/{useThemeObserver-B01D-Vl6.js → useThemeObserver-CG8k8U18.js} +1 -1
- package/src/ui/public/assets/{vendor-dO4B7wQf.js → vendor-BIgZEtkx.js} +27 -27
- package/src/ui/public/assets/vue-flow-CPCFgenl.js +39 -0
- package/src/ui/public/index.html +17 -17
- package/src/ui/server/index.js +4 -0
- package/src/ui/server/routes/config.js +16 -3
- package/src/ui/server/routes/exec.js +19 -11
- package/src/ui/server/routes/fileOpen.js +142 -2
- package/src/ui/server/routes/remoteRepos.js +604 -0
- package/src/ui/server/routes/remoteRepos.test.js +386 -0
- package/src/ui/server/routes/workbench/agentChat.js +86 -180
- package/src/ui/server/routes/workbench/agentChatShared.test.js +151 -0
- package/src/ui/server/routes/workbench/agentMarketplace.js +986 -0
- package/src/ui/server/routes/workbench/agentMarketplace.test.js +306 -0
- package/src/ui/server/routes/workbench/agentParity.test.js +167 -0
- package/src/ui/server/routes/workbench/agentRoutes.js +86 -1
- package/src/ui/server/routes/workbench/agentRoutes.test.js +50 -1
- package/src/ui/server/routes/workbench/jobStore.js +17 -3
- package/src/ui/server/routes/workbench/orchestratorStore.js +42 -2
- package/src/ui/server/routes/workbench/projectRegistry.js +6 -0
- package/src/ui/server/routes/workbench/projectRegistry.test.js +70 -0
- package/src/ui/server/routes/workbench/projectTool.js +198 -0
- package/src/ui/server/routes/workbench/projectTool.test.js +152 -0
- package/src/ui/server/routes/workbench/remoteCliPromptParity.test.js +177 -0
- package/src/ui/server/routes/workbench/taskRunner.js +269 -12
- package/src/ui/server/routes/workbench/taskRunner.toolcalls.test.js +307 -0
- package/src/ui/server/socket/registerUiSocketHandlers.js +18 -11
- package/src/utils/shellPath.js +253 -0
- package/src/utils/shellPath.test.js +221 -0
- package/src/ui/public/assets/AgentView-CdJaeRh0.js +0 -5
- package/src/ui/public/assets/AgentView-DAY4S5WN.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CiijSYfM.js +0 -2
- package/src/ui/public/assets/BranchSelector-BS7ajwvv.js +0 -1
- package/src/ui/public/assets/CommitForm-CEOSqXCA.js +0 -6
- package/src/ui/public/assets/CommonDialog-CAaEi-4T.js +0 -1
- package/src/ui/public/assets/EditorView-DyaFyRDD.js +0 -1
- package/src/ui/public/assets/FileDiffViewer-BvX9cQjR.js +0 -14
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DIBpUSt_.js +0 -21
- package/src/ui/public/assets/IconButton-CnoW2on6.js +0 -1
- package/src/ui/public/assets/LogList-D2espSVo.js +0 -7
- package/src/ui/public/assets/MindmapView-CZF-rZVs.js +0 -1
- package/src/ui/public/assets/MonitorView-DjLr7qOG.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DzsU-QIO.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-CuuG2ucT.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-BGf8L0of.js +0 -1
- package/src/ui/public/assets/SourceMapView-CFb02m6z.js +0 -3
- package/src/ui/public/assets/SvgIcon-SRAeYjnN.js +0 -1
- package/src/ui/public/assets/TemplateManager-gedqAXl8.js +0 -1
- package/src/ui/public/assets/UserInputNode-BOCuMQW6.js +0 -1
- package/src/ui/public/assets/WorkbenchView-CRoR21LQ.js +0 -13
- package/src/ui/public/assets/_plugin-vue_export-helper-C8OE7U9H.js +0 -19
- package/src/ui/public/assets/configStore-CuMElTgV.js +0 -1
- package/src/ui/public/assets/flow-mindmap-CurCIiCp.js +0 -139
- package/src/ui/public/assets/index-cApp6w0b.css +0 -1
- package/src/ui/public/assets/index-yXm9JLGv.js +0 -24
- package/src/ui/public/assets/previewSandbox-DkXYEpLP.js +0 -0
- package/src/ui/public/assets/toolsStore-DGxlRe3Y.js +0 -1
- package/src/ui/public/assets/vue-flow-CsvDD38k.js +0 -39
package/README.md
CHANGED
|
@@ -166,6 +166,8 @@ In either mode, click **AI Generate** to fill in the fields automatically based
|
|
|
166
166
|
|
|
167
167
|
> Click the directory name in the header (or the folder icon) to open this dialog. Type a path, hit **浏览** to use the OS file picker, or pick from **常用目录** for one-click switching. **使用新标签打开** spawns a new GUI tab on that path so you can keep the current project open.
|
|
168
168
|
|
|
169
|
+
When the GUI is opened on a directory that is not a Git repository, the right pane shows the **Recent projects** list instead — every recent directory with its Git badges (behind / ahead / uncommitted) and one-click "open in a new tab". Each page load runs a `git fetch` pass over all of them automatically, so the ahead/behind badges show the real state rather than the snapshot from the last fetch; the **刷新全部** button does the same thing on demand.
|
|
170
|
+
|
|
169
171
|
---
|
|
170
172
|
|
|
171
173
|
### Branch Management
|
|
@@ -391,6 +393,7 @@ A dedicated view (robot icon in the activity bar) for chatting with the built-in
|
|
|
391
393
|
| Live session entry | Sending the first message of a new session makes it show up in the list **immediately** with a "Generating..." badge, instead of waiting for the whole turn to finish; once the reply ends and the server persists the session, the entry is replaced by the real timestamp and message count |
|
|
392
394
|
| Streaming chat | SSE-based real-time streaming with thinking process, content, tool calls, and tool results rendered inline |
|
|
393
395
|
| 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 |
|
|
396
|
+
| Recent-projects awareness | Ask "which of my projects need a pull?" and the agent calls its built-in `list_projects` tool instead of scanning the disk: it returns exactly the list behind the GUI's **Recent projects** panel (recent directories plus any directory a task was created in, with branch / ahead / behind / uncommitted counts and task progress), so the agent's answer and the UI agree. Ahead/behind reads local refs, so the agent can pass `refresh=true` to run a `git fetch` pass first when the question is about pulling |
|
|
394
397
|
| 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 |
|
|
395
398
|
| 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 |
|
|
396
399
|
| Preset questions | Quick-start buttons on the welcome screen for common tasks (view project structure, analyze code quality, write tests, check git status) |
|
|
@@ -779,6 +782,8 @@ $ ZEN_ALLOWED_ORIGINS="https://zen.example.com,http://10.0.0.5:8080" g ui
|
|
|
779
782
|
|
|
780
783
|
> 点击顶部条里的目录名(或文件夹图标)即可弹出该对话框。直接输入路径、点击 **浏览** 唤起系统文件选择器,或从 **常用目录** 一键切换。**使用新标签打开** 会在新 GUI 标签里加载目标路径,原项目保持不动。
|
|
781
784
|
|
|
785
|
+
当 GUI 打开在一个**不是 Git 仓库**的目录上时,右侧会改为显示「最近项目」列表 —— 每个最近目录一张卡片,带 Git 徽标(落后 / 领先 / 未提交)与「在新标签页打开」。每次打开界面时会自动对所有项目跑一遍 `git fetch`,让「领先/落后」显示真实状态而不是上次 fetch 时的快照;**刷新全部** 按钮可以随时手动再刷一遍。
|
|
786
|
+
|
|
782
787
|
---
|
|
783
788
|
|
|
784
789
|
### 分支管理
|
|
@@ -1006,6 +1011,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
|
|
|
1006
1011
|
| 会话实时入列 | 新会话发出第一条消息后,左侧列表**立刻**出现这一条(带「正在生成中…」标记),不用等整轮回答跑完;回答结束、服务端落盘后自动替换成真实的时间与条数 |
|
|
1007
1012
|
| 流式对话 | 基于 SSE 的实时流式输出,包含思考过程、正文内容、工具调用和工具结果的内联渲染 |
|
|
1008
1013
|
| 工具调用展示 | 每次工具调用(run_command、read_file、edit_file、list_files、search_text、write_file)以可折叠卡片形式展示,含参数预览和执行结果 |
|
|
1014
|
+
| 最近项目感知 | 问「我哪些项目需要 pull」时,智能体调用内置的 `list_projects` 工具,而不是自己去扫盘:返回的就是 GUI「最近项目」面板那份清单(最近目录 + 建过任务的目录,带分支 / 领先 / 落后 / 未提交数与任务进度),回答与界面对得上。领先/落后读的是本地引用,因此问到"要不要拉"时它可以带 `refresh=true` 先联网 fetch 一轮再答 |
|
|
1009
1015
|
| 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
|
|
1010
1016
|
| 单轮工具调用上限 | 一条消息内智能体最多连续调用多少次工具(默认 **200**,可调范围 1–2000)。在 **设置 → AI 模型配置 → 智能体运行时** 中修改;达到上限本轮会被强制结束并提示再发一条消息继续。CLI 智能体共用同一项设置 |
|
|
1011
1017
|
| 预设问题 | 开场界面提供快捷按钮(查看项目结构、分析代码质量、写测试、Git 状态检查)|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-gitsync",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.10",
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"verify:wb-proj-branch": "node scripts/verify-workbench-proj-branch.cjs",
|
|
37
37
|
"verify:wb-proj-filter": "node scripts/verify-wb-proj-filter.cjs",
|
|
38
38
|
"verify:wb-panel-resize": "node scripts/verify-wb-panel-resize.cjs",
|
|
39
|
+
"verify:wb-toolcalls": "node scripts/verify-workbench-toolcalls.mjs",
|
|
39
40
|
"verify:wb-responsive": "node scripts/verify-wb-responsive.cjs",
|
|
40
41
|
"verify:wb-editor-persist": "node scripts/verify-workbench-editor-persist.cjs",
|
|
41
42
|
"e2e": "cd src/ui/client && npx playwright test",
|
package/src/cli/ai/agent.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// - 默认使用 g ui 里配置的模型(~/.zen-gitsync/config.json 顶层 models,
|
|
21
21
|
// isDefault 优先,否则取第一个);--model=<序号|名称> 或会话内 /model 切换
|
|
22
22
|
// - OpenAI 兼容流式 function calling:模型可以调用 run_command /
|
|
23
|
-
// read_file / write_file / edit_file / list_files / search_text
|
|
23
|
+
// read_file / write_file / edit_file / list_files / search_text / ask_user
|
|
24
24
|
// - 图片:Alt+V 粘贴剪贴板图片、/image <路径> 附加本地图片,
|
|
25
25
|
// 以多模态(image_url)消息发给模型(需模型支持视觉)
|
|
26
26
|
// - 权限:启动目录内全开放,其他目录可读写;唯一红线由 safety.js 拦截
|
|
@@ -44,8 +44,9 @@ import chalk from 'chalk'
|
|
|
44
44
|
import config from '../../config.js'
|
|
45
45
|
import { getCwd } from '../../utils/index.js'
|
|
46
46
|
import { registerCleanup } from '../cleanup.js'
|
|
47
|
-
import { terminateCommand } from './tools.js'
|
|
47
|
+
import { terminateCommand, TOOL_DEFINITIONS } from './tools.js'
|
|
48
48
|
import { runAgentTurn } from './turn.js'
|
|
49
|
+
import { AgentExtensions } from './extensions.js'
|
|
49
50
|
import { repairToolHistory, loadProjectInstructions } from './context.js'
|
|
50
51
|
import {
|
|
51
52
|
printBanner, printHelpPanel,
|
|
@@ -96,6 +97,14 @@ const STRINGS = {
|
|
|
96
97
|
thinkingHint: '… 已显示前 12 行;输入 /think full 开启后续完整思考',
|
|
97
98
|
emptyResponse: '模型没有返回回答,请重试或切换模型。',
|
|
98
99
|
chars: '字符',
|
|
100
|
+
extLoadingMcp: (n) => `正在连接 ${n} 个 MCP 服务…`,
|
|
101
|
+
extLoaded: (skills, tools) => `扩展已就绪: ${skills} 个 Skill · ${tools} 个 MCP 工具(/skills 查看)`,
|
|
102
|
+
extNone: '',
|
|
103
|
+
extSkillTitle: '已安装的 Skill',
|
|
104
|
+
extSkillEmpty: '没有装 Skill。可以在 g ui → 智能体 → Skill 广场 里挑一个装到当前项目或 g ai。',
|
|
105
|
+
extMcpTitle: '已接入的 MCP 服务',
|
|
106
|
+
extMcpEmpty: '没有接入 MCP 服务。可以在 g ui → 智能体 → MCP 广场 里挑一个装到当前项目或 g ai。',
|
|
107
|
+
extMcpFailed: '连接失败',
|
|
99
108
|
imagePasting: '正在读取剪贴板图片…',
|
|
100
109
|
imageAttached: (n, size) => `📎 图片 #${n} 已附加(${size}),将随下一条消息发送`,
|
|
101
110
|
imageEmpty: '剪贴板中没有图片;可先截图再按 Alt+V,或用 /image <路径> 附加本地图片',
|
|
@@ -145,6 +154,14 @@ const STRINGS = {
|
|
|
145
154
|
thinkingHint: '… first 12 lines shown; enter /think full for future reasoning',
|
|
146
155
|
emptyResponse: 'The model returned no answer. Retry or switch models.',
|
|
147
156
|
chars: 'chars',
|
|
157
|
+
extLoadingMcp: (n) => `Connecting to ${n} MCP server(s)…`,
|
|
158
|
+
extLoaded: (skills, tools) => `Extensions ready: ${skills} skill(s) · ${tools} MCP tool(s) (/skills for details)`,
|
|
159
|
+
extNone: '',
|
|
160
|
+
extSkillTitle: 'Installed skills',
|
|
161
|
+
extSkillEmpty: 'No skills installed. Browse g ui → Agent → Skill marketplace to install one into this project or g ai.',
|
|
162
|
+
extMcpTitle: 'Connected MCP servers',
|
|
163
|
+
extMcpEmpty: 'No MCP servers connected. Browse g ui → Agent → MCP marketplace to install one.',
|
|
164
|
+
extMcpFailed: 'failed',
|
|
148
165
|
imagePasting: 'Reading clipboard image…',
|
|
149
166
|
imageAttached: (n, size) => `📎 Image #${n} attached (${size}); sent with your next message`,
|
|
150
167
|
imageEmpty: 'No image in clipboard; take a screenshot first, or use /image <path>',
|
|
@@ -176,14 +193,28 @@ function modelLabel(m) {
|
|
|
176
193
|
// ──────────────────────────────────────────────
|
|
177
194
|
// 系统 prompt — 明确告知环境、权限边界与工作方式
|
|
178
195
|
// ──────────────────────────────────────────────
|
|
196
|
+
// 顺序固定:基础人设 → 项目说明(AGENTS.md/CLAUDE.md)→ 扩展能力(skill 清单 + MCP 工具)。
|
|
197
|
+
// 把扩展放最后,是因为它是"增量能力",不该插在权限与环境说明中间打断阅读。
|
|
198
|
+
// state 里存着已加载的 AgentExtensions;没加载(如单测直接调)时 extra 为空,行为与改动前一致。
|
|
199
|
+
function extensionSuffix(state) {
|
|
200
|
+
try {
|
|
201
|
+
return state?.extensions?.promptSuffix?.(state.locale) || ''
|
|
202
|
+
} catch {
|
|
203
|
+
return ''
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
async function buildProjectPrompt(options) {
|
|
180
|
-
return buildSystemPrompt(options) + await loadProjectInstructions(options.cwd)
|
|
208
|
+
return buildSystemPrompt(options) + await loadProjectInstructions(options.cwd) + (options.extra || '')
|
|
181
209
|
}
|
|
182
210
|
|
|
183
211
|
function buildSystemPrompt({ cwd, locale, shellDesc }) {
|
|
184
212
|
const zh = !String(locale || '').startsWith('en')
|
|
185
213
|
const now = new Date().toLocaleString()
|
|
186
214
|
const isWin = process.platform === 'win32'
|
|
215
|
+
// 工具数量动态取自 tools.js —— 之前这里写死过"6 个",加 ask_user 时没人想起来改,
|
|
216
|
+
// 就变成了对模型撒谎。数字跟着定义走,以后再加工具不会漏。
|
|
217
|
+
const builtinToolCount = TOOL_DEFINITIONS.length
|
|
187
218
|
if (zh) {
|
|
188
219
|
return `你是 "g ai" —— zen-gitsync CLI 内置的终端编码智能体,通过工具在用户真实电脑上完成编码任务。
|
|
189
220
|
|
|
@@ -207,6 +238,21 @@ ${isWin ? `- 当前是 Windows cmd.exe,以下 Unix 命令**不存在**,用了必
|
|
|
207
238
|
· 查命令路径 → cmd 的 where(不是 which)
|
|
208
239
|
- 必须跑 shell 时优先跨平台写法(如 node -e "..."),别用 Unix 专属命令` : `- 当前是 POSIX 环境,Unix 命令可用`}
|
|
209
240
|
|
|
241
|
+
# 远程仓库(GitHub / Gitee)
|
|
242
|
+
- 问"这个项目关联哪个远端" → run_command 跑 \`git remote -v\`:本地信息,不联网、不依赖任何 CLI,任何环境都能答
|
|
243
|
+
- 问"我账号下有哪些仓库"或某仓库的 PR / Issue → 用官方 CLI。凭据由 CLI 自己保管:
|
|
244
|
+
· GitHub → \`gh\`。列仓库 \`gh repo list --limit 50 --json name,visibility,updatedAt,primaryLanguage\`;
|
|
245
|
+
看详情 \`gh repo view <owner/repo>\`;PR \`gh pr list\`;登录态 \`gh auth status\`
|
|
246
|
+
· Gitee → \`gitee\`。列仓库 \`gitee repo list\`;登录态 \`gitee auth status\`
|
|
247
|
+
(未登录时退出码**仍是 0**,必须看 \`--json\` 里的 status 字段,只看退出码会误判成已登录)
|
|
248
|
+
· 两者默认只覆盖**当前登录账号**;组织仓库、私有仓库可能需要更大的 token scope,拉不到就如实说明,
|
|
249
|
+
不要用其他途径绕过
|
|
250
|
+
· 绝不向用户索要 token —— 也不要让用户把 token 粘进对话
|
|
251
|
+
- CLI 可能没装、或没在当前进程的 PATH 里(装完没重启服务就是这个表现):
|
|
252
|
+
报"不是内部或外部命令" / command not found 时,**不要换写法反复重试**,更不要凭印象编仓库名或目录名。
|
|
253
|
+
直接告诉用户未检测到该 CLI,可在 ZenGitSync 的「远程仓库」页一键安装/登录;
|
|
254
|
+
若只是想回答本项目的问题,退回 \`git remote -v\`
|
|
255
|
+
|
|
210
256
|
# 权限(用户已明确授权,无需反复征求同意)
|
|
211
257
|
- 工作目录内:读写文件、执行命令等所有操作直接执行
|
|
212
258
|
- 其他目录:同样可以读取和修改
|
|
@@ -226,7 +272,8 @@ ${isWin ? `- 当前是 Windows cmd.exe,以下 Unix 命令**不存在**,用了必
|
|
|
226
272
|
|
|
227
273
|
# 与用户交互
|
|
228
274
|
- 需要向用户确认、提问或汇报重要决策时,直接用普通文本输出 —— 用户能实时看到你的文本;
|
|
229
|
-
|
|
275
|
+
需要暂停当前任务并等待用户决定或补充信息时,调用 ask_user,不要猜测或只在普通文本里提问
|
|
276
|
+
不要调用不存在的工具,内置工具就是上面列出的 ${builtinToolCount} 个(若用户装了 MCP,你的工具表里还会多出 mcp__ 开头的工具)
|
|
230
277
|
- 用户可能通过 Alt+V 或 /image 附加图片:图片以 image_url 部件出现在 user 消息里;
|
|
231
278
|
如果当前模型不支持视觉(带图请求报错),提醒用户换用支持视觉的模型
|
|
232
279
|
- 发现高风险或状态不一致的情况(例如版本号 / git tag / CHANGELOG 对不上、发布前环境异常、
|
|
@@ -258,6 +305,24 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
|
|
|
258
305
|
· Find executable → cmd's where (not which)
|
|
259
306
|
- For shell one-offs, prefer cross-platform forms like node -e "..."` : `- POSIX environment: Unix commands are available`}
|
|
260
307
|
|
|
308
|
+
# Remote repositories (GitHub / Gitee)
|
|
309
|
+
- "Which remote does this project point at?" → run_command \`git remote -v\`: local info, no network, no CLI needed
|
|
310
|
+
- "Which repos do I have?" or a repo's PRs / issues → use the official CLI. The CLI owns the credentials:
|
|
311
|
+
· GitHub → \`gh\`. List repos \`gh repo list --limit 50 --json name,visibility,updatedAt,primaryLanguage\`;
|
|
312
|
+
details \`gh repo view <owner/repo>\`; PRs \`gh pr list\`; auth state \`gh auth status\`
|
|
313
|
+
· Gitee → \`gitee\`. List repos \`gitee repo list\`; auth state \`gitee auth status\`
|
|
314
|
+
(the exit code is still 0 when logged out — read the status field from \`--json\`, or you will
|
|
315
|
+
wrongly report "logged in")
|
|
316
|
+
· Both default to the **currently authenticated account only**; org and private repos may need
|
|
317
|
+
broader token scopes. If you cannot see them, say so plainly — do not route around it
|
|
318
|
+
· Never ask the user for a token, and never have them paste one into the conversation
|
|
319
|
+
- The CLI may not be installed, or missing from this process's PATH (that is what "installed but the
|
|
320
|
+
server cannot find it" looks like):
|
|
321
|
+
on "not recognized" / command not found, do NOT retry with a different spelling and do NOT invent
|
|
322
|
+
repo or directory names. Tell the user the CLI was not found and point them to ZenGitSync's
|
|
323
|
+
"Remote repositories" page (one-click install / login); if you only need to answer for this project,
|
|
324
|
+
fall back to \`git remote -v\`
|
|
325
|
+
|
|
261
326
|
# Permissions (explicitly granted by the user — do not keep asking)
|
|
262
327
|
- Inside the working directory: read/write files and run commands directly
|
|
263
328
|
- Other directories: may also be read and modified
|
|
@@ -265,6 +330,7 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
|
|
|
265
330
|
|
|
266
331
|
# How to work
|
|
267
332
|
- Act first, ask later: use tools (list_files / read_file / search_text / run_command) instead of asking the user
|
|
333
|
+
- When a decision or missing detail must come from the user, call ask_user. Do not guess or merely describe a question in plain text.
|
|
268
334
|
- After editing code, verify: run tests, builds, or at least a syntax check via run_command
|
|
269
335
|
- Prefer edit_file for precise replacements; read_file first, old_string must match the file exactly
|
|
270
336
|
- Read large files in segments (offset/limit)
|
|
@@ -275,7 +341,7 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
|
|
|
275
341
|
- For a brief overview, inspect only the necessary structure and entry points; keep exploration proportional to the request
|
|
276
342
|
|
|
277
343
|
# Talking to the user
|
|
278
|
-
- When you need to confirm something, ask a question, or report an important decision, just write plain text — the user sees your output in real time. Never call tools that do not exist;
|
|
344
|
+
- When you need to confirm something, ask a question, or report an important decision, just write plain text — the user sees your output in real time. Use ask_user when the task must pause for an answer. Never call tools that do not exist; the built-in set is the ${builtinToolCount} tools listed above (if the user installed MCP servers, extra tools prefixed with mcp__ will also appear in your tool list)
|
|
279
345
|
- The user can attach images (Alt+V or /image); they arrive as image_url parts in your user messages. If the current model rejects images (no vision support), tell the user to switch to a vision-capable model
|
|
280
346
|
- When you spot high-risk or inconsistent state (version number / git tag / CHANGELOG mismatch, abnormal release environment, unexpected repo state), explain the finding and its impact in text, then STOP and wait for the user's decision instead of proceeding with destructive operations
|
|
281
347
|
|
|
@@ -285,54 +351,10 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
|
|
|
285
351
|
}
|
|
286
352
|
|
|
287
353
|
// ──────────────────────────────────────────────
|
|
288
|
-
//
|
|
354
|
+
// 消息准备(消毒 / 旧图片降级)统一实现在 context.js —— CLI 与 Web 智能体面板
|
|
355
|
+
// 共用同一份,避免两侧各写一遍后行为分叉。这里 re-export 保持既有引用与测试不断。
|
|
289
356
|
// ──────────────────────────────────────────────
|
|
290
|
-
export
|
|
291
|
-
for (const m of messages) {
|
|
292
|
-
if (m == null || typeof m !== 'object') continue
|
|
293
|
-
if (m.content === null || m.content === undefined) {
|
|
294
|
-
if (m.role === 'assistant') m.content = null
|
|
295
|
-
continue
|
|
296
|
-
}
|
|
297
|
-
if (typeof m.content !== 'string') continue
|
|
298
|
-
const trimmed = m.content.trim()
|
|
299
|
-
if (trimmed === '') {
|
|
300
|
-
if (m.role === 'assistant') {
|
|
301
|
-
m.content = null
|
|
302
|
-
} else if (m.role === 'tool') {
|
|
303
|
-
m.content = '(no output)'
|
|
304
|
-
} else if (m.role === 'user') {
|
|
305
|
-
m.content = ' '
|
|
306
|
-
}
|
|
307
|
-
continue
|
|
308
|
-
}
|
|
309
|
-
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
310
|
-
m.content = null
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return messages
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ──────────────────────────────────────────────
|
|
317
|
-
// 多模态历史:base64 图片很占上下文,只保留"最近一条带图消息"里的图片,
|
|
318
|
-
// 更早消息里的 image_url 部件降级为文字占位(模型仍知道这里曾有图)
|
|
319
|
-
// ──────────────────────────────────────────────
|
|
320
|
-
export function stripStaleImages(messages, locale) {
|
|
321
|
-
const placeholder = String(locale || '').startsWith('en')
|
|
322
|
-
? '[image omitted from history]'
|
|
323
|
-
: '[图片已从历史中省略]'
|
|
324
|
-
let seenLatest = false
|
|
325
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
326
|
-
const m = messages[i]
|
|
327
|
-
if (m?.role !== 'user' || !Array.isArray(m.content)) continue
|
|
328
|
-
const hasImage = m.content.some(p => p?.type === 'image_url')
|
|
329
|
-
if (!hasImage) continue
|
|
330
|
-
if (!seenLatest) { seenLatest = true; continue }
|
|
331
|
-
m.content = m.content.map(p => p?.type === 'image_url'
|
|
332
|
-
? { type: 'text', text: placeholder }
|
|
333
|
-
: p)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
357
|
+
export { sanitizeMessages, stripStaleImages } from './context.js'
|
|
336
358
|
|
|
337
359
|
// ──────────────────────────────────────────────
|
|
338
360
|
// 单轮 agent 循环:用户一句话 → 流式输出 → 工具调用 → 再调用模型 … 直到模型给出最终文本
|
|
@@ -350,6 +372,7 @@ function printSlashHelp(t, locale) {
|
|
|
350
372
|
' /think compact 预览前 12 行思考',
|
|
351
373
|
' /think off 隐藏思考;/think 切换完整与隐藏',
|
|
352
374
|
' /tools [模式] compact 精简 / full 完整工具输出',
|
|
375
|
+
' /skills 查看已安装的 Skill 与已接入的 MCP 服务',
|
|
353
376
|
' /stats 查看上一轮与会话累计用量、耗时',
|
|
354
377
|
' /new 开启新对话',
|
|
355
378
|
' /resume 选择并恢复之前的对话',
|
|
@@ -370,6 +393,7 @@ function printSlashHelp(t, locale) {
|
|
|
370
393
|
' /think compact Preview the first 12 thinking lines',
|
|
371
394
|
' /think off Hide thinking; /think toggles full / hidden',
|
|
372
395
|
' /tools [mode] compact / full tool output',
|
|
396
|
+
' /skills List installed skills and connected MCP servers',
|
|
373
397
|
' /stats Last turn timing and session token usage',
|
|
374
398
|
' /new Start a new conversation',
|
|
375
399
|
' /resume Choose and resume a previous conversation',
|
|
@@ -388,7 +412,7 @@ export async function handleSlashCommand(state, input, t) {
|
|
|
388
412
|
const [cmd, ...rest] = input.split(/\s+/)
|
|
389
413
|
const arg = rest.join(' ').trim()
|
|
390
414
|
const zh = !String(state.locale).startsWith('en')
|
|
391
|
-
if (state.busy && !['/help', '/stats', '/think', '/tools', '/exit', '/quit'].includes(cmd)) {
|
|
415
|
+
if (state.busy && !['/help', '/stats', '/think', '/tools', '/skills', '/mcp', '/exit', '/quit'].includes(cmd)) {
|
|
392
416
|
printDim(t.busy)
|
|
393
417
|
return 'ok'
|
|
394
418
|
}
|
|
@@ -467,7 +491,7 @@ export async function handleSlashCommand(state, input, t) {
|
|
|
467
491
|
state.sessionStats = session.sessionStats || null
|
|
468
492
|
const systemMessage = {
|
|
469
493
|
role: 'system',
|
|
470
|
-
content: await buildProjectPrompt({ cwd: state.ctx.cwd, locale: state.locale, shellDesc: state.shellDesc }),
|
|
494
|
+
content: await buildProjectPrompt({ cwd: state.ctx.cwd, locale: state.locale, shellDesc: state.shellDesc, extra: extensionSuffix(state) }),
|
|
471
495
|
}
|
|
472
496
|
if (state.messages[0]?.role === 'system') state.messages[0] = systemMessage
|
|
473
497
|
else state.messages.unshift(systemMessage)
|
|
@@ -499,6 +523,26 @@ export async function handleSlashCommand(state, input, t) {
|
|
|
499
523
|
printOk(zh ? `后续工具结果:${state.fullTools ? '完整' : '精简'}` : `Future tool output: ${state.fullTools ? 'full' : 'compact'}`)
|
|
500
524
|
return 'ok'
|
|
501
525
|
}
|
|
526
|
+
if (cmd === '/skills' || cmd === '/mcp') {
|
|
527
|
+
const info = state.extensions?.summary(state.locale) || { skills: [], servers: [] }
|
|
528
|
+
console.log(chalk.bold(`\n ${t.extSkillTitle}`))
|
|
529
|
+
if (info.skills.length === 0) printDim(t.extSkillEmpty)
|
|
530
|
+
for (const skill of info.skills) {
|
|
531
|
+
const scope = skill.scope === 'project' ? (zh ? '项目' : 'project') : (zh ? '全局' : 'global')
|
|
532
|
+
console.log(` · ${chalk.cyan(skill.name)} ${chalk.dim(`[${scope}]`)}${skill.description ? chalk.dim(` — ${skill.description}`) : ''}`)
|
|
533
|
+
}
|
|
534
|
+
console.log(chalk.bold(`\n ${t.extMcpTitle}`))
|
|
535
|
+
if (info.servers.length === 0) printDim(t.extMcpEmpty)
|
|
536
|
+
for (const server of info.servers) {
|
|
537
|
+
if (server.error) {
|
|
538
|
+
console.log(` · ${chalk.red(server.id)} ${chalk.dim(`— ${t.extMcpFailed}: ${server.error}`)}`)
|
|
539
|
+
} else {
|
|
540
|
+
console.log(` · ${chalk.cyan(server.id)} ${chalk.dim(`— ${server.tools} ${zh ? '个工具' : 'tools'} (${server.command})`)}`)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
console.log('')
|
|
544
|
+
return 'ok'
|
|
545
|
+
}
|
|
502
546
|
if (cmd === '/stats') {
|
|
503
547
|
if (state.lastTurnStats) printTurnSummary(state.lastTurnStats, { locale: state.locale, session: state.sessionStats })
|
|
504
548
|
else printDim(zh ? '完成一轮对话后显示用量与耗时。' : 'Usage and timing appear after a turn completes.')
|
|
@@ -602,8 +646,10 @@ export async function handleSlashCommand(state, input, t) {
|
|
|
602
646
|
const st = await fsp.stat(target)
|
|
603
647
|
if (!st.isDirectory()) throw new Error('not a dir')
|
|
604
648
|
state.ctx.cwd = target
|
|
605
|
-
//
|
|
606
|
-
|
|
649
|
+
// 项目级 skill 与 .mcp.json 跟着工作目录走:先重载扩展(会关掉旧目录起的 MCP 子进程),
|
|
650
|
+
// 再重建首条 system 消息 —— 顺序反了会把旧目录的 skill 清单写进新目录的提示词。
|
|
651
|
+
await state.extensions?.reload({ cwd: target, locale: state.locale, onWarn: printWarn })
|
|
652
|
+
state.messages[0] = { role: 'system', content: await buildProjectPrompt({ cwd: target, locale: state.locale, shellDesc: state.shellDesc, extra: extensionSuffix(state) }) }
|
|
607
653
|
await state.persistSession?.()
|
|
608
654
|
printOk(t.cdOk(target))
|
|
609
655
|
} catch {
|
|
@@ -660,12 +706,21 @@ export async function runAiAgent(argv = []) {
|
|
|
660
706
|
? 'cmd.exe (Windows CMD)'
|
|
661
707
|
: (process.env.SHELL || '/bin/sh')
|
|
662
708
|
|
|
709
|
+
// 加载扩展(Skill 清单 + MCP 服务)。MCP 要起子进程,npx 首次还得下载包,
|
|
710
|
+
// 所以这一步可能明显停顿 —— 有 MCP 时先提示一句。任何失败都只降级并打印原因。
|
|
711
|
+
const extensions = await AgentExtensions.load({
|
|
712
|
+
cwd,
|
|
713
|
+
locale,
|
|
714
|
+
onWarn: printWarn,
|
|
715
|
+
onNotice: (count) => printDim(t.extLoadingMcp(count)),
|
|
716
|
+
})
|
|
717
|
+
|
|
663
718
|
const state = {
|
|
664
719
|
// 会话持久化:CLI 对话也保存到 ~/.zen-gitsync/agent-sessions/,
|
|
665
720
|
// 在 Web UI 智能体 tab 中可见(带 CLI 标记)
|
|
666
721
|
sessionId: genSessionId(),
|
|
667
722
|
sessionCreatedAt: new Date().toISOString(),
|
|
668
|
-
messages: [{ role: 'system', content: await buildProjectPrompt({ cwd, locale, shellDesc }) }],
|
|
723
|
+
messages: [{ role: 'system', content: await buildProjectPrompt({ cwd, locale, shellDesc, extra: extensions.promptSuffix(locale) }) }],
|
|
669
724
|
ctx: {
|
|
670
725
|
cwd,
|
|
671
726
|
onChild: (child) => {
|
|
@@ -690,11 +745,11 @@ export async function runAiAgent(argv = []) {
|
|
|
690
745
|
fullTools: false,
|
|
691
746
|
lastTurnStats: null,
|
|
692
747
|
sessionStats: null,
|
|
693
|
-
prepareMessages: (messages) => { stripStaleImages(messages, locale); sanitizeMessages(messages) },
|
|
694
748
|
pendingImages: [], // Alt+V / /image 附加的待发送图片 [{path, bytes}]
|
|
695
749
|
pasting: false, // 剪贴板读取进行中(防 Alt+V 连打并发)
|
|
696
750
|
inWizard: false, // /addmodel 交互式向导进行中:忽略 REPL 的 line 事件
|
|
697
751
|
rl: null, // REPL readline 引用(供 /addmodel 复用做 rl.question)
|
|
752
|
+
extensions, // 已加载的 Skill + MCP 扩展(见 extensions.js)
|
|
698
753
|
}
|
|
699
754
|
|
|
700
755
|
// 持久化当前会话到磁盘(供 Web UI 读取)
|
|
@@ -733,13 +788,21 @@ export async function runAiAgent(argv = []) {
|
|
|
733
788
|
state.cancelRequested = true
|
|
734
789
|
try { state.abortController?.abort() } catch (_) {}
|
|
735
790
|
await terminateCommand(state.currentChild)
|
|
791
|
+
// MCP 子进程必须显式收掉:它们挂在 stdio 上,不收会让进程无法自然退出
|
|
792
|
+
await extensions.close()
|
|
736
793
|
await persistSession()
|
|
737
794
|
})
|
|
738
795
|
|
|
796
|
+
// 扩展摘要:一条 dim 行,没有装扩展时完全静默(extNone 为空串)
|
|
797
|
+
const extSummary = extensions.skills.length || extensions.tools.length
|
|
798
|
+
? t.extLoaded(extensions.skills.length, extensions.tools.length)
|
|
799
|
+
: t.extNone
|
|
800
|
+
|
|
739
801
|
// 单发模式:g ai "帮我改个 bug"(位置参数拼成 prompt,跑一轮就退出)
|
|
740
802
|
const oneShot = argv.filter(a => !a.startsWith('-')).join(' ').trim()
|
|
741
803
|
if (oneShot) {
|
|
742
804
|
console.log(chalk.dim(`[${t.bannerModel}] ${modelLabel(model)} · [${t.bannerCwd}] ${cwd}`))
|
|
805
|
+
if (extSummary) printDim(extSummary)
|
|
743
806
|
state.abortController = new AbortController()
|
|
744
807
|
const stats = await runAgentTurn(state, oneShot, t)
|
|
745
808
|
if (stats.status === 'completed') printDim(t.oneShotDone)
|
|
@@ -757,6 +820,7 @@ export async function runAiAgent(argv = []) {
|
|
|
757
820
|
cwdText: t.bannerCwd,
|
|
758
821
|
tip: t.bannerTip,
|
|
759
822
|
})
|
|
823
|
+
if (extSummary) printDim(extSummary)
|
|
760
824
|
|
|
761
825
|
const rl = readline.createInterface({
|
|
762
826
|
input: process.stdin,
|
|
@@ -767,6 +831,94 @@ export async function runAiAgent(argv = []) {
|
|
|
767
831
|
})
|
|
768
832
|
state.rl = rl // 供 /addmodel 等需要 rl.question 的斜杠命令复用
|
|
769
833
|
|
|
834
|
+
// ask_user pauses the tool loop inside the same readline session. The REPL
|
|
835
|
+
// line handler is bypassed while this small wizard owns the input line.
|
|
836
|
+
state.ctx.askUser = ({ question, options = [], allowFreeText = true }) => {
|
|
837
|
+
const zh = !String(state.locale || '').startsWith('en')
|
|
838
|
+
const safeOptions = Array.isArray(options) ? options.filter(Boolean).slice(0, 20) : []
|
|
839
|
+
state.inWizard = true
|
|
840
|
+
if (safeOptions.length > 0) {
|
|
841
|
+
console.log(chalk.cyan(`\n${question}`))
|
|
842
|
+
safeOptions.forEach((option, index) => console.log(` ${chalk.cyan(String(index + 1))}. ${option}`))
|
|
843
|
+
} else {
|
|
844
|
+
console.log(chalk.cyan(`\n${question}`))
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return new Promise(resolve => {
|
|
848
|
+
let settled = false
|
|
849
|
+
const questionController = new AbortController()
|
|
850
|
+
const parentSignal = state.abortController?.signal
|
|
851
|
+
const cleanup = () => {
|
|
852
|
+
rl.removeListener?.('close', onClose)
|
|
853
|
+
rl.removeListener?.('operationCancel', onCancel)
|
|
854
|
+
rl.input?.removeListener?.('keypress', onKeypressCancel)
|
|
855
|
+
parentSignal?.removeEventListener?.('abort', onParentAbort)
|
|
856
|
+
}
|
|
857
|
+
const finish = answer => {
|
|
858
|
+
if (settled) return
|
|
859
|
+
settled = true
|
|
860
|
+
cleanup()
|
|
861
|
+
state.inWizard = false
|
|
862
|
+
resolve(answer)
|
|
863
|
+
}
|
|
864
|
+
const cancel = () => {
|
|
865
|
+
if (settled) return
|
|
866
|
+
questionController.abort()
|
|
867
|
+
finish('Cancelled by user; the question was not answered.')
|
|
868
|
+
}
|
|
869
|
+
const onClose = () => cancel()
|
|
870
|
+
const onCancel = () => {
|
|
871
|
+
state.cancelRequested = true
|
|
872
|
+
try { state.abortController?.abort() } catch (_) {}
|
|
873
|
+
cancel()
|
|
874
|
+
}
|
|
875
|
+
const onParentAbort = () => cancel()
|
|
876
|
+
const onKeypressCancel = (_str, key) => {
|
|
877
|
+
if (!key?.ctrl || key.name !== 'c') return
|
|
878
|
+
key.name = 'escape'
|
|
879
|
+
key.sequence = ''
|
|
880
|
+
key.ctrl = false
|
|
881
|
+
key.meta = false
|
|
882
|
+
key.shift = false
|
|
883
|
+
onCancel()
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
rl.once('close', onClose)
|
|
887
|
+
rl.once('operationCancel', onCancel)
|
|
888
|
+
rl.input?.prependListener?.('keypress', onKeypressCancel)
|
|
889
|
+
parentSignal?.addEventListener?.('abort', onParentAbort, { once: true })
|
|
890
|
+
|
|
891
|
+
const promptAnswer = () => {
|
|
892
|
+
const hint = safeOptions.length > 0
|
|
893
|
+
? (allowFreeText ? (zh ? '请输入序号或直接输入回答' : 'Choose a number or type an answer') : (zh ? '请输入序号' : 'Choose a number'))
|
|
894
|
+
: (zh ? '请输入回答' : 'Type your answer')
|
|
895
|
+
rl.question(`${hint}: `, { signal: questionController.signal }, answer => {
|
|
896
|
+
if (settled) return
|
|
897
|
+
const trimmed = String(answer || '').trim()
|
|
898
|
+
if (safeOptions.length > 0) {
|
|
899
|
+
const index = Number.parseInt(trimmed, 10)
|
|
900
|
+
if (Number.isInteger(index) && index >= 1 && index <= safeOptions.length) {
|
|
901
|
+
finish(safeOptions[index - 1])
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
if (!allowFreeText || !trimmed) {
|
|
905
|
+
console.log(chalk.yellow(zh ? `请输入 1-${safeOptions.length} 之间的序号` : `Enter a number from 1 to ${safeOptions.length}`))
|
|
906
|
+
promptAnswer()
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (!trimmed) {
|
|
911
|
+
console.log(chalk.yellow(zh ? '回答不能为空' : 'Answer cannot be empty'))
|
|
912
|
+
promptAnswer()
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
finish(trimmed)
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
promptAnswer()
|
|
919
|
+
})
|
|
920
|
+
}
|
|
921
|
+
|
|
770
922
|
// 输入提示符管理。
|
|
771
923
|
//
|
|
772
924
|
// readline 的 _refreshLine() 用 prevRows 追踪“提示符占了几行”,
|
package/src/cli/ai/context.js
CHANGED
|
@@ -64,6 +64,64 @@ export function buildRequestMessages(messages, { maxMessages = 40, maxChars = 80
|
|
|
64
64
|
return result
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Provider compatibility. Some providers (Moonshot/Kimi, Zhipu, Volcengine, MiniMax…)
|
|
68
|
+
// reject an assistant message whose content is empty while tool_calls are present
|
|
69
|
+
// ("chat content is empty (2013)"), and implementations disagree on which shapes are
|
|
70
|
+
// legal. Runs on the request copy only — what the model actually produced stays on disk.
|
|
71
|
+
// - assistant with tool_calls → content forced to null
|
|
72
|
+
// - assistant with blank content → null
|
|
73
|
+
// - user with blank content → a single space (null is rejected by some providers)
|
|
74
|
+
// - tool with blank content → '(no output)' (otherwise it can be dropped while serializing)
|
|
75
|
+
export function sanitizeMessages(messages) {
|
|
76
|
+
for (const m of messages) {
|
|
77
|
+
if (m == null || typeof m !== 'object') continue
|
|
78
|
+
// Non-string content (multimodal user parts, already null) is left alone.
|
|
79
|
+
if (m.content === null || m.content === undefined) {
|
|
80
|
+
if (m.role === 'assistant') m.content = null
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (typeof m.content !== 'string') continue
|
|
84
|
+
if (m.content.trim() === '') {
|
|
85
|
+
if (m.role === 'assistant') m.content = null
|
|
86
|
+
else if (m.role === 'tool') m.content = '(no output)'
|
|
87
|
+
else if (m.role === 'user') m.content = ' '
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
91
|
+
m.content = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return messages
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Base64 images dominate the payload, so only the newest image-bearing user message
|
|
98
|
+
// keeps its images; older ones degrade to a text placeholder (the model still knows
|
|
99
|
+
// an image was there). Reassigns `content` on the request copy, never on the transcript.
|
|
100
|
+
export function stripStaleImages(messages, locale) {
|
|
101
|
+
const placeholder = String(locale || '').startsWith('en')
|
|
102
|
+
? '[image omitted from history]'
|
|
103
|
+
: '[图片已从历史中省略]'
|
|
104
|
+
let seenLatest = false
|
|
105
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
106
|
+
const m = messages[i]
|
|
107
|
+
if (m?.role !== 'user' || !Array.isArray(m.content)) continue
|
|
108
|
+
if (!m.content.some(p => p?.type === 'image_url')) continue
|
|
109
|
+
if (!seenLatest) { seenLatest = true; continue }
|
|
110
|
+
m.content = m.content.map(p => p?.type === 'image_url' ? { type: 'text', text: placeholder } : p)
|
|
111
|
+
}
|
|
112
|
+
return messages
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Single entry point for every outgoing payload. The CLI (`turn.js`) and the GUI agent
|
|
116
|
+
// panel (`agentChat.js`) must both call this instead of assembling their own copy —
|
|
117
|
+
// that is the only thing keeping the two from drifting apart again.
|
|
118
|
+
// Returns a fresh array; the caller's transcript is never modified.
|
|
119
|
+
export function prepareRequestMessages(messages, { locale, maxMessages = 40, maxChars = 80000 } = {}) {
|
|
120
|
+
const copy = buildRequestMessages(messages, { maxMessages, maxChars })
|
|
121
|
+
stripStaleImages(copy, locale)
|
|
122
|
+
return sanitizeMessages(copy)
|
|
123
|
+
}
|
|
124
|
+
|
|
67
125
|
// Only load explicitly named project instruction files, with bounded local redirects.
|
|
68
126
|
export async function loadProjectInstructions(cwd) {
|
|
69
127
|
const visited = new Set(), sections = []
|