zen-gitsync 2.17.23 → 2.17.25
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 +10 -4
- package/package.json +8 -1
- package/scripts/run-tests.cjs +5 -0
- package/src/cli/ai/agent.js +20 -0
- package/src/cli/ai/termui.js +73 -1
- package/src/cli/ai/termui.test.js +51 -0
- package/src/cli/ai/tools.js +114 -0
- package/src/cli/ai/tools.test.js +65 -1
- package/src/cli/ai/turn.js +3 -0
- package/src/config.js +34 -3
- package/src/memory/library.js +273 -0
- package/src/memory/library.test.js +187 -0
- package/src/memory/store.js +199 -0
- package/src/memory/store.test.js +137 -0
- package/src/memory/templates.js +113 -0
- package/src/paths.js +18 -0
- package/src/ui/public/assets/{AgentEngineSelector-DLJ8He83.js → AgentEngineSelector-BArUIELz.js} +1 -1
- package/src/ui/public/assets/{AgentView-DnN-vCMi.css → AgentView-B3LhhRsy.css} +1 -1
- package/src/ui/public/assets/{AgentView-CQ18nw02.js → AgentView-LpT8A9Fo.js} +1 -1
- package/src/ui/public/assets/{AppVersionBadge-1pyUzRer.js → AppVersionBadge-DNXQm_Si.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-DrsrZiWJ.js → BranchSelector-DZPTz5Ql.js} +1 -1
- package/src/ui/public/assets/{CommitForm-B5p-7P7m.js → CommitForm-6avyu1Kv.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-BEm2cK1X.js → CommonDialog-CRXyMlp5.js} +1 -1
- package/src/ui/public/assets/EditorView-BIHL8I9g.js +1 -0
- package/src/ui/public/assets/{EditorView-C6HFYeEM.css → EditorView-DqxBkn6c.css} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-CYxcLw10.js → FileDiffViewer-BEYcCYs5.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-DTmlBLXQ.js → FlowExecutionViewer-DKRHDqbG.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-Da_UmTRT.js → FlowOrchestrationWorkspace-BnCsF4z4.js} +1 -1
- package/src/ui/public/assets/{IconButton-BShiC2AP.js → IconButton-D4df4t9A.js} +1 -1
- package/src/ui/public/assets/{LogList-5M5JBhlB.js → LogList-DXqB4JJv.js} +1 -1
- package/src/ui/public/assets/{MindmapView-BUV_w14M.js → MindmapView-Ddc55wzp.js} +1 -1
- package/src/ui/public/assets/{MonitorView-D9N4QH5Y.js → MonitorView-DeERavq-.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DYYQ-O7P.js → ProjectStartupButton-DaLU4iS4.js} +1 -1
- package/src/ui/public/assets/{RecentDirectoriesChat-BtWa_HMY.js → RecentDirectoriesChat-DVtsf7lW.js} +1 -1
- package/src/ui/public/assets/RecentDirectoriesChat-o4xK1Koc.css +1 -0
- package/src/ui/public/assets/{RemoteManagerDialog-C4QIJK_F.js → RemoteManagerDialog-C3cfDsZi.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-BmDh538v.js → RemoteRepoCard-D1Hso5Fq.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Ceamya-N.js → SourceMapView-Dwn5wy1o.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-xtz4vvc9.js → SvgIcon-diKLLWK9.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-DhKTZ4EM.js → TemplateManager-D9gyZbHP.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-DfeCGlEO.js → UserInputNode-JNtVWcbE.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-CJCxYK-K.css +1 -0
- package/src/ui/public/assets/WorkbenchView-DxaAjZAA.js +6 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-C7IvbEC3.js +19 -0
- package/src/ui/public/assets/agentConversations-nN-yDz8q.js +5 -0
- package/src/ui/public/assets/configStore-C-htWfWm.js +1 -0
- package/src/ui/public/assets/{gitStore-ByP9lpuT.js → gitStore-DYpd8vZK.js} +1 -1
- package/src/ui/public/assets/index-BZFKDSGv.css +1 -0
- package/src/ui/public/assets/index-eq1OUcdk.js +29 -0
- package/src/ui/public/assets/{previewSandbox-DSzZ_Rky.js → previewSandbox-Z707CZD9.js} +1 -1
- package/src/ui/public/assets/toolsStore-DFu4Alwg.js +1 -0
- package/src/ui/public/assets/vendor-BY5ZDlW1.css +1 -0
- package/src/ui/public/assets/{vendor-CyWsJd6S.js → vendor-BjomZKSS.js} +28 -120
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/index.js +4 -0
- package/src/ui/server/routes/config.js +11 -2
- package/src/ui/server/routes/fileOpen.js +78 -13
- package/src/ui/server/routes/memory.js +125 -0
- package/src/ui/server/routes/workbench/agentChat.js +35 -3
- package/src/ui/server/routes/workbench/agentChatShared.test.js +49 -0
- package/src/ui/server/routes/workbench/executorModels.js +329 -0
- package/src/ui/server/routes/workbench/executorModels.test.js +326 -0
- package/src/ui/server/routes/workbench/index.js +45 -2
- package/src/ui/server/routes/workbench/jobActivity.js +6 -0
- package/src/ui/server/routes/workbench/memoryContext.js +125 -0
- package/src/ui/server/routes/workbench/memoryContext.test.js +79 -0
- package/src/ui/server/routes/workbench/orchestratorStore.js +4 -1
- package/src/ui/server/routes/workbench/runExternalTurn.js +6 -1
- package/src/ui/server/routes/workbench/taskRunner.js +48 -5
- package/src/ui/public/assets/EditorView-fuMrS9P7.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-vHT8jMxW.css +0 -1
- package/src/ui/public/assets/WorkbenchView-D9l3i4Qj.css +0 -1
- package/src/ui/public/assets/WorkbenchView-DX2epPD9.js +0 -6
- package/src/ui/public/assets/_plugin-vue_export-helper-C_bQVzeb.js +0 -19
- package/src/ui/public/assets/agentConversations-BUPj8vzH.js +0 -5
- package/src/ui/public/assets/configStore-DJH7OlB_.js +0 -1
- package/src/ui/public/assets/index-CFX8sK4w.js +0 -29
- package/src/ui/public/assets/index-DVJITdth.css +0 -1
- package/src/ui/public/assets/toolsStore-B49mCaQ_.js +0 -1
- package/src/ui/public/assets/vendor-BzMymUBi.css +0 -1
package/src/ui/public/index.html
CHANGED
|
@@ -10,29 +10,29 @@
|
|
|
10
10
|
<meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
|
|
11
11
|
<title>Zen GitSync</title>
|
|
12
12
|
<!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-eq1OUcdk.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-Bx3j6vzG.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/monaco-DkJuNcCr.js">
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/IconButton-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-BjomZKSS.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C7IvbEC3.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-diKLLWK9.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/IconButton-D4df4t9A.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-C-htWfWm.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/gitStore-
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/gitStore-DYpd8vZK.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/fileIcon-B5p5pGN9.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-OPE_D4da.js">
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-Z707CZD9.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DYxFoHh1.js">
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/toolsStore-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-CRXyMlp5.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-BEYcCYs5.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/toolsStore-DFu4Alwg.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-D9gyZbHP.js">
|
|
32
32
|
<link rel="modulepreload" crossorigin href="/assets/vue-flow-fVVGZspa.js">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-Qexo5hiM.css">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
|
35
|
-
<link rel="stylesheet" crossorigin href="/assets/vendor-
|
|
35
|
+
<link rel="stylesheet" crossorigin href="/assets/vendor-BY5ZDlW1.css">
|
|
36
36
|
<link rel="stylesheet" crossorigin href="/assets/SvgIcon-BQ6bR7l0.css">
|
|
37
37
|
<link rel="stylesheet" crossorigin href="/assets/IconButton-W2X09OKH.css">
|
|
38
38
|
<link rel="stylesheet" crossorigin href="/assets/flow-mindmap-BMpmHdqh.css">
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-B6zy6NiA.css">
|
|
42
42
|
<link rel="stylesheet" crossorigin href="/assets/TemplateManager-DiM3vrbf.css">
|
|
43
43
|
<link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
|
|
44
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
44
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BZFKDSGv.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
|
47
47
|
<div id="app"></div>
|
package/src/ui/server/index.js
CHANGED
|
@@ -52,6 +52,7 @@ import { registerCodeAnalysisRoutes } from './routes/codeAnalysis.js';
|
|
|
52
52
|
import { registerInstancesRoutes } from './routes/instances.js';
|
|
53
53
|
import { registerMonitorRoutes } from './routes/monitor.js';
|
|
54
54
|
import { registerMindmapRoutes } from './routes/mindmap.js';
|
|
55
|
+
import { registerMemoryRoutes } from './routes/memory.js';
|
|
55
56
|
import { registerAgentRoutes } from './routes/workbench/agentRoutes.js';
|
|
56
57
|
import { createWorkspaceSnapshotter } from './routes/aiContext/wiring.js';
|
|
57
58
|
import { createInstanceRegistry, getRegistryPath } from './utils/instanceRegistry.js';
|
|
@@ -478,6 +479,9 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
478
479
|
// 思维导图:.mindmap.json 文件的列/读/写/建/删/重命名
|
|
479
480
|
registerMindmapRoutes({ app });
|
|
480
481
|
|
|
482
|
+
// 记忆库:跨轮经验的浏览 / 读正文 / 删条目(设置 → 记忆库面板)
|
|
483
|
+
registerMemoryRoutes({ app });
|
|
484
|
+
|
|
481
485
|
// 智能体:Web 端 AI 编码助手(含工具调用 + 会话持久化)
|
|
482
486
|
// 快照生成器在上面已经建好(要走同一个实例),这里注入进去复用。
|
|
483
487
|
registerAgentRoutes({
|
|
@@ -989,7 +989,7 @@ export function registerConfigRoutes({
|
|
|
989
989
|
// 保存通用设置(主题、语言、工作台任务执行器、任务完成提示)
|
|
990
990
|
app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
|
|
991
991
|
try {
|
|
992
|
-
const { theme, locale, taskExecutor, notifyOnTaskDone } = req.body
|
|
992
|
+
const { theme, locale, taskExecutor, notifyOnTaskDone, notifySoundOnTaskDone } = req.body
|
|
993
993
|
|
|
994
994
|
// 读取原始配置以保留项目设置
|
|
995
995
|
const rawConfig = await configManager.readRawConfigFile()
|
|
@@ -1015,13 +1015,22 @@ export function registerConfigRoutes({
|
|
|
1015
1015
|
if (normalizedNotify !== null) {
|
|
1016
1016
|
rawConfig.notifyOnTaskDone = normalizedNotify
|
|
1017
1017
|
}
|
|
1018
|
+
// 任务完成提示音开关(全局,默认开)。同上:非布尔值静默忽略。
|
|
1019
|
+
// 它从属于 notifyOnTaskDone —— 这里不替前端做联动(关总开关时不顺带清掉这个),
|
|
1020
|
+
// 两个键各自保留用户的选择,联动只在消费端(useTaskNotifier)。这样用户
|
|
1021
|
+
// 临时关掉总开关再打开,提示音设置还在,不用重设一遍。
|
|
1022
|
+
const normalizedNotifySound = configManager.normalizeNotifySoundOnTaskDone(notifySoundOnTaskDone)
|
|
1023
|
+
if (normalizedNotifySound !== null) {
|
|
1024
|
+
rawConfig.notifySoundOnTaskDone = normalizedNotifySound
|
|
1025
|
+
}
|
|
1018
1026
|
|
|
1019
1027
|
// 直接写入原始配置,避免覆盖项目设置
|
|
1020
1028
|
await configManager.writeRawConfigFile(rawConfig)
|
|
1021
1029
|
res.json({
|
|
1022
1030
|
success: true,
|
|
1023
1031
|
taskExecutor: normalizedExecutor || undefined,
|
|
1024
|
-
notifyOnTaskDone: normalizedNotify ?? undefined
|
|
1032
|
+
notifyOnTaskDone: normalizedNotify ?? undefined,
|
|
1033
|
+
notifySoundOnTaskDone: normalizedNotifySound ?? undefined
|
|
1025
1034
|
})
|
|
1026
1035
|
} catch (error) {
|
|
1027
1036
|
res.status(500).json({ success: false, error: error.message })
|
|
@@ -437,24 +437,82 @@ async function launchClaudeCode(dirPath, { permissionMode } = {}) {
|
|
|
437
437
|
return launchInTerminal(dirPath, 'claude', cliArgs);
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
/**
|
|
441
|
+
* codex 的启动参数(按权限档位)。与 claude / opencode 的档位对应关系:
|
|
442
|
+
*
|
|
443
|
+
* 默认(不带 token) → 一个 flag 都不加,跟随 `~/.codex/config.toml` 的
|
|
444
|
+
* approval / sandbox 设置
|
|
445
|
+
* 'sandboxed' → `-a never -s workspace-write`:**从不问人,但命令仍在沙箱里跑**
|
|
446
|
+
* (写限工作区、默认无网络)。这是"不想被批准提示打断、又不想
|
|
447
|
+
* 完全交出去"的那一档,语义上对应 claude 的「批准文件编辑」。
|
|
448
|
+
* 'bypass' → `--dangerously-bypass-approvals-and-sandbox`:免批准 + 免沙箱。
|
|
449
|
+
* 官方帮助原文 "Skip all confirmation prompts and execute commands
|
|
450
|
+
* without sandboxing. EXTREMELY DANGEROUS."。**完全批准**就是它。
|
|
451
|
+
* 'bypassPermissions'→ 同上。只为让调用方(顶栏菜单与编排台项目菜单)跟 claude 那档
|
|
452
|
+
* 共用同一个字符串,不是第三种行为。
|
|
453
|
+
*
|
|
454
|
+
* ⚠️ codex 没有 claude 的 `--permission-mode`,两档都得自己拼 flag。
|
|
455
|
+
* ⚠️ 别只看 sandbox:本机 config.toml 写的是 `sandbox_mode = "danger-full-access"`,
|
|
456
|
+
* 也就是说这台机器的**默认档本来就没有沙箱**,完全批准档真正多出来的只是"不再弹批准"。
|
|
457
|
+
*
|
|
458
|
+
* 回归 `test/open-with-permission.test.mjs`(与 opencode 那份参数映射放一起验)。
|
|
459
|
+
*/
|
|
460
|
+
const CODEX_MODE_FLAGS = new Map([
|
|
461
|
+
['sandboxed', ['-a', 'never', '-s', 'workspace-write']],
|
|
462
|
+
['bypass', ['--dangerously-bypass-approvals-and-sandbox']],
|
|
463
|
+
['bypassPermissions', ['--dangerously-bypass-approvals-and-sandbox']],
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
export function buildCodexArgs(permissionMode) {
|
|
467
|
+
// Map.get 对非字符串返回 undefined → 未知 token 一律空数组,白名单式,不存在拼参数注入
|
|
468
|
+
return [...(CODEX_MODE_FLAGS.get(permissionMode) || [])];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function launchCodex(dirPath, { permissionMode } = {}) {
|
|
472
|
+
// OpenAI Codex CLI - https://github.com/openai/codex
|
|
473
|
+
const cliArgs = buildCodexArgs(permissionMode);
|
|
442
474
|
if (process.platform === 'win32') {
|
|
443
|
-
return spawnDetached('cmd.exe', ['/c', 'start', '""', 'codex'], {
|
|
475
|
+
return spawnDetached('cmd.exe', ['/c', 'start', '""', 'codex', ...cliArgs], {
|
|
444
476
|
cwd: dirPath
|
|
445
477
|
});
|
|
446
478
|
}
|
|
447
|
-
return launchInTerminal(dirPath, 'codex');
|
|
479
|
+
return launchInTerminal(dirPath, 'codex', cliArgs);
|
|
448
480
|
}
|
|
449
481
|
|
|
450
|
-
|
|
482
|
+
/**
|
|
483
|
+
* opencode 的「完全批准」= `--auto`(官方文档里的 auto mode):把**未被显式拒绝**的
|
|
484
|
+
* 权限请求自动批准。
|
|
485
|
+
*
|
|
486
|
+
* 它不等于"什么都放行":配置里显式 `deny` 的仍然会被拦(opencode 默认就 deny 了
|
|
487
|
+
* `read` 的 `*.env` / `*.env.*`,TUI 里 `--auto` 也一样读不到),这与 claude 的
|
|
488
|
+
* bypassPermissions 受 `~/.claude/settings.json` deny 约束是同一个口径 —— 两边都是
|
|
489
|
+
* "除非你明确禁止,否则全放"。
|
|
490
|
+
*
|
|
491
|
+
* 档位 token:`auto` 是本工具的正式叫法;`bypassPermissions` 只是为了让调用方
|
|
492
|
+
* (顶栏菜单与编排台项目菜单)与 claude 那档共用同一个字符串。
|
|
493
|
+
*/
|
|
494
|
+
const OPENCODE_AUTO_MODES = new Set(['auto', 'bypassPermissions']);
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* opencode 的启动参数(按权限档位)。
|
|
498
|
+
*
|
|
499
|
+
* 单独抽成纯函数是为了能**真的跑断言**而不用 spawn 一个终端窗口 —— 带 `--auto` 的
|
|
500
|
+
* TUI 会弹出来并停在那里等人输入,验证不能有这种副作用。回归 `test/opencode-permission.test.mjs`。
|
|
501
|
+
* 白名单式:非字符串 / 未知 token 一律返回空数组,不存在拼参数注入的面。
|
|
502
|
+
*/
|
|
503
|
+
export function buildOpencodeArgs(permissionMode) {
|
|
504
|
+
return OPENCODE_AUTO_MODES.has(permissionMode) ? ['--auto'] : [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function launchOpenCode(dirPath, { permissionMode } = {}) {
|
|
451
508
|
// opencode (sst/opencode) CLI - https://opencode.ai
|
|
509
|
+
const cliArgs = buildOpencodeArgs(permissionMode);
|
|
452
510
|
if (process.platform === 'win32') {
|
|
453
|
-
return spawnDetached('cmd.exe', ['/c', 'start', '""', 'opencode'], {
|
|
511
|
+
return spawnDetached('cmd.exe', ['/c', 'start', '""', 'opencode', ...cliArgs], {
|
|
454
512
|
cwd: dirPath
|
|
455
513
|
});
|
|
456
514
|
}
|
|
457
|
-
return launchInTerminal(dirPath, 'opencode');
|
|
515
|
+
return launchInTerminal(dirPath, 'opencode', cliArgs);
|
|
458
516
|
}
|
|
459
517
|
|
|
460
518
|
// Run the CLI shipped with this server, even when `g` is not in PATH or points
|
|
@@ -960,7 +1018,7 @@ export function registerFileOpenRoutes({
|
|
|
960
1018
|
// 用 Codex 打开目录
|
|
961
1019
|
app.post('/api/open-directory-with-codex', asyncRoute(async (req, res) => {
|
|
962
1020
|
try {
|
|
963
|
-
const { path: dirPath } = req.body || {};
|
|
1021
|
+
const { path: dirPath, permissionMode } = req.body || {};
|
|
964
1022
|
if (!dirPath) {
|
|
965
1023
|
throw new HttpError(400, '目录路径不能为空');
|
|
966
1024
|
}
|
|
@@ -972,8 +1030,12 @@ export function registerFileOpenRoutes({
|
|
|
972
1030
|
}
|
|
973
1031
|
|
|
974
1032
|
try {
|
|
975
|
-
await launchCodex(dirPath);
|
|
976
|
-
|
|
1033
|
+
await launchCodex(dirPath, { permissionMode });
|
|
1034
|
+
const codexArgs = buildCodexArgs(permissionMode);
|
|
1035
|
+
const message = codexArgs.length
|
|
1036
|
+
? `已用 Codex 打开目录(${codexArgs.join(' ')})`
|
|
1037
|
+
: '已用 Codex 打开目录';
|
|
1038
|
+
res.json({ success: true, message });
|
|
977
1039
|
} catch (error) {
|
|
978
1040
|
res.status(400).json({
|
|
979
1041
|
success: false,
|
|
@@ -988,7 +1050,7 @@ export function registerFileOpenRoutes({
|
|
|
988
1050
|
// 用 OpenCode 打开目录
|
|
989
1051
|
app.post('/api/open-directory-with-opencode', asyncRoute(async (req, res) => {
|
|
990
1052
|
try {
|
|
991
|
-
const { path: dirPath } = req.body || {};
|
|
1053
|
+
const { path: dirPath, permissionMode } = req.body || {};
|
|
992
1054
|
if (!dirPath) {
|
|
993
1055
|
throw new HttpError(400, '目录路径不能为空');
|
|
994
1056
|
}
|
|
@@ -1000,8 +1062,11 @@ export function registerFileOpenRoutes({
|
|
|
1000
1062
|
}
|
|
1001
1063
|
|
|
1002
1064
|
try {
|
|
1003
|
-
await launchOpenCode(dirPath);
|
|
1004
|
-
|
|
1065
|
+
await launchOpenCode(dirPath, { permissionMode });
|
|
1066
|
+
const message = OPENCODE_AUTO_MODES.has(permissionMode)
|
|
1067
|
+
? '已用 OpenCode 打开目录(--auto:自动批准未被显式拒绝的权限)'
|
|
1068
|
+
: '已用 OpenCode 打开目录';
|
|
1069
|
+
res.json({ success: true, message });
|
|
1005
1070
|
} catch (error) {
|
|
1006
1071
|
res.status(400).json({
|
|
1007
1072
|
success: false,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// 记忆库的浏览 / 删除接口(设置 → 记忆库面板用)。
|
|
16
|
+
//
|
|
17
|
+
// 口径(与其它 workbench 路由一致,详见 routes/workbench/index.js 头部三条):
|
|
18
|
+
// - 非 SSE 路由统一 asyncRoute 包装,**handler 内不写 try/catch**
|
|
19
|
+
// - 定制状态码用 `throw new HttpError(4xx, msg)`
|
|
20
|
+
// - 危险操作(删)要求调用方显式传 `confirm: true`
|
|
21
|
+
//
|
|
22
|
+
// ⚠️ **本文件不导出"任意路径"接口。** 前端只传 `{scope, file}`,
|
|
23
|
+
// 绝对路径由 src/memory/library.js 从 scope 重新拼(见那个文件头注释)。
|
|
24
|
+
// 这是结构性防越界:不合法参数压根不参与路径拼接,
|
|
25
|
+
// 也就没有"前端传个 path 就删到别处"的可能。**别为了"省一次校验"改成传路径。**
|
|
26
|
+
|
|
27
|
+
import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
|
|
28
|
+
import {
|
|
29
|
+
listScopes,
|
|
30
|
+
listEntries,
|
|
31
|
+
readEntry,
|
|
32
|
+
deleteEntry,
|
|
33
|
+
isValidScope,
|
|
34
|
+
isValidFile,
|
|
35
|
+
memoryDirExists,
|
|
36
|
+
SCOPE_GLOBAL,
|
|
37
|
+
SCOPE_GLOBAL_INDEX,
|
|
38
|
+
} from '../../../memory/library.js';
|
|
39
|
+
|
|
40
|
+
/** 批量删除的单次上限:再多就该分批,别让一个请求把整个库清空 */
|
|
41
|
+
const BATCH_MAX = 200;
|
|
42
|
+
|
|
43
|
+
function requireScope(scope) {
|
|
44
|
+
if (!isValidScope(scope)) throw new HttpError(400, 'scope 不合法');
|
|
45
|
+
return scope;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requireFile(file) {
|
|
49
|
+
if (!isValidFile(file)) throw new HttpError(400, 'file 不合法');
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerMemoryRoutes({ app }) {
|
|
54
|
+
// ── 有哪些可看的范围(全局两篇 + 每个项目)──────────────────────────
|
|
55
|
+
app.get('/api/memory/scopes', asyncRoute(async (_req, res) => {
|
|
56
|
+
const available = await memoryDirExists();
|
|
57
|
+
if (!available) {
|
|
58
|
+
// 没就绪是**合法状态**(用户还没开过工作台),给空列表而不是 500:
|
|
59
|
+
// 面板据此显示"尚未初始化",而不是一个看不懂的报错
|
|
60
|
+
res.json({ success: true, available: false, scopes: [] });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.json({ success: true, available: true, scopes: await listScopes() });
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// ── 某个范围下有哪些条目 ────────────────────────────────────────────
|
|
67
|
+
app.get('/api/memory/entries', asyncRoute(async (req, res) => {
|
|
68
|
+
const scope = requireScope(String(req.query.scope || ''));
|
|
69
|
+
res.json({ success: true, scope, entries: await listEntries(scope) });
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// ── 读单条正文(展开看内容时按需拉)─────────────────────────────────
|
|
73
|
+
app.get('/api/memory/entry', asyncRoute(async (req, res) => {
|
|
74
|
+
const scope = requireScope(String(req.query.scope || ''));
|
|
75
|
+
const file = requireFile(String(req.query.file || ''));
|
|
76
|
+
const r = await readEntry(scope, file);
|
|
77
|
+
if (r.error === 'invalid') throw new HttpError(400, 'scope 或 file 不合法');
|
|
78
|
+
if (r.tooLarge) throw new HttpError(413, `内容过大(${Math.round(r.size / 1024)} KB),不予展示`);
|
|
79
|
+
if (r.missing) throw new HttpError(404, '条目不存在');
|
|
80
|
+
res.json({ success: true, scope, file, content: r.content, size: r.size, mtime: r.mtime });
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// ── 删一条(连带清掉 INDEX.md 里那行索引)───────────────────────────
|
|
84
|
+
app.delete('/api/memory/entry', asyncRoute(async (req, res) => {
|
|
85
|
+
if (req.body?.confirm !== true) throw new HttpError(400, '需要 confirm: true');
|
|
86
|
+
const scope = requireScope(String(req.body?.scope || ''));
|
|
87
|
+
const file = requireFile(String(req.body?.file || ''));
|
|
88
|
+
// 全局两篇是索引/规范本身,删了整套记忆就失去记账口径 —— 不给删
|
|
89
|
+
if (scope === SCOPE_GLOBAL || scope === SCOPE_GLOBAL_INDEX) {
|
|
90
|
+
throw new HttpError(400, '全局索引与规范文件不可删除');
|
|
91
|
+
}
|
|
92
|
+
const r = await deleteEntry(scope, file);
|
|
93
|
+
res.json({ success: true, ...r });
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
// ── 批量删 ──────────────────────────────────────────────────────────
|
|
97
|
+
app.post('/api/memory/batch-delete', asyncRoute(async (req, res) => {
|
|
98
|
+
if (req.body?.confirm !== true) throw new HttpError(400, '需要 confirm: true');
|
|
99
|
+
const items = Array.isArray(req.body?.items) ? req.body.items : [];
|
|
100
|
+
if (items.length === 0) throw new HttpError(400, '没有选中任何条目');
|
|
101
|
+
if (items.length > BATCH_MAX) throw new HttpError(400, `一次最多删 ${BATCH_MAX} 条`);
|
|
102
|
+
|
|
103
|
+
let removed = 0;
|
|
104
|
+
let indexLinesRemoved = 0;
|
|
105
|
+
const failed = [];
|
|
106
|
+
for (const it of items) {
|
|
107
|
+
const scope = String(it?.scope || '');
|
|
108
|
+
const file = String(it?.file || '');
|
|
109
|
+
if (!isValidScope(scope) || !isValidFile(file)) { failed.push({ scope, file, reason: 'invalid' }); continue; }
|
|
110
|
+
if (scope === SCOPE_GLOBAL || scope === SCOPE_GLOBAL_INDEX) {
|
|
111
|
+
failed.push({ scope, file, reason: 'protected' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const r = await deleteEntry(scope, file);
|
|
116
|
+
if (r.removed) removed += 1;
|
|
117
|
+
if (r.indexLineRemoved) indexLinesRemoved += 1;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// 单条失败**不中断整批** —— 否则前几条已删、后几条报错,用户重试会重复删
|
|
120
|
+
failed.push({ scope, file, reason: err?.message || 'delete failed' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
res.json({ success: true, removed, indexLinesRemoved, failed });
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
// - { type: 'meta', sessionId, isNew, title }
|
|
22
22
|
// - { type: 'thinking', delta } — 推理过程增量
|
|
23
23
|
// - { type: 'content', delta } — 正文增量
|
|
24
|
-
// - { type: 'tool_call_start', toolCallId, name, argsPreview }
|
|
24
|
+
// - { type: 'tool_call_start', toolCallId, name, argsPreview, arguments }
|
|
25
|
+
// arguments 只在计划类工具(update_plan 等)上出现:前端要拿原始 steps 渲染
|
|
26
|
+
// 计划清单,argsPreview 是截断过的摘要拆不出步骤。其他工具不带全文,免得
|
|
27
|
+
// write_file 那种几十 KB 的 content 灌进 SSE。
|
|
25
28
|
// - { type: 'tool_output', toolCallId, chunk } — 命令执行中的增量输出(仅展示)
|
|
26
29
|
// - { type: 'tool_result', toolCallId, name, result }
|
|
27
30
|
// - { type: 'ask_user', interactionId, question, options, allowFreeText, multiple }
|
|
@@ -33,7 +36,7 @@ import os from 'os';
|
|
|
33
36
|
import { logger } from './shared.js';
|
|
34
37
|
|
|
35
38
|
// 从 CLI 侧导入工具定义、执行器与 LLM 传输层(同一 monorepo,路径可达)
|
|
36
|
-
import { TOOL_DEFINITIONS, executeTool } from '../../../../cli/ai/tools.js';
|
|
39
|
+
import { TOOL_DEFINITIONS, executeTool, normalizePlanSteps, summarizePlan, isPlanToolName } from '../../../../cli/ai/tools.js';
|
|
37
40
|
import { prepareRequestMessages } from '../../../../cli/ai/context.js';
|
|
38
41
|
import { streamChatOnce } from '../../../../cli/ai/transport.js';
|
|
39
42
|
import { checkDangerousCommand } from '../../../../cli/ai/safety.js';
|
|
@@ -131,6 +134,15 @@ ${isWin ? `- 当前是 Windows,以下 Unix 命令**不存在**,用了必定报"
|
|
|
131
134
|
- run_command 默认就在工作目录执行,不要再加 cd 前缀;默认超时 120 秒,长任务加大 timeout_seconds(最大 600)
|
|
132
135
|
- 命令在 ${shellDesc} 下执行,注意语法兼容
|
|
133
136
|
|
|
137
|
+
# 任务计划(多步任务必用)
|
|
138
|
+
- 任务需要多步时(改代码、排查、调研、多文件改动),**动手之前**先调一次 update_plan,把要做的事
|
|
139
|
+
拆成 3-8 个可核对的步骤,让用户在你改任何东西之前就知道范围和验收标准
|
|
140
|
+
- 单步小事(读个文件、答一个问题)不要列计划,那是噪音
|
|
141
|
+
- 每次都传**完整**的当前计划(不是增量),顺序即执行顺序;同一时刻最多一个 in_progress
|
|
142
|
+
- 完成一步就更新一次状态,别攒到最后一次性刷;计划与实际不符时直接改计划,不要硬着头皮往下走
|
|
143
|
+
- 全部做完后把 steps 传空表示收尾
|
|
144
|
+
- update_plan 只是给人看的进度板,不要为了"更新计划"去改文件或跑命令
|
|
145
|
+
|
|
134
146
|
# 与用户交互
|
|
135
147
|
- 需要向用户确认、提问或汇报重要决策时,直接用普通文本输出
|
|
136
148
|
- 需要暂停当前任务并等待用户决定或补充信息时,调用 ask_user,不要猜测或只在普通文本里提问
|
|
@@ -213,6 +225,17 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
213
225
|
- run_command defaults to the working directory; default timeout 120s, max 600s
|
|
214
226
|
- When the task must pause for a decision or missing detail, call ask_user and wait for the user's answer. The user may attach images to a message; they arrive as image_url parts in the user message. If the current model rejects images (no vision support), tell the user to switch to a vision-capable model
|
|
215
227
|
|
|
228
|
+
# Task plan (required for multi-step work)
|
|
229
|
+
- When a task takes several steps (code changes, debugging, research, multi-file edits), call update_plan
|
|
230
|
+
**before touching anything**: break it into 3-8 verifiable steps so the user knows the scope and the
|
|
231
|
+
acceptance criteria before you modify a single file
|
|
232
|
+
- Do not plan single trivial actions (read one file, answer one question) — that is noise
|
|
233
|
+
- Always send the **complete** current plan (not a delta), in execution order; at most one in_progress
|
|
234
|
+
- Update the status right after finishing a step instead of batching everything at the end; when the
|
|
235
|
+
plan no longer matches reality, rewrite the plan instead of pushing ahead
|
|
236
|
+
- Send an empty steps list once everything is done
|
|
237
|
+
- update_plan is a progress board for humans: never edit files or run commands just to "update the plan"
|
|
238
|
+
|
|
216
239
|
# Output
|
|
217
240
|
- Your text output is displayed in the user's Web UI
|
|
218
241
|
- After completing a task, briefly summarize the result`;
|
|
@@ -384,7 +407,11 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
384
407
|
|
|
385
408
|
// 工具参数预览(给前端展示)
|
|
386
409
|
const argsPreview = summarizeArgs(name, args);
|
|
387
|
-
|
|
410
|
+
// 计划类工具额外发**完整参数**:前端要拿原始 steps 渲染清单,而 argsPreview
|
|
411
|
+
// 是截断过的摘要,拆不出步骤。只给计划类工具发完整参数 ——
|
|
412
|
+
// write_file 的 content 可能几十 KB,为它开一路全文等于给 SSE 带宽找麻烦。
|
|
413
|
+
const rawArgsForClient = isPlanToolName(name) ? rawArgs : undefined;
|
|
414
|
+
send({ type: 'tool_call_start', toolCallId, name, argsPreview, arguments: rawArgsForClient });
|
|
388
415
|
|
|
389
416
|
const toolCtx = {
|
|
390
417
|
...ctx,
|
|
@@ -430,6 +457,11 @@ function summarizeArgs(name, args) {
|
|
|
430
457
|
return String(args.path || '.');
|
|
431
458
|
case 'search_text':
|
|
432
459
|
return String(args.pattern || '');
|
|
460
|
+
case 'update_plan': {
|
|
461
|
+
const steps = normalizePlanSteps(args.steps ?? args.todos ?? args.plan);
|
|
462
|
+
const why = String(args.explanation || '').replace(/\s+/g, ' ').trim();
|
|
463
|
+
return [summarizePlan(steps), why].filter(Boolean).join(' — ').slice(0, 200);
|
|
464
|
+
}
|
|
433
465
|
default:
|
|
434
466
|
return JSON.stringify(args).slice(0, 200);
|
|
435
467
|
}
|
|
@@ -179,3 +179,52 @@ test('400 且正文提到 tool/function 时提示换模型,而不是甩网关 JS
|
|
|
179
179
|
assert.match(error.error, /不支持 function calling/)
|
|
180
180
|
assert.match(error.error, /换用支持工具调用的模型/)
|
|
181
181
|
})
|
|
182
|
+
|
|
183
|
+
// ── 计划工具的透传契约 ──────────────────────────────────────
|
|
184
|
+
// 前端要拿原始 steps 渲染计划清单,而它拿到的是 argsPreview(截断过的摘要)——
|
|
185
|
+
// 摘要里拆不出步骤,所以服务端必须为计划类工具额外带一份完整 arguments。
|
|
186
|
+
// 这条钉死了「只给计划类发全文」的口径:多发会让 write_file 的 content
|
|
187
|
+
// 几十 KB 灌进 SSE,少发则前端永远退化成普通工具块。
|
|
188
|
+
test('计划类工具的 tool_call_start 带完整 arguments,其他工具不带', async () => {
|
|
189
|
+
const planArgs = JSON.stringify({
|
|
190
|
+
steps: [
|
|
191
|
+
{ content: '读代码', status: 'completed' },
|
|
192
|
+
{ content: '改代码', status: 'in_progress' }
|
|
193
|
+
],
|
|
194
|
+
explanation: '先改库再改宿主'
|
|
195
|
+
})
|
|
196
|
+
const round = [
|
|
197
|
+
sse([event({
|
|
198
|
+
choices: [{ delta: { tool_calls: [
|
|
199
|
+
{ index: 0, id: 'p1', function: { name: 'update_plan', arguments: planArgs } },
|
|
200
|
+
{ index: 1, id: 'p2', function: { name: 'write_file', arguments: JSON.stringify({ path: 'a.js', content: 'x'.repeat(50) }) } }
|
|
201
|
+
] } }, { finish_reason: 'tool_calls' }],
|
|
202
|
+
}), 'data: [DONE]\n\n']),
|
|
203
|
+
okStream(),
|
|
204
|
+
]
|
|
205
|
+
let turn = 0
|
|
206
|
+
const session = newSession('ag-sandbox-plan', withSystem())
|
|
207
|
+
|
|
208
|
+
const { events } = await runWeb({
|
|
209
|
+
session,
|
|
210
|
+
userMessage: '把这个功能做出来',
|
|
211
|
+
respond: () => round[turn++] || okStream(),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const starts = events.filter(e => e.type === 'tool_call_start')
|
|
215
|
+
assert.equal(starts.length, 2)
|
|
216
|
+
const planStart = starts.find(e => e.name === 'update_plan')
|
|
217
|
+
const writeStart = starts.find(e => e.name === 'write_file')
|
|
218
|
+
|
|
219
|
+
// 计划:全文必须一字不差地带到前端(前端靠它渲染清单)
|
|
220
|
+
assert.equal(planStart.arguments, planArgs)
|
|
221
|
+
assert.match(planStart.argsPreview, /1\/2|1 完成/)
|
|
222
|
+
// 其他工具:只给摘要,不灌全文
|
|
223
|
+
assert.equal(writeStart.arguments, undefined)
|
|
224
|
+
|
|
225
|
+
const planResult = events.find(e => e.type === 'tool_result' && e.name === 'update_plan')
|
|
226
|
+
assert.match(planResult.result, /计划已更新/)
|
|
227
|
+
// 计划也要进会话记录 —— 重新打开会话时前端靠历史里的 arguments 复原清单
|
|
228
|
+
const assistantWithPlan = session.messages.find(m => m.role === 'assistant' && m.tool_calls?.some(t => t.function.name === 'update_plan'))
|
|
229
|
+
assert.equal(assistantWithPlan.tool_calls[0].function.arguments, planArgs)
|
|
230
|
+
})
|