zen-gitsync 2.17.4 → 2.17.6
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 +13 -0
- package/package.json +2 -1
- package/scripts/release.js +93 -66
- package/src/aiCommit.js +6 -3
- package/src/cli/ai/agent.js +23 -8
- package/src/cli/ai/modelSetup.js +6 -8
- package/src/config.js +57 -4
- package/src/ui/public/assets/{AgentView-BkNdHFRW.js → AgentView-BC3F0F6m.js} +3 -3
- package/src/ui/public/assets/AgentView-CzUeyQfR.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-pJ33fsvn.js → AppVersionBadge-DV6KuLqb.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-Ixv2uRS6.js → BranchSelector-DGXDAOkR.js} +1 -1
- package/src/ui/public/assets/{CommitForm-DqJvDzSw.js → CommitForm-BYBj7UUa.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-DfT04-V7.js → CommonDialog-DWMCeujE.js} +1 -1
- package/src/ui/public/assets/{EditorView-Br0JBESF.js → EditorView-D-TEQZso.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-C3DxCngU.js → FileDiffViewer-C4ZANajI.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-C_FOK1_9.js → FlowExecutionViewer-DUv7mBuG.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-wu9ieYbz.js → FlowOrchestrationWorkspace-BiSuvzL7.js} +1 -1
- package/src/ui/public/assets/{IconButton-CQB_swX6.js → IconButton-BltPKVbV.js} +1 -1
- package/src/ui/public/assets/{LogList-CSpeYuAd.js → LogList-BY1Haq9r.js} +1 -1
- package/src/ui/public/assets/{MindmapView-8AHGHv9g.js → MindmapView-BKsvCnYv.js} +1 -1
- package/src/ui/public/assets/{MonitorView-Do6DcGd9.js → MonitorView-B3_NaJRy.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DwrukeUb.js → ProjectStartupButton-DP3eDVRp.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-Dhdwvb2y.js → RemoteManagerDialog-Bl9b_iRv.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-DjKHYGRi.js → RemoteRepoCard-TQ3DxDnq.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-COuEBUTw.js → SourceMapView-BF-Mzv3V.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-C6aJB-3Q.js → SvgIcon-DxazW7dI.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-Th1ibeIh.js → TemplateManager-BN6gWFWX.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-B8F60XEE.js → UserInputNode-CPwvIfP6.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-CDu61WWs.js +13 -0
- package/src/ui/public/assets/WorkbenchView-DEejub9Z.css +1 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-B7A-NgBp.js +19 -0
- package/src/ui/public/assets/configStore-Dx84lvTX.js +1 -0
- package/src/ui/public/assets/{gitStore-QokQdW7C.js → gitStore-CYrdjXPU.js} +1 -1
- package/src/ui/public/assets/index-DNHOYMCg.js +24 -0
- package/src/ui/public/assets/index-Dsfmp6w8.css +1 -0
- package/src/ui/public/assets/{previewSandbox-CA1fFxdo.js → previewSandbox-D_j5PWX1.js} +0 -0
- package/src/ui/public/assets/{toolsStore-Bu8Ujw_A.js → toolsStore-BiC2mmC3.js} +1 -1
- package/src/ui/public/assets/{vendor-CwZs6S_i.js → vendor-92fKgtnz.js} +7 -7
- package/src/ui/public/assets/{vendor-_6pgW9Z2.css → vendor-B1UrVTTI.css} +1 -1
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/routes/codeAnalysis.js +6 -3
- package/src/ui/server/routes/config.js +32 -6
- package/src/ui/server/routes/workbench/agentChat.js +34 -8
- package/src/ui/server/routes/workbench/llmClient.js +15 -8
- package/src/utils/aiEndpoint.js +231 -0
- package/src/utils/aiEndpoint.test.js +175 -0
- package/src/ui/public/assets/AgentView-BsN9y4Kg.css +0 -1
- package/src/ui/public/assets/WorkbenchView-CML-A0y6.css +0 -1
- package/src/ui/public/assets/WorkbenchView-jutdH9WI.js +0 -13
- package/src/ui/public/assets/_plugin-vue_export-helper-nUAWISuX.js +0 -19
- package/src/ui/public/assets/configStore-CRYvJSvr.js +0 -1
- package/src/ui/public/assets/index-BLO9MkBC.js +0 -24
- package/src/ui/public/assets/index-ClqvC4kJ.css +0 -1
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) |
|
|
@@ -454,6 +455,12 @@ to numeric input. `Esc` or `Ctrl+C` cancels the wizard cleanly.
|
|
|
454
455
|
|
|
455
456
|
In-session commands: `/help`, `/model`, `/addmodel`, `/cd <path>`, `/image [path]`, `/think`, `/clear`, `/exit`.
|
|
456
457
|
|
|
458
|
+
Tool-call budget: one message may trigger up to N tool calls in a row before the turn is
|
|
459
|
+
force-ended with a "max tool iterations reached" notice (send another message to continue).
|
|
460
|
+
N defaults to **200** and is configurable in **Settings → AI models → Agent Runtime**
|
|
461
|
+
(`aiMaxToolIterations` in `~/.zen-gitsync/config.json`, range 1–2000) — the Web agent shares
|
|
462
|
+
the same value.
|
|
463
|
+
|
|
457
464
|
Images: press `Alt+V` in the REPL to paste a clipboard image (screenshot), or attach a
|
|
458
465
|
local file with `/image <path>`; images are sent as multimodal `image_url` parts with your
|
|
459
466
|
next message (requires a vision-capable model). `/image` alone lists pending images,
|
|
@@ -973,6 +980,7 @@ Activity Bar 中的机器人图标视图,可直接在浏览器中与内置 AI
|
|
|
973
980
|
| 流式对话 | 基于 SSE 的实时流式输出,包含思考过程、正文内容、工具调用和工具结果的内联渲染 |
|
|
974
981
|
| 工具调用展示 | 每次工具调用(run_command、read_file、edit_file、list_files、search_text、write_file)以可折叠卡片形式展示,含参数预览和执行结果 |
|
|
975
982
|
| 会话持久化 | 所有对话保存为 JSON 文件到 `~/.zen-gitsync/agent-sessions/`;CLI 智能体(`g ai`)写入同一目录,Web 端与 CLI 端会话统一管理 |
|
|
983
|
+
| 单轮工具调用上限 | 一条消息内智能体最多连续调用多少次工具(默认 **200**,可调范围 1–2000)。在 **设置 → AI 模型配置 → 智能体运行时** 中修改;达到上限本轮会被强制结束并提示再发一条消息继续。CLI 智能体共用同一项设置 |
|
|
976
984
|
| 预设问题 | 开场界面提供快捷按钮(查看项目结构、分析代码质量、写测试、Git 状态检查)|
|
|
977
985
|
| 停止生成 | 流式输出期间出现浮动停止按钮;中止 LLM 请求及正在运行的子进程 |
|
|
978
986
|
| 主题同步 | 对话区域跟随 GUI 当前主题(浅色 / 深色 / 自动)|
|
|
@@ -1038,6 +1046,11 @@ $ g ai --model=2 # 使用第 2 个已配置的模型(序号或
|
|
|
1038
1046
|
|
|
1039
1047
|
会话内命令:`/help`、`/model`、`/addmodel`、`/cd <路径>`、`/image [路径]`、`/think`、`/clear`、`/exit`。
|
|
1040
1048
|
|
|
1049
|
+
工具调用预算:一条消息内智能体最多连续调用 N 次工具,触顶后本轮被强制结束并提示
|
|
1050
|
+
"已达单轮最大工具调用次数",再发一条消息即可继续。N 默认 **200**,可在
|
|
1051
|
+
**设置 → AI 模型配置 → 智能体运行时** 修改(即 `~/.zen-gitsync/config.json` 的
|
|
1052
|
+
`aiMaxToolIterations`,范围 1–2000),Web 端智能体共用同一项设置。
|
|
1053
|
+
|
|
1041
1054
|
图片:在 REPL 中按 `Alt+V` 粘贴剪贴板图片(截图),或用 `/image <路径>` 附加本地图片;
|
|
1042
1055
|
图片以多模态 `image_url` 部件随下一条消息发送(需视觉模型)。单独 `/image` 查看待发送图片,
|
|
1043
1056
|
`/image clear` 清除。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-gitsync",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.6",
|
|
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",
|
|
@@ -37,6 +37,7 @@
|
|
|
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
39
|
"verify:wb-responsive": "node scripts/verify-wb-responsive.cjs",
|
|
40
|
+
"verify:wb-editor-persist": "node scripts/verify-workbench-editor-persist.cjs",
|
|
40
41
|
"e2e": "cd src/ui/client && npx playwright test",
|
|
41
42
|
"e2e:headed": "cd src/ui/client && npx playwright test --headed",
|
|
42
43
|
"e2e:install": "cd src/ui/client && npx playwright install chromium",
|
package/scripts/release.js
CHANGED
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
* npm run release -- --skip-self-update # 发布后不自动 npm install -g zen-gitsync
|
|
27
27
|
* npm run release -- --skip-push # 只发布到 npm,不 push git
|
|
28
28
|
* npm run release -- --dry-run # 只打印计划,不真正改 package.json / commit / publish
|
|
29
|
-
* npm run release -- --poll-interval=20 --poll-timeout=600 #
|
|
29
|
+
* npm run release -- --poll-interval=20 --poll-timeout=600 # 调自更新重试节奏(秒)
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* 发布后自更新:反复尝试 `npm install -g zen-gitsync@<版本>` 并校验全局版本;
|
|
32
|
+
* 精确版本号解析不到(ETARGET)时改用 tarball URL 直连兜底。
|
|
33
|
+
* 原因见 tarballUrl() / selfUpdateGlobal() 处注释 —— publish 成功不等于 packument 立即可见。
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
36
|
import fs from 'node:fs'
|
|
@@ -67,12 +68,27 @@ function readNumberArg(name, fallback) {
|
|
|
67
68
|
// 仍是 2.17.2,versions 里没有 2.17.3;packument 响应头
|
|
68
69
|
// `Cache-Control: public, max-age=300` —— 光 CDN 层就可能滞后 5 分钟。
|
|
69
70
|
// 所以"publish 完立刻 npm install -g"必然装回旧版(实测 `npm ls -g` 停在 2.17.2)。
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
71
|
+
//
|
|
72
|
+
// 再实测(2026-09-20, v2.17.4):滞后不止 600s。那次脚本"先等 dist-tags 翻牌"一直等到
|
|
73
|
+
// 超时(600s 里 latest 全程是 2.17.3),超时后再 install 依旧 ETARGET —— 因为
|
|
74
|
+
// `npm view dist-tags` 和 `npm install pkg@ver` 读的是**同一份 packument**:
|
|
75
|
+
// 等它、用它,走的是同一条被缓存的路。只把超时 600s 往上加,只是把失败往后推。
|
|
76
|
+
//
|
|
77
|
+
// 兜底:tarball URL 是不可变资源,发布完成即可取,且不经过 packument 的那层缓存
|
|
78
|
+
// (见 tarballUrl 注释)。所以自更新不再把安装卡在 dist-tags 后面,而是
|
|
79
|
+
// "反复尝试安装 + 校验全局版本",精确版本号解析不到时自动改用 tarball 直连。
|
|
80
|
+
// 默认 15s 一轮、上限 600s;可用 `--poll-interval=<秒>` / `--poll-timeout=<秒>` 调。
|
|
73
81
|
const POLL_INTERVAL_MS = readNumberArg('--poll-interval', 15) * 1000
|
|
74
82
|
const POLL_TIMEOUT_MS = readNumberArg('--poll-timeout', 600) * 1000
|
|
75
83
|
|
|
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`
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
// 跨平台 sleep:Node 原生,避免 `sleep N || ping` 的 Windows 兜底 hack
|
|
77
93
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
78
94
|
|
|
@@ -531,86 +547,97 @@ function readGlobalInstalledVersion() {
|
|
|
531
547
|
}
|
|
532
548
|
}
|
|
533
549
|
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
console.log(chalk.gray(
|
|
539
|
-
|
|
550
|
+
// 执行一次全局安装。退出码 0 才返回 ok。
|
|
551
|
+
// 输出先兜住不打印:重试期间每失败一次就刷一屏 `npm error` 太吵,
|
|
552
|
+
// 只有整轮尝试全部失败时,调用方才回放最后一份输出。
|
|
553
|
+
function tryInstallGlobal(spec) {
|
|
554
|
+
console.log(chalk.gray(` → npm install -g ${spec}`))
|
|
555
|
+
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
|
+
})
|
|
561
|
+
return { ok: true }
|
|
562
|
+
} catch (err) {
|
|
563
|
+
const out = `${err.stdout ?? ''}${err.stderr ?? ''}`.trim()
|
|
564
|
+
return { ok: false, output: out || String(err.message || err) }
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 自更新全局版本:反复尝试安装,直到全局版本校验通过或超时。
|
|
569
|
+
//
|
|
570
|
+
// 为什么不是"先等 dist-tags 翻牌,再安装"(v2.17.4 之前的老写法):
|
|
571
|
+
// `npm view dist-tags.latest` 和 `npm install -g pkg@ver` 读的是同一份 packument。
|
|
572
|
+
// packument 被 CDN 缓存(max-age=300,实测还能更久)时两边一起滞后 ——
|
|
573
|
+
// 等待阶段白等 600s,超时后再装照样 ETARGET,等于什么都没解决。
|
|
574
|
+
//
|
|
575
|
+
// 所以顺序反过来:先"尝试装",装不上才等。每轮按两条路试,任一条装上且校验通过就结束:
|
|
576
|
+
// ① `pkg@<version>` —— 常规路径,packument 已同步时一次命中;
|
|
577
|
+
// ② tarball URL 直连 —— packument 还是旧的时候的唯一出路(见 tarballUrl 注释)。
|
|
578
|
+
async function selfUpdateGlobal(version) {
|
|
579
|
+
console.log(chalk.blue('\n=== 发布后自更新全局版本 ==='))
|
|
580
|
+
console.log(chalk.gray('publish 成功 ≠ packument 立即可见(registry 处理 + CDN 缓存)。'))
|
|
581
|
+
console.log(chalk.gray('不空等 dist-tags,直接反复尝试安装;精确版本解析不到就 tarball 直连兜底。'))
|
|
540
582
|
console.log(chalk.gray(
|
|
541
|
-
|
|
542
|
-
+ '
|
|
583
|
+
`间隔 ${POLL_INTERVAL_MS / 1000}s,上限 ${POLL_TIMEOUT_MS / 1000}s`
|
|
584
|
+
+ '(可用 --poll-interval / --poll-timeout 调)。'
|
|
543
585
|
))
|
|
544
586
|
|
|
545
587
|
const startedAt = Date.now()
|
|
546
588
|
const deadline = startedAt + POLL_TIMEOUT_MS
|
|
589
|
+
const tarball = tarballUrl(version)
|
|
547
590
|
let attempt = 0
|
|
591
|
+
let lastOutput = ''
|
|
548
592
|
|
|
549
593
|
for (;;) {
|
|
550
594
|
attempt += 1
|
|
551
|
-
const
|
|
595
|
+
const packumentSynced = readLatestDistTag() === version
|
|
596
|
+
// packument 已同步 → 先走常规精确版本;还没同步 → tarball 优先,争取一轮命中
|
|
597
|
+
const targets = packumentSynced
|
|
598
|
+
? [`${PKG_NAME}@${version}`, tarball]
|
|
599
|
+
: [tarball, `${PKG_NAME}@${version}`]
|
|
600
|
+
|
|
601
|
+
console.log(chalk.gray(
|
|
602
|
+
`第 ${attempt} 次尝试(dist-tags.latest${packumentSynced ? '已同步' : '仍是旧版本'})...`
|
|
603
|
+
))
|
|
552
604
|
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
618
|
+
}
|
|
619
|
+
// 退出码 0 但版本不对(极少见):按失败处理,继续下一轮
|
|
620
|
+
lastOutput = `安装命令退出码 0,但全局版本读到 ${installed ?? '未知'}`
|
|
621
|
+
console.log(chalk.yellow(` 安装命令成功,但全局版本是 ${installed ?? '未知'},继续重试`))
|
|
557
622
|
}
|
|
558
623
|
|
|
559
624
|
const remain = deadline - Date.now()
|
|
560
625
|
if (remain <= 0) {
|
|
561
|
-
console.
|
|
562
|
-
|
|
563
|
-
|
|
626
|
+
console.error(chalk.red(`已尝试 ${attempt} 次,仍未装上 ${PKG_NAME}@${version}`))
|
|
627
|
+
if (lastOutput) console.error(chalk.gray(lastOutput))
|
|
628
|
+
console.error(chalk.gray(
|
|
629
|
+
'可稍后手动重试:\n'
|
|
630
|
+
+ ` npm install -g ${PKG_NAME}@${version}\n`
|
|
631
|
+
+ '若仍报 ETARGET(packument 缓存滞后),直连 tarball:\n'
|
|
632
|
+
+ ` npm install -g ${tarball}`
|
|
564
633
|
))
|
|
565
|
-
return
|
|
634
|
+
return
|
|
566
635
|
}
|
|
567
|
-
|
|
568
|
-
console.log(chalk.gray(
|
|
569
|
-
`第 ${attempt} 次:latest=${latest ?? '查询失败(网络或 registry 波动)'},`
|
|
570
|
-
+ `期望 ${version},${Math.ceil(remain / 1000)}s 后重试`
|
|
571
|
-
))
|
|
636
|
+
console.log(chalk.gray(` ${Math.ceil(remain / 1000)}s 后重试`))
|
|
572
637
|
await sleep(Math.min(POLL_INTERVAL_MS, remain))
|
|
573
638
|
}
|
|
574
639
|
}
|
|
575
640
|
|
|
576
|
-
// 装"精确版本"而不是 latest:即便 dist-tags 的 CDN 缓存滞后,
|
|
577
|
-
// 精确版本号也只会命中刚发布的那份 tarball。
|
|
578
|
-
function installGlobal(version) {
|
|
579
|
-
console.log(chalk.gray(`执行 npm install -g ${PKG_NAME}@${version}...`))
|
|
580
|
-
execSync(
|
|
581
|
-
`npm install -g ${PKG_NAME}@${version} --registry=${NPM_REGISTRY} --prefer-online`,
|
|
582
|
-
{ stdio: 'inherit' }
|
|
583
|
-
)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// 等待可见 → 全局安装 → 校验装到的版本(校验不看退出码,退出码 0 不代表版本对)
|
|
587
|
-
async function selfUpdateGlobal(version) {
|
|
588
|
-
const visible = await waitForVersionVisible(version)
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
installGlobal(version)
|
|
592
|
-
} catch (err) {
|
|
593
|
-
console.error(chalk.red('全局安装失败:'), err.message || err)
|
|
594
|
-
console.error(chalk.gray(`可稍后手动重试: npm install -g ${PKG_NAME}@${version}`))
|
|
595
|
-
return
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const installed = readGlobalInstalledVersion()
|
|
599
|
-
if (installed === version) {
|
|
600
|
-
console.log(chalk.green(`全局已更新到 ${PKG_NAME}@${version}`))
|
|
601
|
-
if (!visible) {
|
|
602
|
-
console.log(chalk.gray('(轮询阶段 npm view 未查到该版本,但安装已拿到正确版本)'))
|
|
603
|
-
}
|
|
604
|
-
return
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
console.error(chalk.red(
|
|
608
|
-
`全局版本校验不一致:期望 ${version},实际 ${installed ?? '未知'}`
|
|
609
|
-
+ (visible ? '' : '(且轮询阶段未在 registry 上看到该版本)')
|
|
610
|
-
))
|
|
611
|
-
console.error(chalk.gray(`等 registry 同步完成后手动重试: npm install -g ${PKG_NAME}@${version}`))
|
|
612
|
-
}
|
|
613
|
-
|
|
614
641
|
// 发布到 NPM
|
|
615
642
|
async function publishToNpm(version) {
|
|
616
643
|
console.log(chalk.blue('\n=== 发布到 NPM ==='))
|
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 =
|
|
280
|
-
|
|
281
|
-
|
|
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 }],
|
package/src/cli/ai/agent.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { readClipboardImage, checkImageFile, imageToDataUrl, formatBytes } from './images.js'
|
|
59
59
|
import { runModelSetup, collectModelInput, buildModelConfig, selectFromList } from './modelSetup.js'
|
|
60
60
|
import { genSessionId, autoTitle, writeSession, enforceRetention, listSessions } from './sessionStore.js'
|
|
61
|
+
import { buildAiChatRequest, describeAiHttpError } from '../../utils/aiEndpoint.js'
|
|
61
62
|
|
|
62
63
|
// truncateDisplay 已迁移到 termui.js;这里 re-export 保持既有测试/外部引用不断
|
|
63
64
|
export { truncateDisplay } from './termui.js'
|
|
@@ -65,7 +66,7 @@ export { truncateDisplay } from './termui.js'
|
|
|
65
66
|
// ──────────────────────────────────────────────
|
|
66
67
|
// 常量
|
|
67
68
|
// ──────────────────────────────────────────────
|
|
68
|
-
const
|
|
69
|
+
const DEFAULT_MAX_TOOL_ITERATIONS = 200 // 单轮工具调用循环数兜底值(防失控);实际值取配置里的 aiMaxToolIterations
|
|
69
70
|
const MAX_HISTORY_MESSAGES = 40 // 历史消息上限(超出后从最旧的整段对话裁剪)
|
|
70
71
|
const LLM_TIMEOUT_MS = 300000 // 单次 LLM 请求超时(5 分钟,长推理模型够用)
|
|
71
72
|
|
|
@@ -285,10 +286,14 @@ ${isWin ? `- This is Windows cmd.exe. The following Unix commands do NOT exist h
|
|
|
285
286
|
// LLM 流式调用(OpenAI 兼容 + function calling)
|
|
286
287
|
// 返回 { content, toolCalls, aborted }
|
|
287
288
|
// ──────────────────────────────────────────────
|
|
288
|
-
async function streamChatOnce({ model, messages, signal, onToken }) {
|
|
289
|
-
|
|
290
|
-
const headers = {
|
|
291
|
-
|
|
289
|
+
async function streamChatOnce({ model, messages, signal, onToken, sessionId }) {
|
|
290
|
+
// sessionId = 会话 ID:OpenCode 网关靠它做路由与提示缓存,同一条会话的所有请求要复用
|
|
291
|
+
const { url, headers } = buildAiChatRequest({
|
|
292
|
+
baseURL: model.baseURL,
|
|
293
|
+
model: model.model,
|
|
294
|
+
apiKey: model.apiKey,
|
|
295
|
+
sessionId,
|
|
296
|
+
})
|
|
292
297
|
|
|
293
298
|
const body = JSON.stringify({
|
|
294
299
|
model: model.model,
|
|
@@ -314,7 +319,8 @@ async function streamChatOnce({ model, messages, signal, onToken }) {
|
|
|
314
319
|
const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal })
|
|
315
320
|
if (!resp.ok || !resp.body) {
|
|
316
321
|
const errText = await resp.text().catch(() => '')
|
|
317
|
-
|
|
322
|
+
// 抽 error.message:provider 的说明都在里面,整坨 JSON 打到终端没法看
|
|
323
|
+
const snippet = describeAiHttpError(errText, resp.status)
|
|
318
324
|
// 400 且提到 tools/functions:大概率是模型不支持 function calling
|
|
319
325
|
if (resp.status === 400 && /tool|function/i.test(snippet)) {
|
|
320
326
|
throw new Error(`HTTP 400: 当前模型可能不支持 function calling(${snippet})。请在 g ui 中换用支持工具调用的模型(如 deepseek / qwen / gpt 系列)。`)
|
|
@@ -472,7 +478,11 @@ async function runAgentTurn(state, userText, t, images = []) {
|
|
|
472
478
|
// 旧消息里的图片降级为占位文字,防止 base64 随对话轮次累积撑爆上下文
|
|
473
479
|
stripStaleImages(state.messages, state.locale)
|
|
474
480
|
|
|
475
|
-
|
|
481
|
+
const maxIterations = Number.isFinite(state.maxToolIterations) && state.maxToolIterations > 0
|
|
482
|
+
? state.maxToolIterations
|
|
483
|
+
: DEFAULT_MAX_TOOL_ITERATIONS
|
|
484
|
+
|
|
485
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
476
486
|
trimHistory(state.messages)
|
|
477
487
|
|
|
478
488
|
// 首个 token 到达前转 spinner,到达后停掉并让位给流式渲染
|
|
@@ -508,6 +518,8 @@ async function runAgentTurn(state, userText, t, images = []) {
|
|
|
508
518
|
model: state.model,
|
|
509
519
|
messages: state.messages,
|
|
510
520
|
signal: state.abortController?.signal,
|
|
521
|
+
// 同一会话(含工具调用产生的每一轮请求)复用同一个会话 ID
|
|
522
|
+
sessionId: state.sessionId,
|
|
511
523
|
onToken: ({ thinking, content }) => {
|
|
512
524
|
if (thinking) renderSeg({ thinking })
|
|
513
525
|
if (content) for (const seg of thinkFilter.feed(content)) renderSeg(seg)
|
|
@@ -580,7 +592,7 @@ async function runAgentTurn(state, userText, t, images = []) {
|
|
|
580
592
|
// 工具结果全部入历史后继续循环,让模型基于结果决定下一步
|
|
581
593
|
}
|
|
582
594
|
|
|
583
|
-
printWarn(t.toolIterLimit(
|
|
595
|
+
printWarn(t.toolIterLimit(maxIterations))
|
|
584
596
|
}
|
|
585
597
|
|
|
586
598
|
// JSON 解析失败时的参数回显:单行截断,不进 summarizeToolArgs(它没有结构化参数可用)
|
|
@@ -897,6 +909,9 @@ export async function runAiAgent(argv = []) {
|
|
|
897
909
|
model,
|
|
898
910
|
locale,
|
|
899
911
|
shellDesc,
|
|
912
|
+
// 单轮工具调用上限:来自全局配置 aiMaxToolIterations(loadConfig 已规范化),
|
|
913
|
+
// 读不到时由 runAgentTurn 兜底成 DEFAULT_MAX_TOOL_ITERATIONS
|
|
914
|
+
maxToolIterations: cfg.aiMaxToolIterations,
|
|
900
915
|
currentChild: null,
|
|
901
916
|
abortController: null,
|
|
902
917
|
cancelRequested: false,
|
package/src/cli/ai/modelSetup.js
CHANGED
|
@@ -35,6 +35,7 @@ import readline from 'node:readline'
|
|
|
35
35
|
import chalk from 'chalk'
|
|
36
36
|
import config from '../../config.js'
|
|
37
37
|
import { startSpinner, renderSelectableListBody, parseKeyForSelectableList } from './termui.js'
|
|
38
|
+
import { buildAiChatRequest, buildAiApiRequest } from '../../utils/aiEndpoint.js'
|
|
38
39
|
|
|
39
40
|
// ──────────────────────────────────────────────
|
|
40
41
|
// 预设服务商列表
|
|
@@ -279,14 +280,13 @@ export async function fetchModelsFromApi({ baseURL, apiKey, timeoutMs = 5000, fe
|
|
|
279
280
|
const fetch = fetchFn || globalThis.fetch
|
|
280
281
|
if (!fetch) return []
|
|
281
282
|
|
|
282
|
-
const url = `${String(baseURL || '').replace(/\/$/, '')}/models`
|
|
283
|
-
const headers = {}
|
|
284
|
-
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
|
285
|
-
|
|
286
283
|
const controller = new AbortController()
|
|
287
284
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
288
285
|
|
|
289
286
|
try {
|
|
287
|
+
// 拉模型列表也要带客户端身份/会话头:OpenCode 网关对没有身份的请求照样拦。
|
|
288
|
+
// 放在 try 里保证"本函数不抛错"的契约不被 baseURL 为空破坏。
|
|
289
|
+
const { url, headers } = buildAiApiRequest({ baseURL, path: '/models', apiKey })
|
|
290
290
|
const res = await fetch(url, {
|
|
291
291
|
method: 'GET',
|
|
292
292
|
headers,
|
|
@@ -327,10 +327,6 @@ export async function testModelConnection({ baseURL, model, apiKey, timeoutMs =
|
|
|
327
327
|
const fetch = fetchFn || globalThis.fetch
|
|
328
328
|
if (!fetch) throw new Error('fetch is not available')
|
|
329
329
|
|
|
330
|
-
const url = `${String(baseURL || '').replace(/\/$/, '')}/chat/completions`
|
|
331
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
332
|
-
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
|
333
|
-
|
|
334
330
|
const body = JSON.stringify({
|
|
335
331
|
model,
|
|
336
332
|
messages: [{ role: 'user', content: 'hi' }],
|
|
@@ -341,6 +337,8 @@ export async function testModelConnection({ baseURL, model, apiKey, timeoutMs =
|
|
|
341
337
|
const controller = new AbortController()
|
|
342
338
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
343
339
|
try {
|
|
340
|
+
// 统一构造:身份/会话头 + OpenCode 网关的协议族校验(非 chat 模型直接给可读报错)
|
|
341
|
+
const { url, headers } = buildAiChatRequest({ baseURL, model, apiKey })
|
|
344
342
|
const res = await fetch(url, {
|
|
345
343
|
method: 'POST',
|
|
346
344
|
headers,
|
package/src/config.js
CHANGED
|
@@ -50,6 +50,29 @@ function resolveConfigPath() {
|
|
|
50
50
|
return _configPathReady;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// AI 智能体单轮工具调用上限的可调范围。
|
|
54
|
+
// 下限 1 是为了保留"防失控"语义 —— 0 会让智能体一动手就停,没人是这个意图;
|
|
55
|
+
// 上限 2000 是防手改配置写成天文数字后,一轮对话把 API 额度烧光。
|
|
56
|
+
const AI_MAX_TOOL_ITERATIONS_MIN = 1;
|
|
57
|
+
const AI_MAX_TOOL_ITERATIONS_MAX = 2000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 规范化单轮最大工具调用次数。
|
|
61
|
+
*
|
|
62
|
+
* 非法值(非数字 / NaN / 越界)一律**夹取**到合法区间而不是回退默认值 ——
|
|
63
|
+
* 用户手改成 5000 的意图明显是"想更大",夹到 2000 比悄悄退回 200 更贴近意图。
|
|
64
|
+
* 完全无法解析(undefined / 'abc')才返回 null,交给调用方取默认值。
|
|
65
|
+
*/
|
|
66
|
+
function normalizeAiMaxToolIterations(value) {
|
|
67
|
+
if (value === undefined || value === null || value === '') return null;
|
|
68
|
+
const n = Number(value);
|
|
69
|
+
if (!Number.isFinite(n)) return null;
|
|
70
|
+
const int = Math.floor(n);
|
|
71
|
+
if (int < AI_MAX_TOOL_ITERATIONS_MIN) return AI_MAX_TOOL_ITERATIONS_MIN;
|
|
72
|
+
if (int > AI_MAX_TOOL_ITERATIONS_MAX) return AI_MAX_TOOL_ITERATIONS_MAX;
|
|
73
|
+
return int;
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
// 默认配置
|
|
54
77
|
const defaultConfig = {
|
|
55
78
|
defaultCommitMessage: "submit",
|
|
@@ -90,6 +113,11 @@ const defaultConfig = {
|
|
|
90
113
|
locale: 'zh-CN', // 语言: zh-CN | en-US
|
|
91
114
|
// AI 模型配置
|
|
92
115
|
models: [],
|
|
116
|
+
// AI 智能体单轮最大工具调用次数(防失控)。CLI `g ai` 与 Web 智能体共用,全局生效。
|
|
117
|
+
// 2026-09-20: 40 → 200。40 轮在"读多个文件 → 逐个验证 → 再改"这类任务里很容易触顶,
|
|
118
|
+
// 触顶后本轮被强制结束,用户必须再发一条消息才能接着跑,体感像被截断。
|
|
119
|
+
// 想调小/调大改这个值即可(GUI: 设置 → AI 模型配置)。
|
|
120
|
+
aiMaxToolIterations: 200,
|
|
93
121
|
// UI 状态(跨项目共享,存到顶层 ui 对象)
|
|
94
122
|
// 之前散落在 localStorage,因随机端口启动而失效,迁到文件持久化
|
|
95
123
|
ui: {
|
|
@@ -396,7 +424,12 @@ async function loadConfig() {
|
|
|
396
424
|
}
|
|
397
425
|
// 兼容旧版(全局扁平结构)
|
|
398
426
|
if (raw && !raw.projects) {
|
|
399
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
...defaultConfig,
|
|
429
|
+
...raw,
|
|
430
|
+
aiMaxToolIterations: normalizeAiMaxToolIterations(raw.aiMaxToolIterations)
|
|
431
|
+
?? defaultConfig.aiMaxToolIterations
|
|
432
|
+
};
|
|
400
433
|
}
|
|
401
434
|
|
|
402
435
|
// 新版结构:{ projects: { [key]: projectConfig }, theme?, locale?, ui?, recentDirectories? }
|
|
@@ -409,7 +442,10 @@ async function loadConfig() {
|
|
|
409
442
|
theme: raw?.theme ?? defaultConfig.theme,
|
|
410
443
|
locale: raw?.locale ?? defaultConfig.locale,
|
|
411
444
|
models: raw?.models ?? defaultConfig.models,
|
|
412
|
-
ui: raw?.ui ?? defaultConfig.ui
|
|
445
|
+
ui: raw?.ui ?? defaultConfig.ui,
|
|
446
|
+
// 同 models:全局配置,始终取顶层,防止被项目配置里的旧值覆盖
|
|
447
|
+
aiMaxToolIterations: normalizeAiMaxToolIterations(raw?.aiMaxToolIterations)
|
|
448
|
+
?? defaultConfig.aiMaxToolIterations
|
|
413
449
|
};
|
|
414
450
|
}
|
|
415
451
|
|
|
@@ -457,7 +493,7 @@ async function saveConfig(config) {
|
|
|
457
493
|
|
|
458
494
|
// 分离全局设置和项目设置
|
|
459
495
|
// models / ui 也是全局配置(跨项目共享),和 theme/locale 一样存到顶层
|
|
460
|
-
const { theme, locale, models, ui, ...projectConfig } = config;
|
|
496
|
+
const { theme, locale, models, ui, aiMaxToolIterations, ...projectConfig } = config;
|
|
461
497
|
|
|
462
498
|
// 保存全局设置到根级别
|
|
463
499
|
if (theme !== undefined) {
|
|
@@ -472,6 +508,11 @@ async function saveConfig(config) {
|
|
|
472
508
|
if (ui !== undefined) {
|
|
473
509
|
raw.ui = ui;
|
|
474
510
|
}
|
|
511
|
+
// 工具调用上限同属全局设置:非法值不落盘(保留磁盘上的旧值),避免把手改坏的值固化
|
|
512
|
+
const normalizedIterations = normalizeAiMaxToolIterations(aiMaxToolIterations);
|
|
513
|
+
if (normalizedIterations !== null) {
|
|
514
|
+
raw.aiMaxToolIterations = normalizedIterations;
|
|
515
|
+
}
|
|
475
516
|
|
|
476
517
|
// 写入当前项目配置(在 defaultConfig 基础上合并,但不清空顶层其它键)
|
|
477
518
|
const existingProjectConfig = (raw.projects[key] && typeof raw.projects[key] === 'object') ? raw.projects[key] : {};
|
|
@@ -662,7 +703,19 @@ export default {
|
|
|
662
703
|
// 显式删除某个项目的全部分文件。写路径刻意不做删除(防"只加载了一个项目就写回"
|
|
663
704
|
// 连带清空其它项目),清理失效项目只能走这里。
|
|
664
705
|
deleteProjectConfig: deleteProjectConfigAndInvalidate,
|
|
706
|
+
// AI 智能体单轮工具调用上限的规范化/区间(GUI 保存前也要用,见 /api/config/save-ai-settings)
|
|
707
|
+
normalizeAiMaxToolIterations,
|
|
708
|
+
AI_MAX_TOOL_ITERATIONS_MIN,
|
|
709
|
+
AI_MAX_TOOL_ITERATIONS_MAX,
|
|
665
710
|
};
|
|
666
711
|
|
|
667
712
|
// 命名导出 — 用于测试与外部复用
|
|
668
|
-
export {
|
|
713
|
+
export {
|
|
714
|
+
ConfigWriteError,
|
|
715
|
+
normalizeProjectPath,
|
|
716
|
+
invalidateCurrentProjectKey,
|
|
717
|
+
invalidateRawConfigCache,
|
|
718
|
+
normalizeAiMaxToolIterations,
|
|
719
|
+
AI_MAX_TOOL_ITERATIONS_MIN,
|
|
720
|
+
AI_MAX_TOOL_ITERATIONS_MAX,
|
|
721
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"./rolldown-runtime-C0FnF6B9.js";import{$n as e,At as t,Bn as n,Dn as r,En as i,Er as a,G as o,Gt as s,In as c,On as l,Pn as u,Pr as d,Qr as f,Tn as p,Wn as m,Y as h,Yr as g,Zn as _,_n as ee,a as v,bn as te,br as y,cn as ne,cr as b,gt as
|
|
2
|
-
`);s=n.pop()??``;for(let e of n){let t=e.trim();if(!t.startsWith(`data:`))continue;let n=t.slice(5).trim();if(!n)continue;let i;try{i=JSON.parse(n)}catch{continue}switch(i.type){case`meta`:i.sessionId&&(p=i.sessionId,r.value=i.sessionId);break;case`thinking`:m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning+=String(i.delta||``),m.status=`streaming`;break;case`content`:m.status===`pending`&&(m.status=`streaming`);{let e=String(i.delta||``);m.content+=e;let t=
|
|
1
|
+
import"./rolldown-runtime-C0FnF6B9.js";import{$n as e,At as t,Bn as n,Dn as r,En as i,Er as a,G as o,Gt as s,In as c,On as l,Pn as u,Pr as d,Qr as f,Tn as p,Wn as m,Y as h,Yr as g,Zn as _,_n as ee,a as v,bn as te,br as y,cn as ne,cr as b,gt as re,i as x,kn as S,lr as C,o as ie,or as w,pr as T,qr as E,tt as D,un as O,ut as ae,vr as k,xn as oe,zt as se}from"./element-plus-KTmRV-so.js";import{a as A,n as j,r as ce}from"./vendor-92fKgtnz.js";import{n as M,t as N}from"./_plugin-vue_export-helper-B7A-NgBp.js";import{t as P}from"./configStore-Dx84lvTX.js";T();var F=65536;function I(e){if(!e||!Array.isArray(e.messages))return[];let t=[],n=e.messages.length;for(let r=0;r<n;r++){let i=e.messages[r];if(i.role!==`system`){if(i.role===`user`){let e=``,n;if(typeof i.content==`string`)e=i.content;else if(Array.isArray(i.content)){e=i.content.filter(e=>e?.type===`text`).map(e=>e.text||``).join(` `);let t=i.content.filter(e=>e?.type===`image_url`&&typeof e.image_url?.url==`string`).map((e,t)=>({id:`att-${r}-${t}`,name:`image`,size:0,type:z(e.image_url?.url||``)||`image/*`,preview:e.image_url?.url}));t.length>0&&(n=t)}(e||n)&&t.push({id:`msg-${r}`,role:`user`,content:e,attachments:n,status:`done`,createdAt:Date.now()})}else if(i.role===`assistant`){let a=``;typeof i.content==`string`&&(a=i.content),a.length>F&&(a=`…(前文已截断)\n${a.slice(-65536)}`);let o=[];if(i.tool_calls&&i.tool_calls.length>0)for(let e of i.tool_calls)o.push({id:e.id||e.function?.name||j(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:L(e.function?.name||``,e.function?.arguments||``),status:`done`,result:``});let s=r+1;for(;s<n&&e.messages[s].role===`tool`;){let t=e.messages[s],n=t.tool_call_id||t.name||``,i=o.find(e=>e.id===n);if(i){let e=t.content;typeof e!=`string`&&(e=`(non-text result)`),e.length>F&&(e=`…(前文已截断)\n${e.slice(-65536)}`),i.result=e}r=s,s++}let c=!!a,l=o.length>0;(c||l)&&t.push({id:`msg-${r}`,role:`assistant`,content:a||``,toolCalls:l?o:void 0,status:`done`,createdAt:Date.now()})}}}return t}function L(e,t){try{let n=JSON.parse(t);switch(e){case`run_command`:return String(n.command||``).slice(0,200);case`read_file`:case`write_file`:case`edit_file`:return String(n.path||``);case`list_files`:return String(n.path||`.`);case`search_text`:return String(n.pattern||``);default:return JSON.stringify(n).slice(0,200)}}catch{return t.slice(0,200)}}function R(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=()=>t(String(r.result||``)),r.onerror=()=>n(r.error||Error(`FileReader failed`)),r.readAsDataURL(e)})}function z(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function B(){let e=P(),t=y([]),n=y(!1),r=y(null),i=y([]),a=y(!1),o=y(!1),s=null,c=0,l=0;async function u(){let r=++l,i=e.currentDirectory||``;if(!i){t.value=[],n.value=!1;return}n.value=!0;try{let e=`/api/agent/sessions?cwd=${encodeURIComponent(i)}`,n=await fetch(e).then(e=>e.json());if(r!==l)return;if(!n.success){v.error(n.error||M(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;v.error(M(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===l&&(n.value=!1)}}async function d(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){v.error(t.error||M(`@AGENT:加载会话失败`));return}i.value=I(t.session)}catch(e){v.error(M(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function f(e){try{await x.confirm(M(`@AGENT:确认删除该会话`),M(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){v.error(n.error||M(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),v.success(M(`@AGENT:已删除`))}catch(e){v.error(M(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function p(e,n){try{let r=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({title:n})}).then(e=>e.json());if(!r.success){v.error(r.error||M(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),v.success(M(`@AGENT:已重命名`))}catch(e){v.error(M(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function m(){r.value=null,i.value=[]}async function h(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&v.warning(M(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>R(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),d=++c;s=new AbortController;let f=s,p={id:j(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(p.attachments=o.map(e=>({id:e.id,name:e.file.name||`image`,size:e.file.size||0,type:e.file.type||``,preview:e.preview}))),i.value.push(p);let m=k({id:j(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(m),a.value=!0;let h=[];try{let n=await fetch(`/api/agent/chat`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/event-stream`},body:JSON.stringify({sessionId:r.value||``,userMessage:t,cwd:e.currentDirectory||``,...l.length>0?{images:l}:{}}),signal:f.signal});if(d!==c)return;if(!n.ok||!n.body){let e=await n.text().catch(()=>``);throw Error(e||`HTTP ${n.status}`)}let a=n.body.getReader(),o=new TextDecoder(`utf-8`),s=``,p=r.value;for(;;){let{value:e,done:t}=await a.read();if(d!==c)return;if(t)break;s+=o.decode(e,{stream:!0});let n=s.split(`
|
|
2
|
+
`);s=n.pop()??``;for(let e of n){let t=e.trim();if(!t.startsWith(`data:`))continue;let n=t.slice(5).trim();if(!n)continue;let i;try{i=JSON.parse(n)}catch{continue}switch(i.type){case`meta`:i.sessionId&&(p=i.sessionId,r.value=i.sessionId);break;case`thinking`:m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning+=String(i.delta||``),m.status=`streaming`;break;case`content`:m.status===`pending`&&(m.status=`streaming`);{let e=String(i.delta||``);m.content+=e;let t=A(m.content);t.reasoning&&(m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning=t.reasoning,m.content=t.content)}break;case`tool_call_start`:{let e=k({id:i.toolCallId||j(),name:i.name||``,argsPreview:i.argsPreview||``,arguments:i.argsPreview||``,status:`running`,result:``});h.push(e),m.toolCalls||=[],m.toolCalls.push(e),m.status===`pending`&&(m.status=`streaming`);break}case`tool_result`:{let e=h.find(e=>e.id===i.toolCallId);e&&(e.result=String(i.result||``),e.status=`done`);break}case`done`:{let e=i.content||m.content,t=A(e);t.reasoning&&!m.reasoning&&(m.reasoning=t.reasoning),m.content=t.content,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}case`error`:m.status=`error`,m.error=String(i.error||M(`@AGENT:未知错误`)),m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)}}i.value=[...i.value]}if(d!==c)return;m.status===`pending`&&(m.status=`done`),p&&p!==r.value&&(r.value=p),u().catch(()=>{})}catch(e){if(d!==c)return;e?.name===`AbortError`||f.signal.aborted?(m.content=(m.content||``)+`
|
|
3
3
|
|
|
4
|
-
[`+M(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),v.error(m.error||M(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function g(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:d,deleteSession:f,renameSession:p,newSession:m,sendMessage:h,stop:g}}n(),
|
|
4
|
+
[`+M(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),v.error(m.error||M(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function g(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:d,deleteSession:f,renameSession:p,newSession:m,sendMessage:h,stop:g}}n(),T(),d(),O();var V={class:`agent-view`},H={class:`sidebar-header`},U={class:`sidebar-search`},W=[`placeholder`],G={class:`session-list`},le={key:0,class:`empty-state`},ue={class:`empty-text`},de={key:0,class:`empty-hint`},fe=[`onClick`],pe={class:`session-item-main`},K={class:`session-item-title`},me={class:`session-item-meta`},he={class:`meta-time`},ge={class:`meta-count`},_e={key:0,class:`meta-source cli`},ve=[`onClick`],ye=[`onClick`],be={class:`agent-chat-area`},xe={key:0,class:`chat-loading`},Se={key:0,class:`stop-button-bar`},q=N(c({__name:`AgentView`,setup(n){let c=P(),{sessions:d,sessionsLoading:T,currentSessionId:O,messages:k,isStreaming:A,sessionLoading:j,loadSessions:N,loadSession:F,deleteSession:I,renameSession:L,newSession:R,sendMessage:z,stop:q}=B(),Ce=p(()=>{let e=c.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=y(``),Y=p(()=>{if(!J.value.trim())return d.value;let e=J.value.toLowerCase();return d.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),we=p(()=>[{id:`p1`,label:M(`@AGENT:查看项目结构`),prompt:M(`@AGENT:prompt_p1`)},{id:`p2`,label:M(`@AGENT:分析代码质量`),prompt:M(`@AGENT:prompt_p2`)},{id:`p3`,label:M(`@AGENT:帮我写测试`),prompt:M(`@AGENT:prompt_p3`)},{id:`p4`,label:M(`@AGENT:Git 状态检查`),prompt:M(`@AGENT:prompt_p4`)},{id:`p5`,label:M(`@AGENT:帮我启动项目`),prompt:M(`@AGENT:prompt_p5`)}]);async function Te(e){await z(e.text,e.files),await m(),Z()}async function Ee(e){await z(e.prompt),await m(),Z()}let X=y(null);function Z(e=!0){X.value?.scrollToBottom(e)}w(()=>k.value.length,()=>{m(()=>Z(!1))});function De(e){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}F(e)}function Oe(){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}R()}function ke(e,t){if(t.stopPropagation(),A.value&&O.value===e){v.warning(M(`@AGENT:请先停止当前生成`));return}I(e)}async function Ae(e,t,n){n.stopPropagation();try{let{value:n}=await x.prompt(M(`@AGENT:请输入新的会话标题`),M(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:M(`@AGENT:标题不能为空`)});n&&n!==t&&await L(e,n)}catch{}}function je(e){if(!e)return``;try{let t=new Date(e);if(Number.isNaN(t.getTime()))return e;let n=new Date,r=n.getTime()-t.getTime(),i=e=>String(e).padStart(2,`0`);if(t.toDateString()===n.toDateString())return`${i(t.getHours())}:${i(t.getMinutes())}`;if(r<6048e5){let e=Math.floor(r/864e5);return e===0?M(`@AGENT:今天`):`${e}${M(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=y(280),$=y(!1);function Me(e){$.value=!0;let t=e.clientX,n=Q.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Q.value=i},i=()=>{$.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return w(()=>[c.currentDirectory,A.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await N(),O.value&&!A.value&&!d.value.some(e=>e.sessionId===O.value)&&R()},{immediate:!0}),(n,c)=>{let d=ie;return _(),S(`div`,V,[i(`aside`,{class:`agent-sidebar`,style:g({width:Q.value+`px`})},[i(`div`,H,[i(`button`,{class:`new-session-btn`,onClick:Oe},[u(a(h),null,{default:b(()=>[u(a(se))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:新建会话`)),1)])]),i(`div`,U,[u(a(h),{class:`search-icon`},{default:b(()=>[u(a(s))]),_:1}),C(i(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:a(M)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,W),[[ee,J.value]])]),C((_(),S(`div`,G,[Y.value.length===0&&!a(T)?(_(),S(`div`,le,[u(a(h),{size:28,color:`var(--text-tertiary)`},{default:b(()=>[u(a(D))]),_:1}),i(`div`,ue,f(J.value?a(M)(`@AGENT:未找到匹配的会话`):a(M)(`@AGENT:暂无会话`)),1),J.value?l(``,!0):(_(),S(`div`,de,f(a(M)(`@AGENT:点击上方按钮开始对话`)),1))])):l(``,!0),(_(!0),S(oe,null,e(Y.value,e=>(_(),S(`div`,{key:e.sessionId,class:E([`session-item`,{active:a(O)===e.sessionId}]),onClick:t=>De(e.sessionId)},[i(`div`,pe,[i(`div`,K,f(e.title||a(M)(`@AGENT:无标题`)),1),i(`div`,me,[i(`span`,he,f(je(e.updatedAt)),1),c[3]||=i(`span`,{class:`meta-dot`},`·`,-1),i(`span`,ge,f(e.messageCount)+` `+f(a(M)(`@AGENT:条`)),1),e.source===`cli`?(_(),S(`span`,_e,`CLI`)):l(``,!0)])]),i(`div`,{class:`session-item-actions`,onClick:c[1]||=te(()=>{},[`stop`])},[u(a(o),{content:a(M)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn`,onClick:t=>Ae(e.sessionId,e.title,t)},[u(a(h),null,{default:b(()=>[u(a(re))]),_:1})],8,ve)]),_:2},1032,[`content`]),u(a(o),{content:a(M)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn danger`,onClick:t=>ke(e.sessionId,t)},[u(a(h),null,{default:b(()=>[u(a(ae))]),_:1})],8,ye)]),_:2},1032,[`content`])])],10,fe))),128))])),[[d,a(T)]])],4),i(`div`,{class:E([`sidebar-resizer`,{active:$.value}]),onMousedown:Me},null,34),i(`main`,be,[a(j)?(_(),S(`div`,xe,[u(a(h),{class:`is-loading`,size:32},{default:b(()=>[u(a(t))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:加载会话中...`)),1)])):(_(),r(a(ce),{key:1,ref_key:`chatContainerRef`,ref:X,messages:a(k),"preset-questions":we.value,"welcome-title":a(M)(`@AGENT:智能体助手`),"welcome-description":a(M)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Ce.value,disabled:a(A),"upload-config":{accept:`image/*`},placeholder:a(A)?a(M)(`@AGENT:正在生成中...`):a(M)(`@AGENT:输入消息,Enter 发送`),onSend:Te,onSelect:Ee},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),u(ne,{name:`fade`},{default:b(()=>[a(A)?(_(),S(`div`,Se,[i(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>a(q)&&a(q)(...e)},[u(a(h),null,{default:b(()=>[u(a(t),{class:`is-loading`})]),_:1}),i(`span`,null,f(a(M)(`@AGENT:停止生成`)),1)])])):l(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-a1a388e1`]]);export{q as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.agent-view[data-v-a1a388e1]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-a1a388e1]{background:var(--bg-panel);border-right:1px solid var(--border-color);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-a1a388e1]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-a1a388e1]{border:1px dashed var(--border-color);border-radius:var(--radius-md);width:100%;color:var(--text-secondary);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:all .15s;display:flex}.new-session-btn[data-v-a1a388e1]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-a1a388e1]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-a1a388e1]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-a1a388e1]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;padding:4px 0;font-family:inherit;font-size:13px}.sidebar-search .search-input[data-v-a1a388e1]::placeholder{color:var(--text-tertiary)}.session-list[data-v-a1a388e1]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-a1a388e1]{text-align:center;color:var(--text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:48px 16px;display:flex}.empty-state .empty-text[data-v-a1a388e1]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-a1a388e1]{margin-top:4px;font-size:12px}.session-item[data-v-a1a388e1]{border-radius:var(--radius-sm);cursor:pointer;align-items:flex-start;gap:4px;padding:8px 10px;transition:background .12s;display:flex;position:relative}.session-item[data-v-a1a388e1]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-a1a388e1]{opacity:1}.session-item.active[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-a1a388e1]{color:var(--color-primary)}.session-item.active[data-v-a1a388e1]:before{content:"";background:var(--color-primary);border-radius:0 2px 2px 0;width:3px;height:20px;position:absolute;top:50%;left:0;transform:translateY(-50%)}.session-item-main[data-v-a1a388e1]{flex:1;min-width:0}.session-item-title[data-v-a1a388e1]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;margin-bottom:2px;font-size:13px;font-weight:500;overflow:hidden}.session-item-meta[data-v-a1a388e1]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-a1a388e1]{opacity:.5}.session-item-meta .meta-source[data-v-a1a388e1]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-a1a388e1]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-a1a388e1]{width:24px;height:24px;color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:all .12s;display:flex}.item-action-btn[data-v-a1a388e1]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-a1a388e1]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-a1a388e1]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-a1a388e1]:hover,.sidebar-resizer.active[data-v-a1a388e1]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-a1a388e1]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.chat-loading[data-v-a1a388e1]{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;font-size:14px;display:flex}.stop-button-bar[data-v-a1a388e1]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-a1a388e1]{border:1px solid var(--color-danger);background:var(--bg-container);color:var(--color-danger);cursor:pointer;border-radius:20px;align-items:center;gap:6px;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s;display:flex;box-shadow:0 2px 8px #0000001f}.stop-button[data-v-a1a388e1]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-a1a388e1],.fade-leave-active[data-v-a1a388e1]{transition:opacity .2s}.fade-enter-from[data-v-a1a388e1],.fade-leave-to[data-v-a1a388e1]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}
|