zen-gitsync 2.17.16 → 2.17.18
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 +2 -0
- package/package.json +1 -1
- package/src/paths.js +13 -0
- package/src/ui/public/assets/AgentView-AMFKSDeH.css +1 -0
- package/src/ui/public/assets/AgentView-DQAB0ypS.js +13 -0
- package/src/ui/public/assets/AppVersionBadge-B5VG1_Lu.js +2 -0
- package/src/ui/public/assets/AppVersionBadge-DDYQqoy7.css +1 -0
- package/src/ui/public/assets/BranchSelector-Cj6athRO.js +1 -0
- package/src/ui/public/assets/BranchSelector-F4ibldMN.css +1 -0
- package/src/ui/public/assets/CommitForm-4kI2csgs.css +1 -0
- package/src/ui/public/assets/CommitForm-a2tMsfF9.js +6 -0
- package/src/ui/public/assets/CommonDialog-I-GCqRMC.js +1 -0
- package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +1 -0
- package/src/ui/public/assets/EditorView-DBqhjm84.css +1 -0
- package/src/ui/public/assets/EditorView-wTi53Nxc.js +16 -0
- package/src/ui/public/assets/FileDiffViewer-B7i4BmbH.js +15 -0
- package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +1 -0
- package/src/ui/public/assets/{FlowExecutionViewer-Drw4hlJG.js → FlowExecutionViewer-xZjrf8e1.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-R0QKmUpG.js +21 -0
- package/src/ui/public/assets/IconButton-B6eqvssJ.js +1 -0
- package/src/ui/public/assets/IconButton-RlXqKOeu.css +1 -0
- package/src/ui/public/assets/LogList-BPqurE9g.js +7 -0
- package/src/ui/public/assets/LogList-XspUhx-C.css +1 -0
- package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +1 -0
- package/src/ui/public/assets/MindmapView-NdFNlkTF.js +2 -0
- package/src/ui/public/assets/MonitorView-DuSZa8a3.js +1 -0
- package/src/ui/public/assets/MonitorView-DwUwJXnY.css +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BrfKVP-w.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BI7fRsOQ.js +4 -0
- package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-B4E8Ud3F.js +7 -0
- package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +1 -0
- package/src/ui/public/assets/SourceMapView-B0OzHjxP.js +3 -0
- package/src/ui/public/assets/SourceMapView-CfDydtsu.css +1 -0
- package/src/ui/public/assets/SvgIcon-B2h7B2yF.js +1 -0
- package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +1 -0
- package/src/ui/public/assets/TemplateManager-mIWE3Qf8.js +1 -0
- package/src/ui/public/assets/UserInputNode-BafC-B2U.js +1 -0
- package/src/ui/public/assets/UserInputNode-DDNSarag.css +1 -0
- package/src/ui/public/assets/WorkbenchView-BU2kJCAA.css +1 -0
- package/src/ui/public/assets/WorkbenchView-BseGz8hJ.js +56 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-dg2VUnSQ.js → _plugin-vue_export-helper-Dvuzpv2Y.js} +7 -7
- package/src/ui/public/assets/agentConversations-BEKqkGcm.js +5 -0
- package/src/ui/public/assets/{configStore-DtjAc3w9.js → configStore-CX0FkQ94.js} +1 -1
- package/src/ui/public/assets/element-plus-C1xzgNSR.css +1 -0
- package/src/ui/public/assets/{element-plus-DxBPQwL7.js → element-plus-D7LDHcAn.js} +3 -3
- package/src/ui/public/assets/flow-mindmap-zXXF3qDh.js +139 -0
- package/src/ui/public/assets/gitStore-CMuWWr2d.js +6 -0
- package/src/ui/public/assets/index-CB2cIiHl.js +115 -0
- package/src/ui/public/assets/index-DIU5200G.css +1 -0
- package/src/ui/public/assets/{office-docx-CZXLkCf5.js → office-docx-C8sYW8O1.js} +5 -5
- package/src/ui/public/assets/{office-excel-CBKszjVA.js → office-excel-DpZn5My2.js} +1 -1
- package/src/ui/public/assets/{office-pptx-NeUSnN3k.js → office-pptx-BnYBTwT7.js} +1 -1
- package/src/ui/public/assets/previewSandbox-CxLaNg4X.css +1 -0
- package/src/ui/public/assets/previewSandbox-DxR4iN9P.js +0 -0
- package/src/ui/public/assets/toolsStore-DMCSd287.js +1 -0
- package/src/ui/public/assets/{useThemeObserver-E7Hk50l3.js → useThemeObserver-QpveGpnn.js} +1 -1
- package/src/ui/public/assets/{vendor-DnB9EJa3.css → vendor-C-GCxMMu.css} +1 -1
- package/src/ui/public/assets/{vendor-bdHhLiED.js → vendor-CLmPUj3K.js} +8 -8
- package/src/ui/public/assets/vue-flow-CxnoC7Uq.js +39 -0
- package/src/ui/public/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin-ext.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin-italic.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin.woff2 +0 -0
- package/src/ui/public/index.html +28 -27
- package/src/ui/server/index.js +28 -2
- package/src/ui/server/routes/aiContext/collectors.js +360 -0
- package/src/ui/server/routes/aiContext/collectors.test.js +297 -0
- package/src/ui/server/routes/aiContext/index.js +444 -0
- package/src/ui/server/routes/aiContext/index.test.js +433 -0
- package/src/ui/server/routes/aiContext/jobRefresh.js +110 -0
- package/src/ui/server/routes/aiContext/jobRefresh.test.js +191 -0
- package/src/ui/server/routes/aiContext/render.js +366 -0
- package/src/ui/server/routes/aiContext/render.test.js +255 -0
- package/src/ui/server/routes/aiContext/wiring.js +80 -0
- package/src/ui/server/routes/fs.js +43 -0
- package/src/ui/server/routes/mindmap.js +36 -24
- package/src/ui/server/routes/monitor.js +44 -35
- package/src/ui/server/routes/remoteRepos.js +57 -48
- package/src/ui/server/routes/workbench/agentChat.js +94 -10
- package/src/ui/server/routes/workbench/agentChatContext.test.js +171 -0
- package/src/ui/server/routes/workbench/agentChatShared.test.js +33 -3
- package/src/ui/server/routes/workbench/agentEngines.js +77 -0
- package/src/ui/server/routes/workbench/agentEngines.test.js +65 -0
- package/src/ui/server/routes/workbench/agentRoutes.js +208 -48
- package/src/ui/server/routes/workbench/agentRoutes.test.js +160 -4
- package/src/ui/server/routes/workbench/agentSessionStore.js +3 -0
- package/src/ui/server/routes/workbench/envContext.js +61 -64
- package/src/ui/server/routes/workbench/envContext.test.js +133 -63
- package/src/ui/server/routes/workbench/index.js +78 -32
- package/src/ui/server/routes/workbench/runExternalTurn.js +279 -0
- package/src/ui/server/routes/workbench/runExternalTurn.test.js +275 -0
- package/src/ui/server/routes/workbench/shared.js +17 -0
- package/src/ui/server/utils/agentAttachments.js +169 -0
- package/src/ui/server/utils/agentAttachments.test.js +140 -0
- package/src/ui/public/assets/AgentView-DOyN97Ve.js +0 -5
- package/src/ui/public/assets/AgentView-DkM2KXuc.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CJrDr7GV.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-oON3sAj4.js +0 -2
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +0 -1
- package/src/ui/public/assets/BranchSelector-Cpj7eGqY.js +0 -1
- package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +0 -1
- package/src/ui/public/assets/CommitForm-D_ovYhVw.js +0 -6
- package/src/ui/public/assets/CommonDialog-ChZbqfRF.js +0 -1
- package/src/ui/public/assets/CommonDialog-jP0E9mco.css +0 -1
- package/src/ui/public/assets/EditorView-BPzpI0GV.js +0 -1
- package/src/ui/public/assets/EditorView-DZMLvQds.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B_qEKA9L.js +0 -15
- package/src/ui/public/assets/FlowOrchestrationWorkspace-C7yTD1gj.js +0 -21
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +0 -1
- package/src/ui/public/assets/IconButton-DEARmMkZ.js +0 -1
- package/src/ui/public/assets/IconButton-tjkOjzkC.css +0 -1
- package/src/ui/public/assets/LogList-BA_gowxE.js +0 -7
- package/src/ui/public/assets/LogList-BlJplIi1.css +0 -1
- package/src/ui/public/assets/MindmapView-8X6z245q.css +0 -1
- package/src/ui/public/assets/MindmapView-K-XcLLXr.js +0 -2
- package/src/ui/public/assets/MonitorView-C0RL5K9W.js +0 -1
- package/src/ui/public/assets/MonitorView-DetlFFXq.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DC36_vvU.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-Bkqja660.js +0 -4
- package/src/ui/public/assets/RemoteRepoCard-Bsm7cj_H.js +0 -7
- package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +0 -1
- package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +0 -1
- package/src/ui/public/assets/SourceMapView-Y5NKdIk6.js +0 -3
- package/src/ui/public/assets/SvgIcon-D_mFHXP4.js +0 -1
- package/src/ui/public/assets/TemplateManager-BzKVXZhy.js +0 -1
- package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +0 -1
- package/src/ui/public/assets/UserInputNode-BPLK4aYx.js +0 -1
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +0 -1
- package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +0 -1
- package/src/ui/public/assets/WorkbenchView-xbGdiVMQ.js +0 -56
- package/src/ui/public/assets/element-plus-Cwwq-Gje.css +0 -1
- package/src/ui/public/assets/flow-mindmap-BqFb1WO5.js +0 -139
- package/src/ui/public/assets/gitStore-BZo9_zyy.js +0 -6
- package/src/ui/public/assets/index-CHuCYLWm.js +0 -111
- package/src/ui/public/assets/index-CmbatbSR.css +0 -1
- package/src/ui/public/assets/previewSandbox-BTTTXooN.js +0 -0
- package/src/ui/public/assets/previewSandbox-CgdwPcuW.css +0 -1
- package/src/ui/public/assets/vue-flow-fLUQ0_kE.js +0 -39
|
@@ -43,9 +43,8 @@ import {
|
|
|
43
43
|
fsp,
|
|
44
44
|
PROMPTS_FILE,
|
|
45
45
|
TASKS_FILE,
|
|
46
|
-
CONFIG_FILE,
|
|
47
46
|
JOBS_FILE,
|
|
48
|
-
|
|
47
|
+
TRUTH_FILES,
|
|
49
48
|
IMAGES_DIR,
|
|
50
49
|
MAX_IMAGE_BYTES,
|
|
51
50
|
MAX_ATTACHMENTS_PER_TASK,
|
|
@@ -110,6 +109,7 @@ import {
|
|
|
110
109
|
canonicalProjectPath,
|
|
111
110
|
} from './projectRegistry.js';
|
|
112
111
|
import { buildEnvContextBlock } from './envContext.js';
|
|
112
|
+
import { createJobSettledRefresher } from '../aiContext/jobRefresh.js';
|
|
113
113
|
import { resolveInstructionTarget } from './targetResolver.js';
|
|
114
114
|
import {
|
|
115
115
|
readOrchestrator,
|
|
@@ -122,11 +122,24 @@ import {
|
|
|
122
122
|
buildRunningAgents,
|
|
123
123
|
} from './orchestratorStore.js';
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 「任务结束 → 刷快照」的监听器。存成模块级的,是为了在重复装配路由时
|
|
127
|
+
* 先把上一个摘掉(bus 是模块级单例,否则同一次 job 结束会被刷好几遍)。
|
|
128
|
+
* @type {((evt: object) => boolean)|null}
|
|
129
|
+
*/
|
|
130
|
+
let jobSettledHandler = null;
|
|
131
|
+
|
|
125
132
|
/**
|
|
126
133
|
* 解析本次执行用哪个执行器:body.executor 显式指定 > 配置里的全局默认 > 'claude'。
|
|
127
134
|
* 配置读失败一律回落 'claude' —— 执行器选不出来不该挡住任务本身能跑。
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ `configManager` **必须从参数传进来**(2026-09-28 修复):这里是模块作用域,
|
|
137
|
+
* 而 configManager 只是 registerWorkbenchRoutes 的形参 —— 直接引用会抛
|
|
138
|
+
* ReferenceError,被下面这个 try 吞掉,表现为"配置里的全局默认执行器**永远不生效**、
|
|
139
|
+
* 每次都悄悄回落 claude",且只在日志里留一条看不太出问题的 warn。
|
|
140
|
+
* 以后往这个函数里加依赖,一律走形参。
|
|
128
141
|
*/
|
|
129
|
-
async function resolveExecutor(requested) {
|
|
142
|
+
async function resolveExecutor(requested, configManager) {
|
|
130
143
|
const explicit = normalizeTaskExecutor(requested);
|
|
131
144
|
if (explicit) return explicit;
|
|
132
145
|
try {
|
|
@@ -149,8 +162,17 @@ async function resolveExecutor(requested) {
|
|
|
149
162
|
* @param {() => string} deps.getProjectRoomId
|
|
150
163
|
* @param {import('socket.io').Server} deps.io
|
|
151
164
|
* @param {Object} deps.configManager
|
|
165
|
+
* @param {() => object|null} [deps.getAiContextSnapshotter]
|
|
166
|
+
* 工作区状态快照生成器(延迟取值的 getter,见下面 setEnvContextProvider 的注释)
|
|
152
167
|
*/
|
|
153
|
-
export function registerWorkbenchRoutes({
|
|
168
|
+
export function registerWorkbenchRoutes({
|
|
169
|
+
app,
|
|
170
|
+
getCurrentProjectPath,
|
|
171
|
+
getProjectRoomId,
|
|
172
|
+
io,
|
|
173
|
+
configManager,
|
|
174
|
+
getAiContextSnapshotter,
|
|
175
|
+
}) {
|
|
154
176
|
// ════════════════════════════════════════════════════════════════════════
|
|
155
177
|
// §1. AI 生成提示词(基于当前项目)
|
|
156
178
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -563,7 +585,7 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
563
585
|
const liveJob = snapshotJobs().find(j => j.taskId === task.id && (j.status === 'running' || j.status === 'pending'));
|
|
564
586
|
if (liveJob) throw new HttpError(400, '该任务已有正在执行的 job');
|
|
565
587
|
const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
|
|
566
|
-
const executor = await resolveExecutor(req.body?.executor);
|
|
588
|
+
const executor = await resolveExecutor(req.body?.executor, configManager);
|
|
567
589
|
const virtualSub = {
|
|
568
590
|
id: `${task.id}__simple`,
|
|
569
591
|
title: task.title,
|
|
@@ -1097,45 +1119,69 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1097
1119
|
/**
|
|
1098
1120
|
* 注册「运行环境上下文」的提供者 —— 执行引擎(taskRunner)拼 prompt 时会回调它。
|
|
1099
1121
|
*
|
|
1100
|
-
*
|
|
1101
|
-
* 的
|
|
1102
|
-
*
|
|
1122
|
+
* ⚠️ 数据**不再在这里现读**。项目清单 / 各列计数 / 合计口径一律从
|
|
1123
|
+
* aiContext 的 `tasks` 板块拿(collectWorkbenchTasks 的结构化 `data`)。
|
|
1124
|
+
* 以前这里是"读 tasks.json + 最近目录 + buildProjectEntries + summarizeProjectTasks"
|
|
1125
|
+
* 的第二份实现,与 aiContext 那份是同一个口径的两个副本 —— 改一处漏一处不会报错,
|
|
1126
|
+
* 只会让 Agent 在编排台里和智能体页里看到**不同的项目数**。现在只剩一份。
|
|
1103
1127
|
*
|
|
1104
|
-
*
|
|
1105
|
-
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1128
|
+
* 拿之前先**强制刷一次 tasks**:这次派发出去的 prompt 必须看到"刚刚"的看板状态
|
|
1129
|
+
*(别的 g ui 实例刚跑完的任务、刚改过状态的任务)。tasks 的 forceTtlMs = 0,是真刷,
|
|
1130
|
+
* 成本与改动前(每次都现读 tasks.json + refreshJobsFromDisk)完全一样。
|
|
1131
|
+
*
|
|
1132
|
+
* 为什么是 getter 而不是直接传 snapshotter:本函数在 registerWorkbenchRoutes 里被调用,
|
|
1133
|
+
* 而快照生成器在服务端入口创建、注册顺序在它之前就已经建好了 —— 但两者是同一次装配,
|
|
1134
|
+
* 用 getter 可以避免"必须记得按某个顺序注册"这种隐性契约。
|
|
1108
1135
|
*/
|
|
1109
1136
|
setEnvContextProvider(async ({ repoPath } = {}) => {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1137
|
+
const snapshotter = typeof getAiContextSnapshotter === 'function' ? getAiContextSnapshotter() : null;
|
|
1138
|
+
|
|
1139
|
+
if (!snapshotter || typeof snapshotter.refreshSections !== 'function') {
|
|
1140
|
+
// 快照生成器没装配上(理论上不会发生)。至少把**不需要取数**的两样递进去:
|
|
1141
|
+
// 用户偏好与真相源路径 —— 前者少一次就会让任务卡在 https 凭据窗口上。
|
|
1142
|
+
return buildEnvContextBlock({ currentProjectPath: repoPath || '', truthFiles: TRUTH_FILES });
|
|
1143
|
+
}
|
|
1115
1144
|
|
|
1116
|
-
let recentDirs = [];
|
|
1117
1145
|
try {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1146
|
+
await snapshotter.refreshSections(['tasks'], { force: true });
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
// 刷不动就用快照里已有的(哪怕旧一点)—— 上下文是锦上添花,
|
|
1149
|
+
// 不能因为它挂了就让用户的指令执行不了(与 taskRunner.resolveEnvContext 同一条原则)。
|
|
1150
|
+
logger.warn('[workbench] 注入运行环境上下文前刷新 tasks 板块失败,改用现有快照:', err?.message || err);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
let board = null;
|
|
1154
|
+
try {
|
|
1155
|
+
// 用 getSectionResult 而不是 getSnapshot:后者会顺手把其余过期板块推到后台刷,
|
|
1156
|
+
// 派发任务没理由因此去拉 gh/gitee(只在智能体页聊天时才值得那样做)。
|
|
1157
|
+
board = snapshotter.getSectionResult?.('tasks')?.data || null;
|
|
1121
1158
|
} catch (err) {
|
|
1122
|
-
|
|
1123
|
-
logger.warn('[workbench] 注入运行环境上下文时读取最近目录失败,退化为仅按任务路径:', err.message);
|
|
1159
|
+
logger.warn('[workbench] 读取快照里的 tasks 板块失败,本次只注入偏好与真相源路径:', err?.message || err);
|
|
1124
1160
|
}
|
|
1125
1161
|
|
|
1126
|
-
const projects = buildProjectEntries({ recentDirs, tasks });
|
|
1127
1162
|
return buildEnvContextBlock({
|
|
1128
1163
|
currentProjectPath: repoPath || '',
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
jobs: snapshotJobs(),
|
|
1132
|
-
tasksFile: TASKS_FILE,
|
|
1133
|
-
jobsFile: JOBS_FILE,
|
|
1134
|
-
orchestratorFile: ORCHESTRATOR_FILE,
|
|
1135
|
-
configFile: CONFIG_FILE,
|
|
1164
|
+
board,
|
|
1165
|
+
truthFiles: TRUTH_FILES,
|
|
1136
1166
|
});
|
|
1137
1167
|
});
|
|
1138
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* 「工作台任务执行结束」→ 定向刷新快照。
|
|
1171
|
+
*
|
|
1172
|
+
* 挂在 jobStore 的事件总线上(本层已经在消费同一条总线做 SSE 广播)。
|
|
1173
|
+
* 终态判定与去重都在 createJobSettledRefresher 里,见那个文件的头注释。
|
|
1174
|
+
*
|
|
1175
|
+
* `bus.on` 前先 `off` 掉上一个:bus 是模块级单例,重复注册(例如同一进程里
|
|
1176
|
+
* 再次装配路由)会让同一次 job 结束被刷好几遍。
|
|
1177
|
+
*/
|
|
1178
|
+
if (jobSettledHandler) bus.off('event', jobSettledHandler);
|
|
1179
|
+
jobSettledHandler = createJobSettledRefresher({
|
|
1180
|
+
getSnapshotter: getAiContextSnapshotter,
|
|
1181
|
+
onError: (err) => logger.warn('[workbench] 任务结束后刷新工作区快照失败:', err?.message || err),
|
|
1182
|
+
});
|
|
1183
|
+
bus.on('event', jobSettledHandler);
|
|
1184
|
+
|
|
1139
1185
|
app.get('/api/workbench/projects', asyncRoute(async (_req, res) => {
|
|
1140
1186
|
const payload = await loadBoardPayload();
|
|
1141
1187
|
res.json({ success: true, ...payload });
|
|
@@ -1439,7 +1485,7 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1439
1485
|
attachments: [],
|
|
1440
1486
|
};
|
|
1441
1487
|
// 派发控制台可以显式指定执行器;不传则用配置里的全局默认(resolveExecutor 走回落链)
|
|
1442
|
-
const executor = await resolveExecutor(req.body?.executor);
|
|
1488
|
+
const executor = await resolveExecutor(req.body?.executor, configManager);
|
|
1443
1489
|
runSingleSubtask(task, virtualSub, targetPath, '', { executor }).catch(err => {
|
|
1444
1490
|
publish('task:error', { taskId: task.id, error: err.message });
|
|
1445
1491
|
});
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
// 外部 CLI 引擎的「一轮对话」执行器。
|
|
16
|
+
//
|
|
17
|
+
// 与内置 g ai 的**根本差别**:谁跑智能体循环。
|
|
18
|
+
// g ai ── 服务端跑:streamChatOnce 拿 LLM 流 → 服务端 executeTool → 回灌 → 循环
|
|
19
|
+
// 外部 CLI ── CLI 跑:它有自己的工具集、系统提示词、权限模型。服务端退化成一根管子,
|
|
20
|
+
// 只负责 spawn、把它的 NDJSON 翻译成与 g ai **完全相同**的一组 SSE 事件。
|
|
21
|
+
//
|
|
22
|
+
// 所以这个文件里**没有**任何模型调用、工具执行、安全守卫 —— 那些都是 CLI 自己的事。
|
|
23
|
+
//
|
|
24
|
+
// ⚠️ 两条不重复实现的红线(本仓库 agentParity.test.js 就是为这类分叉立的规矩):
|
|
25
|
+
// 1. **spawn 与协议解析一律复用 taskRunner.js**,绝不在这里再写一份。
|
|
26
|
+
// `createClaudeEventHandler` / `createOpencodeEventHandler` / `createCodexEventHandler`
|
|
27
|
+
// 的签名是 `(job, appendOutput, appendThinking, toolCalls)` —— 回调早已抽象好,
|
|
28
|
+
// 这里只是换一组回调(推 SSE 而不是推工作台面板),所以适配层能很薄。
|
|
29
|
+
// 2. **权限档不在这里决定**。taskRunner 的 launcher 已经带死了各自的档位
|
|
30
|
+
// (claude: --permission-mode bypassPermissions --dangerously-skip-permissions;
|
|
31
|
+
// opencode: --auto;codex: --dangerously-bypass-approvals-and-sandbox),
|
|
32
|
+
// 与工作台任务执行保持一致。要改档位请改 launcher,别在这儿加参数。
|
|
33
|
+
//
|
|
34
|
+
// 关于 headless 权限(2026-09-28 实测,三家在非交互下的真实行为):
|
|
35
|
+
// claude 默认档也能跑,但不放开会挡住写操作;工作台那套是 bypassPermissions。
|
|
36
|
+
// codex 默认档是只读沙箱(写文件被拦),所以要 --dangerously-bypass-...
|
|
37
|
+
// opencode **不给 --auto 会卡死**(在等一个永远不会有人回答的交互式批准)。
|
|
38
|
+
// 这正是 AgentView 里没有终端、没人能点"允许"所决定的 —— 必须预设档位。
|
|
39
|
+
|
|
40
|
+
import { spawn, execFile } from 'child_process';
|
|
41
|
+
import { logger } from './shared.js';
|
|
42
|
+
import { engineLabel } from './agentEngines.js';
|
|
43
|
+
import {
|
|
44
|
+
createToolCallTracker,
|
|
45
|
+
createClaudeEventHandler,
|
|
46
|
+
createOpencodeEventHandler,
|
|
47
|
+
createCodexEventHandler,
|
|
48
|
+
launchClaudeInNewWindow,
|
|
49
|
+
launchOpencodeRun,
|
|
50
|
+
launchCodexExec,
|
|
51
|
+
} from './taskRunner.js';
|
|
52
|
+
|
|
53
|
+
/** 引擎 → spawn 实现。复用 taskRunner,不另写(见文件头红线 1)。 */
|
|
54
|
+
export const ENGINE_LAUNCHERS = {
|
|
55
|
+
claude: launchClaudeInNewWindow,
|
|
56
|
+
opencode: launchOpencodeRun,
|
|
57
|
+
codex: launchCodexExec,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** 引擎 → NDJSON 事件处理器工厂。同样复用 taskRunner。 */
|
|
61
|
+
export const ENGINE_HANDLERS = {
|
|
62
|
+
claude: createClaudeEventHandler,
|
|
63
|
+
opencode: createOpencodeEventHandler,
|
|
64
|
+
codex: createCodexEventHandler,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** 输出片段最长保留多少字符(与前端 MAX_LOG_DISPLAY 的口径一致,防止会话文件膨胀) */
|
|
68
|
+
const MAX_ACCUM = 400_000;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 杀掉 CLI 进程树。
|
|
72
|
+
*
|
|
73
|
+
* 为什么不能只 child.kill():claude/opencode/codex 都会自己再拉起子进程(它跑的命令、
|
|
74
|
+
* 它的内部 worker)。只杀父进程的话,用户点了「停止」但机器上还有东西在跑 ——
|
|
75
|
+
* 表现为"界面停了,文件还在被改"。Windows 上必须 taskkill /T。
|
|
76
|
+
*/
|
|
77
|
+
function killChildTree(child) {
|
|
78
|
+
if (!child || !child.pid || child.exitCode !== null) return;
|
|
79
|
+
try {
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
execFile('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true }, () => {});
|
|
82
|
+
} else {
|
|
83
|
+
child.kill('SIGTERM');
|
|
84
|
+
}
|
|
85
|
+
} catch { /* 尽力而为:杀不掉也不该让整轮对话抛错 */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 跑一轮外部 CLI 引擎的对话。
|
|
90
|
+
*
|
|
91
|
+
* @param {object} args
|
|
92
|
+
* @param {object} args.session 会话记录(会被就地更新:messages / engineSessionId)
|
|
93
|
+
* @param {string} args.engine 'claude' | 'opencode' | 'codex'
|
|
94
|
+
* @param {string} args.userMessage 用户这一轮说的话
|
|
95
|
+
* @param {string} args.cwd 工作目录
|
|
96
|
+
* @param {string} [args.promptPrefix] 拼在用户消息**前面**的工作区状态块(见 agentRoutes)
|
|
97
|
+
* @param {string[]} [args.filePaths] 本轮附件落盘的绝对路径,附在用户消息后面
|
|
98
|
+
* @param {AbortSignal} [args.signal] 用户点「停止」时中止
|
|
99
|
+
* @param {(evt: object) => void} args.send SSE 发送器
|
|
100
|
+
* @param {(child: object) => void} [args.onChild] spawn 成功后回传,供路由层做中止兜底
|
|
101
|
+
* @param {object} [args.launchers] 注入口(单测传假的,避免真 spawn CLI)
|
|
102
|
+
* @param {object} [args.handlers] 注入口(同上)
|
|
103
|
+
* @param {Function} [args.killTree] 杀进程树的实现(单测注入,Windows 上真实现走 taskkill)
|
|
104
|
+
* @returns {Promise<{aborted: boolean}>}
|
|
105
|
+
*/
|
|
106
|
+
export async function runExternalTurn({
|
|
107
|
+
session,
|
|
108
|
+
engine,
|
|
109
|
+
userMessage,
|
|
110
|
+
cwd,
|
|
111
|
+
promptPrefix = '',
|
|
112
|
+
filePaths = [],
|
|
113
|
+
signal,
|
|
114
|
+
send,
|
|
115
|
+
onChild,
|
|
116
|
+
launchers = ENGINE_LAUNCHERS,
|
|
117
|
+
handlers = ENGINE_HANDLERS,
|
|
118
|
+
killTree = killChildTree,
|
|
119
|
+
} = {}) {
|
|
120
|
+
const label = engineLabel(engine);
|
|
121
|
+
const launch = launchers[engine];
|
|
122
|
+
const makeHandler = handlers[engine];
|
|
123
|
+
if (typeof launch !== 'function' || typeof makeHandler !== 'function') {
|
|
124
|
+
send({ type: 'error', error: `未知的外部引擎: ${engine}` });
|
|
125
|
+
return { aborted: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
129
|
+
|
|
130
|
+
// 附件只传**路径**,内容不进 prompt —— 与 g ai 那条链路同一条口径
|
|
131
|
+
// (外部 CLI 自己有 read 工具,让它按需读,省 token 也更准)。
|
|
132
|
+
const attachBlock = (Array.isArray(filePaths) ? filePaths : []).filter(Boolean).length > 0
|
|
133
|
+
? `\n\n---\n\n本轮附件已落盘到以下绝对路径,请按需读取:\n${filePaths.filter(Boolean).map(p => `- ${p}`).join('\n')}`
|
|
134
|
+
: '';
|
|
135
|
+
|
|
136
|
+
session.messages.push({ role: 'user', content: userMessage || ' ' });
|
|
137
|
+
|
|
138
|
+
// 工作区状态块拼在**用户消息前面**:外部 CLI 都只认自己的约定文件,
|
|
139
|
+
// 而往项目里写 AGENTS.md / CLAUDE.md 会弄脏用户的工作区(三个引擎实测也不一致:
|
|
140
|
+
// claude 只读 CLAUDE.md、codex 只读 AGENTS.md、opencode 两个都不读)。
|
|
141
|
+
// 所以统一走 prompt 前缀 —— 一份实现喂三家,且不碰用户的仓库。
|
|
142
|
+
const prompt = `${promptPrefix ? `${promptPrefix}\n\n---\n\n` : ''}${userMessage || ''}${attachBlock}`;
|
|
143
|
+
|
|
144
|
+
let child;
|
|
145
|
+
try {
|
|
146
|
+
const launched = await launch(cwd, prompt, session.engineSessionId || '');
|
|
147
|
+
child = launched?.child;
|
|
148
|
+
if (!child) throw new Error('launcher 未返回子进程');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = `${label} 启动失败:${err?.message || err}`;
|
|
151
|
+
send({ type: 'error', error: msg });
|
|
152
|
+
return { aborted: false };
|
|
153
|
+
}
|
|
154
|
+
if (typeof onChild === 'function') onChild(child);
|
|
155
|
+
|
|
156
|
+
// job 只是**载体**:taskRunner 的 handler 与 tracker 都按 job.toolCalls /
|
|
157
|
+
// job.claudeSessionId 这两个字段工作。用一个轻量对象顶上,任务记录不受影响。
|
|
158
|
+
const job = { toolCalls: [], claudeSessionId: session.engineSessionId || '' };
|
|
159
|
+
|
|
160
|
+
let text = '';
|
|
161
|
+
const appendText = (t) => {
|
|
162
|
+
if (typeof t !== 'string' || !t) return;
|
|
163
|
+
text = text.length > MAX_ACCUM ? text : text + t;
|
|
164
|
+
send({ type: 'content', delta: t });
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// 工具调用:tracker 的增量回调直接映射成前端已有的两种事件。
|
|
168
|
+
// 外部 CLI 都给不出"命令执行中的增量输出"(那是 g ai 服务端执行才有的),
|
|
169
|
+
// 所以不发 tool_output —— 前端的 toolCall.result 从空开始、tool_result 时一次填满。
|
|
170
|
+
//
|
|
171
|
+
// ⚠️ **必须先补一条 tool_call_start**,哪怕这次更新已经是终态:
|
|
172
|
+
// claude 会先推 tool_use(running)、再推 tool_result;但 **opencode 与 codex 是
|
|
173
|
+
// 一次性推一条已完成的事件**(opencode state.status=completed / codex item.completed),
|
|
174
|
+
// 根本没有 running 阶段。前端收到 tool_result 时是按 toolCallId 去已有列表里 find 的,
|
|
175
|
+
// 找不到就**静默丢弃** —— 表现是"工具明明跑了,界面上什么都没有"。
|
|
176
|
+
// 所以这里按 id 记一份"已宣告过",第一次见到就补 start。
|
|
177
|
+
const announced = new Set();
|
|
178
|
+
const tracker = createToolCallTracker(job, (updates) => {
|
|
179
|
+
for (const u of updates) {
|
|
180
|
+
if (!u) continue;
|
|
181
|
+
const id = u.id || '';
|
|
182
|
+
if (id && !announced.has(id)) {
|
|
183
|
+
announced.add(id);
|
|
184
|
+
send({ type: 'tool_call_start', toolCallId: id, name: u.name, argsPreview: u.argsPreview || '' });
|
|
185
|
+
}
|
|
186
|
+
if (u.status !== 'running') {
|
|
187
|
+
send({
|
|
188
|
+
type: 'tool_result',
|
|
189
|
+
toolCallId: id,
|
|
190
|
+
result: u.status === 'error' ? (u.error || u.result || '工具执行失败') : (u.result || ''),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const handleEvent = makeHandler(job, appendText, (t) => {
|
|
197
|
+
// 思考过程:与正文分开推,前端有独立可折叠区域
|
|
198
|
+
send({ type: 'thinking', delta: t });
|
|
199
|
+
}, tracker);
|
|
200
|
+
|
|
201
|
+
// NDJSON 分行:最后一段可能不完整,留给下一批(与 taskRunner 同一套做法)
|
|
202
|
+
const lineBuf = { stdout: '', stderr: '' };
|
|
203
|
+
const handleLine = (line, channel) => {
|
|
204
|
+
const trimmed = line.trim();
|
|
205
|
+
if (!trimmed) return;
|
|
206
|
+
if (channel === 'stderr' || !trimmed.startsWith('{')) {
|
|
207
|
+
// 非 JSON 行:原样贴出来(CLI 的告警 / 老版本输出都是这种)
|
|
208
|
+
appendText(trimmed + '\n');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let evt;
|
|
212
|
+
try { evt = JSON.parse(trimmed); } catch { return; }
|
|
213
|
+
handleEvent(evt);
|
|
214
|
+
};
|
|
215
|
+
const parseLines = (channel, buf) => {
|
|
216
|
+
lineBuf[channel] += buf.toString('utf8');
|
|
217
|
+
const lines = lineBuf[channel].split('\n');
|
|
218
|
+
lineBuf[channel] = lines.pop() ?? '';
|
|
219
|
+
for (const line of lines) handleLine(line, channel);
|
|
220
|
+
tracker.flush?.();
|
|
221
|
+
};
|
|
222
|
+
if (child.stdout) child.stdout.on('data', (b) => parseLines('stdout', b));
|
|
223
|
+
if (child.stderr) child.stderr.on('data', (b) => parseLines('stderr', b));
|
|
224
|
+
|
|
225
|
+
// 中止:杀进程树,然后把已完成的部分当成本轮结果落盘(与 g ai 的"已停止"口径一致)
|
|
226
|
+
let aborted = false;
|
|
227
|
+
const onAbort = () => {
|
|
228
|
+
aborted = true;
|
|
229
|
+
killTree(child);
|
|
230
|
+
};
|
|
231
|
+
if (signal) {
|
|
232
|
+
if (signal.aborted) onAbort();
|
|
233
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await new Promise((resolve) => {
|
|
237
|
+
let done = false;
|
|
238
|
+
const finish = () => { if (!done) { done = true; resolve(); } };
|
|
239
|
+
child.on('close', finish);
|
|
240
|
+
child.on('error', (err) => {
|
|
241
|
+
logger.warn(`[agent] ${engine} 子进程出错: ${err?.message || err}`);
|
|
242
|
+
finish();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
246
|
+
|
|
247
|
+
// 进程退出时 stdout 里可能残留最后一段没换行的 NDJSON
|
|
248
|
+
if (lineBuf.stdout.trim()) for (const l of lineBuf.stdout.split('\n')) handleLine(l, 'stdout');
|
|
249
|
+
if (lineBuf.stderr.trim()) for (const l of lineBuf.stderr.split('\n')) handleLine(l, 'stderr');
|
|
250
|
+
tracker.flush?.();
|
|
251
|
+
|
|
252
|
+
// 落盘:assistant(带 tool_calls)+ 逐条 tool 结果,形状与 g ai 那条链路一致,
|
|
253
|
+
// 前端 convertSessionToMessages 的合并逻辑不用改就能渲染历史。
|
|
254
|
+
const calls = Array.isArray(job.toolCalls) ? job.toolCalls : [];
|
|
255
|
+
const assistantMsg = { role: 'assistant', content: text };
|
|
256
|
+
if (calls.length > 0) {
|
|
257
|
+
assistantMsg.tool_calls = calls.map(c => ({
|
|
258
|
+
id: c.id,
|
|
259
|
+
type: 'function',
|
|
260
|
+
function: { name: c.name || 'tool', arguments: c.arguments || '' },
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
session.messages.push(assistantMsg);
|
|
264
|
+
for (const c of calls) {
|
|
265
|
+
session.messages.push({
|
|
266
|
+
role: 'tool',
|
|
267
|
+
tool_call_id: c.id,
|
|
268
|
+
name: c.name || 'tool',
|
|
269
|
+
content: String(c.status === 'error' ? (c.error || c.result || '') : (c.result || '')),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 续聊标识:三家字段名不同(claude session_id / opencode sessionID / codex thread_id),
|
|
274
|
+
// 但 taskRunner 的 handler 已经统一落到 job.claudeSessionId 上,这里只做搬运。
|
|
275
|
+
if (job.claudeSessionId) session.engineSessionId = job.claudeSessionId;
|
|
276
|
+
|
|
277
|
+
send({ type: 'done', content: aborted ? `${text}\n\n[已停止]` : text });
|
|
278
|
+
return { aborted };
|
|
279
|
+
}
|