zen-gitsync 2.17.20 → 2.17.22
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 +22 -14
- package/package.json +9 -1
- package/scripts/release.js +214 -82
- package/src/ui/public/assets/AgentEngineSelector-DR3r5eQE.css +1 -0
- package/src/ui/public/assets/AgentEngineSelector-Dtvp5u3j.js +1 -0
- package/src/ui/public/assets/AgentView-BzfjNz7k.js +1 -0
- package/src/ui/public/assets/AgentView-DnN-vCMi.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-DDYQqoy7.css → AppVersionBadge--5If494j.css} +1 -1
- package/src/ui/public/assets/AppVersionBadge-DAAnAFNp.js +2 -0
- package/src/ui/public/assets/{BranchSelector-F4ibldMN.css → BranchSelector-BTCMYwG0.css} +1 -1
- package/src/ui/public/assets/BranchSelector-C20zVkp_.js +1 -0
- package/src/ui/public/assets/CommitForm-C-cL8_vz.css +1 -0
- package/src/ui/public/assets/CommitForm-COc-NwA8.js +6 -0
- package/src/ui/public/assets/CommonDialog-B7r-EaIr.js +1 -0
- package/src/ui/public/assets/CommonDialog-Bzi5ISqT.css +1 -0
- package/src/ui/public/assets/EditorView-CWCzo4aE.js +1 -0
- package/src/ui/public/assets/EditorView-DnMiWg3T.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-7HZ_CW13.js +14 -0
- package/src/ui/public/assets/FileDiffViewer-B6zy6NiA.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-YKYhtKmd.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-Bg5pSrGY.js +21 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CM2_9TW3.css +1 -0
- package/src/ui/public/assets/IconButton-CyEyp9_D.js +1 -0
- package/src/ui/public/assets/IconButton-W2X09OKH.css +1 -0
- package/src/ui/public/assets/LogList-B0YcWT9U.js +7 -0
- package/src/ui/public/assets/LogList-C7q0Y4J_.css +1 -0
- package/src/ui/public/assets/MindmapView-C4xg3TwX.css +1 -0
- package/src/ui/public/assets/MindmapView-CJI789Gz.js +1 -0
- package/src/ui/public/assets/MonitorView-BWOQfPtI.css +1 -0
- package/src/ui/public/assets/MonitorView-DVrInkz0.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BGrtaPiJ.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-VoQqFee4.css +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-BsM5qfRX.js +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-vHT8jMxW.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BSOph2h2.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-fGsXehZF.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-CG9WPcA_.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-CYXYYtM8.js +1 -0
- package/src/ui/public/assets/SourceMapView-BNyHS4uU.js +3 -0
- package/src/ui/public/assets/SourceMapView-CO_BZJKu.css +1 -0
- package/src/ui/public/assets/{SvgIcon-CDNsk4-Q.css → SvgIcon-BQ6bR7l0.css} +1 -1
- package/src/ui/public/assets/SvgIcon-Cgmy9qD6.js +1 -0
- package/src/ui/public/assets/TemplateManager-DUPI3yBg.js +1 -0
- package/src/ui/public/assets/TemplateManager-DiM3vrbf.css +1 -0
- package/src/ui/public/assets/UserInputNode-DUbPMxsr.js +1 -0
- package/src/ui/public/assets/UserInputNode-d-oRXM_J.css +1 -0
- package/src/ui/public/assets/WorkbenchView-AbJMHSJy.js +6 -0
- package/src/ui/public/assets/WorkbenchView-D8wqeEiu.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-o8uPiAjs.js → _plugin-vue_export-helper-DBFEfstn.js} +5 -5
- package/src/ui/public/assets/{agentConversations-BmOPFH_t.js → agentConversations-CuFU_pdW.js} +1 -1
- package/src/ui/public/assets/configStore-6xFSX9E5.js +1 -0
- package/src/ui/public/assets/element-plus-Bx3j6vzG.js +28 -0
- package/src/ui/public/assets/{flow-mindmap-C22NSZc1.js → flow-mindmap-OPE_D4da.js} +34 -34
- package/src/ui/public/assets/{gitStore-BI_ujQ9r.js → gitStore-BoFStoL3.js} +1 -1
- package/src/ui/public/assets/index-BVkLqLHo.js +29 -0
- package/src/ui/public/assets/index-DVJITdth.css +1 -0
- package/src/ui/public/assets/office-docx-B0sJQnr-.js +31 -0
- package/src/ui/public/assets/{office-excel-BOjWK-1x.js → office-excel-DMRySOdK.js} +1 -1
- package/src/ui/public/assets/office-pptx-Do2blxao.js +60 -0
- package/src/ui/public/assets/previewSandbox-BzitUMhT.js +9 -0
- package/src/ui/public/assets/{previewSandbox-PYnM6Paz.css → previewSandbox-SqNQvuHv.css} +1 -1
- package/src/ui/public/assets/socket-io-TBtw0msS.js +1 -0
- package/src/ui/public/assets/{toolsStore-DgqU92bI.js → toolsStore-B49mCaQ_.js} +1 -1
- package/src/ui/public/assets/{useThemeObserver-CiUpR4zj.js → useThemeObserver-DYxFoHh1.js} +1 -1
- package/src/ui/public/assets/{vendor-CPpc-p6F.js → vendor-CyWsJd6S.js} +45 -52
- package/src/ui/public/assets/vue-flow-fVVGZspa.js +39 -0
- package/src/ui/public/index.html +24 -24
- package/src/ui/server/index.js +8 -0
- package/src/ui/server/routes/config.js +4 -1
- package/src/ui/server/routes/workbench/index.js +165 -7
- package/src/ui/server/routes/workbench/jobActivity.js +254 -0
- package/src/ui/server/routes/workbench/jobActivity.test.js +270 -0
- package/src/ui/server/routes/workbench/jobStore.js +141 -48
- package/src/ui/server/routes/workbench/jobStore.test.js +139 -2
- package/src/ui/server/routes/workbench/liveJobs.js +229 -0
- package/src/ui/server/routes/workbench/liveJobs.test.js +215 -0
- package/src/ui/server/routes/workbench/orchestratorStore.js +164 -2
- package/src/ui/server/routes/workbench/orchestratorStore.progressReport.test.js +177 -0
- package/src/ui/server/routes/workbench/progressReport.js +300 -0
- package/src/ui/server/routes/workbench/progressReport.test.js +375 -0
- package/src/ui/server/routes/workbench/projectRegistry.js +28 -2
- package/src/ui/server/routes/workbench/projectRegistry.test.js +72 -0
- package/src/ui/server/routes/workbench/shared.js +28 -0
- package/src/ui/server/routes/workbench/taskRunner.js +17 -0
- package/src/utils/selfUpdatePolicy.js +123 -0
- package/src/utils/selfUpdatePolicy.test.js +204 -0
- package/src/ui/public/assets/AgentEngineSelector-DIn2efUS.js +0 -2
- package/src/ui/public/assets/AgentEngineSelector-DUf0TfaU.css +0 -1
- package/src/ui/public/assets/AgentView-CS-sFe-x.js +0 -10
- package/src/ui/public/assets/AgentView-CWCc_b0o.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CeHsanRP.js +0 -2
- package/src/ui/public/assets/BranchSelector-CxEoHp2q.js +0 -1
- package/src/ui/public/assets/CommitForm-4kI2csgs.css +0 -1
- package/src/ui/public/assets/CommitForm-DOfYuDqr.js +0 -6
- package/src/ui/public/assets/CommonDialog-CZ5McwHT.js +0 -1
- package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +0 -1
- package/src/ui/public/assets/EditorView-BZjWMYYl.css +0 -1
- package/src/ui/public/assets/EditorView-BxRrHGUj.js +0 -31
- package/src/ui/public/assets/FileDiffViewer-CKcSkMAd.js +0 -15
- package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-DShoSVO7.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BkPZZ0-D.js +0 -21
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +0 -1
- package/src/ui/public/assets/IconButton-D-maDWg7.js +0 -1
- package/src/ui/public/assets/IconButton-RlXqKOeu.css +0 -1
- package/src/ui/public/assets/LogList-Bv5ZqdMf.js +0 -7
- package/src/ui/public/assets/LogList-XspUhx-C.css +0 -1
- package/src/ui/public/assets/MindmapView-C27U_5_C.js +0 -2
- package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +0 -1
- package/src/ui/public/assets/MonitorView-BudY0fTU.js +0 -1
- package/src/ui/public/assets/MonitorView-DwUwJXnY.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-YJPcfS3t.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-Bqd_4eU5.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-CZD_IC5M.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-BUNLKNem.js +0 -4
- package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-Bq1oLUnZ.js +0 -7
- package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +0 -1
- package/src/ui/public/assets/SourceMapView-BCrE4CvT.js +0 -3
- package/src/ui/public/assets/SourceMapView-CfDydtsu.css +0 -1
- package/src/ui/public/assets/SvgIcon-z4ExzvGR.js +0 -1
- package/src/ui/public/assets/TemplateManager-BSq-dNvW.js +0 -1
- package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +0 -1
- package/src/ui/public/assets/UserInputNode-BzbaR9lE.js +0 -1
- package/src/ui/public/assets/UserInputNode-DDNSarag.css +0 -1
- package/src/ui/public/assets/WorkbenchView-BJ7lMt3C.js +0 -70
- package/src/ui/public/assets/WorkbenchView-DGIn0UR4.css +0 -1
- package/src/ui/public/assets/configStore-DrMJDegh.js +0 -1
- package/src/ui/public/assets/element-plus-AaVXaSqv.js +0 -48
- package/src/ui/public/assets/index-Cp5WtCfY.css +0 -1
- package/src/ui/public/assets/index-D_Pwvey4.js +0 -130
- package/src/ui/public/assets/office-docx-CJ6JSCs6.js +0 -32
- package/src/ui/public/assets/office-pptx-CF43ZwXE.js +0 -74
- package/src/ui/public/assets/previewSandbox-B16xvZLS.js +0 -9
- package/src/ui/public/assets/socket-io-BFqQXqAp.js +0 -1
- package/src/ui/public/assets/vue-flow-DwYB6Myr.js +0 -39
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// - Git 状态复用 utils/directoryGitState.js 的批量探测(带并发上限 + TTL 缓存 + 超时)。
|
|
35
35
|
|
|
36
36
|
import { probeDirectoryGitStates } from '../../utils/directoryGitState.js';
|
|
37
|
+
import { pickLiveActivity, tailExcerpt } from './jobActivity.js';
|
|
37
38
|
|
|
38
39
|
/** 看板列,数组顺序即列顺序 */
|
|
39
40
|
export const TASK_COLUMNS = ['todo', 'doing', 'done'];
|
|
@@ -298,10 +299,27 @@ export async function listProjects({
|
|
|
298
299
|
});
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
/**
|
|
302
|
-
|
|
302
|
+
/**
|
|
303
|
+
* 看板任务卡需要的精简字段(完整任务体在 /api/workbench/tasks,这里只给卡片用得到的)。
|
|
304
|
+
*
|
|
305
|
+
* `live`(2026-09-29 补)是「这轮执行现在在干嘛」的一行事实摘要:最近思考、最近一次工具
|
|
306
|
+
* 调用、最新回复、已运行多久、静默多久。在此之前卡片上只有"进行中"三个字 —— 一次跑
|
|
307
|
+
* 二十分钟的任务,用户盯着它看不出是在改代码还是卡住了,只能点进编辑器翻输出。
|
|
308
|
+
*
|
|
309
|
+
* 字段全部由 jobActivity.pickLiveActivity 抽好(与进度报告同一份实现),这里只负责挂上去;
|
|
310
|
+
* 任务没有在跑的 job 时是 null,前端据此不渲染活动区。
|
|
311
|
+
*
|
|
312
|
+
* `lastReply`(2026-09-29 补)是**跑完之后**卡片上留的那一小段「最后说了什么」。
|
|
313
|
+
* 起因:已完成列只有标题 + 时间,而模型常在收尾时反问「要 push 吗?」—— 这类任务
|
|
314
|
+
* 看着"完成了",实际在等用户一句话,用户却只能点进去翻日志才知道。live 帮不上忙:
|
|
315
|
+
* 它只在 running 时才有值,跑完就消失,恰恰把最有信息量的那段收尾丢掉了。
|
|
316
|
+
* 两条互斥:有 job 在跑就只给 live(活动区自己会显示"最新回复"),跑完了才给 lastReply,
|
|
317
|
+
* 免得同一张卡片上出现"最新回复"和"最后回复"两段相似但不同时刻的话。
|
|
318
|
+
*/
|
|
319
|
+
export function decorateTaskForBoard(task, jobsForTask = [], { now = Date.now() } = {}) {
|
|
303
320
|
const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
|
|
304
321
|
const last = latestJob(jobs);
|
|
322
|
+
const live = pickLiveActivity(jobs, now);
|
|
305
323
|
return {
|
|
306
324
|
id: task.id,
|
|
307
325
|
title: task.title || '',
|
|
@@ -310,6 +328,14 @@ export function decorateTaskForBoard(task, jobsForTask = []) {
|
|
|
310
328
|
column: deriveTaskColumn(task, jobs),
|
|
311
329
|
attachmentCount: Array.isArray(task.attachments) ? task.attachments.length : 0,
|
|
312
330
|
runningJobs: jobs.filter(j => j && (j.status === 'running' || j.status === 'pending')).length,
|
|
331
|
+
/** 正在跑时的活动摘要;没有在跑 → null */
|
|
332
|
+
live,
|
|
333
|
+
/**
|
|
334
|
+
* 最近一条 job 的正文摘录("它最后说了什么");有 job 在跑时恒为 null,从没跑过 → ''。
|
|
335
|
+
* 取 `last` 而不是"最近一条有输出的 job":那次执行要是没产出正文,宁可空着,
|
|
336
|
+
* 也不能把上一次的旧话当成这次的结果摆出来。
|
|
337
|
+
*/
|
|
338
|
+
lastReply: live ? null : tailExcerpt(last ? last.output : ''),
|
|
313
339
|
lastJobStatus: last ? last.status : null,
|
|
314
340
|
// 最近一条 job 的结束时间 = 这张卡片"跑完"的时刻。
|
|
315
341
|
// 看板的「已完成」列要按完成时间倒序排(最新完成的在最上边),而 updatedAt 撑不起这个排序:
|
|
@@ -231,6 +231,78 @@ test('decorateTaskForBoard: lastJobEndedAt 取最近一条 job 的结束时间
|
|
|
231
231
|
assert.equal(decorateTaskForBoard(base, running).lastJobEndedAt, '2026-02-01T00:00:00Z');
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
// ── 卡片上的「现在在干嘛」(live) ──────────────────────────────────
|
|
235
|
+
// 卡片过去只有一个"进行中"的圆点,跑二十分钟的任务看不出是在改代码还是卡住了。
|
|
236
|
+
// 这几条守的是:摘要有值、值是真话、没在跑时必须是 null。
|
|
237
|
+
|
|
238
|
+
test('decorateTaskForBoard: 正在跑的任务带上 live(思考 / 工具 / 最新回复 / 时长)', () => {
|
|
239
|
+
const base = { id: 't1', title: '标题', desc: '', createdAt: '2026-02-01T00:00:00Z' };
|
|
240
|
+
const running = [{
|
|
241
|
+
...job('j9', 't1', 'running', '2026-02-01T00:00:00Z'),
|
|
242
|
+
agent: 'claude',
|
|
243
|
+
thinking: '先看锁的写法',
|
|
244
|
+
output: '读完了 login.ts',
|
|
245
|
+
toolCalls: [{ name: 'Read', argsPreview: 'src/login.ts' }],
|
|
246
|
+
lastActivityAt: '2026-02-01T00:09:00Z',
|
|
247
|
+
}];
|
|
248
|
+
const now = Date.parse('2026-02-01T00:10:00Z');
|
|
249
|
+
|
|
250
|
+
const card = decorateTaskForBoard(base, running, { now });
|
|
251
|
+
assert.equal(card.live.agent, 'claude');
|
|
252
|
+
assert.equal(card.live.elapsedMs, 10 * 60 * 1000);
|
|
253
|
+
assert.equal(card.live.lastTool, 'Read src/login.ts');
|
|
254
|
+
assert.equal(card.live.lastThought, '先看锁的写法');
|
|
255
|
+
assert.equal(card.live.lastLine, '读完了 login.ts');
|
|
256
|
+
// 静默 1 分钟:到阈值才给值(阈值见 jobActivity.SILENT_NOTABLE_MS)
|
|
257
|
+
assert.equal(card.live.silentMs, 60 * 1000);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('decorateTaskForBoard: 没有在跑的 job 时 live 是 null(前端据此不渲染活动区)', () => {
|
|
261
|
+
const base = { id: 't1', title: '', desc: '', createdAt: '2025-12-31T00:00:00Z' };
|
|
262
|
+
assert.equal(decorateTaskForBoard(base, []).live, null);
|
|
263
|
+
assert.equal(decorateTaskForBoard(base, [
|
|
264
|
+
job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:10:00Z'),
|
|
265
|
+
]).live, null);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── 卡片上的「最后说了什么」(lastReply) ──────────────────────────────
|
|
269
|
+
// 已完成列过去只有标题 + 时间,而模型收尾时常反问「要 push 吗?」—— 这类任务看着完成了、
|
|
270
|
+
// 其实在等用户回话。这几条守的是:跑完要有摘录、在跑时不能有(与 live 互斥)、
|
|
271
|
+
// 没产出过正文时不能拿别的 job 的旧话顶上。
|
|
272
|
+
|
|
273
|
+
test('decorateTaskForBoard: 跑完的任务带上 lastReply(最近一条 job 的正文尾部)', () => {
|
|
274
|
+
const base = { id: 't1', title: '标题', desc: '', createdAt: '2026-01-01T00:00:00Z' };
|
|
275
|
+
const jobs = [
|
|
276
|
+
{ ...job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:10:00Z'), output: '先看了一遍' },
|
|
277
|
+
{ ...job('j2', 't1', 'done', '2026-01-03T00:00:00Z', '2026-01-03T00:20:00Z'), output: '已经改好了。要 push 吗?' },
|
|
278
|
+
];
|
|
279
|
+
assert.equal(decorateTaskForBoard(base, jobs).lastReply, '已经改好了。要 push 吗?');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('decorateTaskForBoard: 有 job 在跑时 lastReply 是 null(与 live 互斥,卡片上不留两段相似的话)', () => {
|
|
283
|
+
const base = { id: 't1', title: '', desc: '', createdAt: '2026-01-01T00:00:00Z' };
|
|
284
|
+
const running = [
|
|
285
|
+
{ ...job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:10:00Z'), output: '上一轮的收尾' },
|
|
286
|
+
{ ...job('j2', 't1', 'running', '2026-02-01T00:00:00Z'), output: '这一轮刚开始' },
|
|
287
|
+
];
|
|
288
|
+
const card = decorateTaskForBoard(base, running, { now: Date.parse('2026-02-01T00:01:00Z') });
|
|
289
|
+
assert.equal(card.live.lastLine, '这一轮刚开始');
|
|
290
|
+
assert.equal(card.lastReply, null);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('decorateTaskForBoard: 没跑过 / 那次没写正文时 lastReply 是空串(不拿上一条 job 的旧话顶)', () => {
|
|
294
|
+
const base = { id: 't1', title: '', desc: '', createdAt: '2026-01-01T00:00:00Z' };
|
|
295
|
+
assert.equal(decorateTaskForBoard(base, []).lastReply, '');
|
|
296
|
+
assert.equal(decorateTaskForBoard(base, [
|
|
297
|
+
job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:10:00Z'),
|
|
298
|
+
]).lastReply, '');
|
|
299
|
+
// 最近一条(j2)没产出正文:宁可空着,也不能把 j1 的话当成这次的结果摆出来
|
|
300
|
+
assert.equal(decorateTaskForBoard(base, [
|
|
301
|
+
{ ...job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:10:00Z'), output: '上一轮说的' },
|
|
302
|
+
job('j2', 't1', 'error', '2026-01-03T00:00:00Z', '2026-01-03T00:00:05Z'),
|
|
303
|
+
]).lastReply, '');
|
|
304
|
+
});
|
|
305
|
+
|
|
234
306
|
// ── 编排状态与活动流 ────────────────────────────────────────────────
|
|
235
307
|
|
|
236
308
|
test('normalizeOrchestrator: 缺字段/脏数据都要能起来', () => {
|
|
@@ -41,14 +41,42 @@ export const INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-instruction.json');
|
|
|
41
41
|
// 执行日志持久化:jobs.json 是历史档案,jobs-config.json 是保留策略
|
|
42
42
|
export const JOBS_FILE = path.join(DATA_DIR, 'jobs.json');
|
|
43
43
|
export const JOBS_CONFIG_FILE = path.join(DATA_DIR, 'jobs-config.json');
|
|
44
|
+
// 落盘防抖。名字里的 jobs 指的是"执行记录"这件事本身:2026-09-28 起它只给
|
|
45
|
+
// **运行中**那一份(live-jobs)用,历史档案 jobs.json 改成只在终态写(见 jobStore.js)。
|
|
44
46
|
export const JOBS_SAVE_DEBOUNCE_MS = 1500;
|
|
45
47
|
export const DEFAULT_JOBS_CONFIG = { maxCount: 500, maxSizeMB: 256 };
|
|
46
48
|
|
|
49
|
+
// 「运行中」job 的跨实例广播目录:每个 g ui 进程只写自己的 <pid>.json。
|
|
50
|
+
// 为什么不复用 jobs.json:那份是历史档案,实测本机 48 条就有 7.5MB,而跑任务期间
|
|
51
|
+
// 1.5s 一次的防抖落盘要读+解析+序列化+保留策略再写一遍 —— 为了同步一条"正在跑"
|
|
52
|
+
// 的状态去反复搬 7.5MB 不划算(还要阻塞事件循环,反过来卡住流式输出)。
|
|
53
|
+
// 每进程一个文件同时也消掉了多进程 read-modify-write 的丢更新(同 instances/ 的理由)。
|
|
54
|
+
export const LIVE_JOBS_DIR = path.join(DATA_DIR, 'live-jobs');
|
|
55
|
+
|
|
47
56
|
// 主 Agent 编排台:调度开关(active)+ 人类干预指令存档。
|
|
48
57
|
// 指令存档是「我说过什么」的流水,不参与执行逻辑,只用于控制台日志流回放。
|
|
49
58
|
export const ORCHESTRATOR_FILE = path.join(DATA_DIR, 'orchestrator.json');
|
|
50
59
|
export const MAX_ORCHESTRATOR_INSTRUCTIONS = 200;
|
|
51
60
|
|
|
61
|
+
// 进度报告:**配置与历史分两个文件**。
|
|
62
|
+
// 报告历史(每份都带着当时的任务事实,几 KB 一份)如果塞进 orchestrator.json,
|
|
63
|
+
// 那个被 5s 轮询的接口每轮都要把几十上百 KB 解析一遍 —— 与 jobs.json / jobs-config.json
|
|
64
|
+
// 分成两个文件是同一个理由:轮询读配置,历史按需读。
|
|
65
|
+
export const ORCHESTRATOR_REPORTS_FILE = path.join(DATA_DIR, 'orchestrator-reports.json');
|
|
66
|
+
export const MAX_PROGRESS_REPORTS = 20;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 自动进度报告允许的间隔(毫秒),0 = 关闭。
|
|
70
|
+
*
|
|
71
|
+
* 用白名单而不是"任意正整数":这个值直接决定**每隔多久烧一次模型额度**,
|
|
72
|
+
* 一个手滑输进去的 5(毫秒)会在几分钟里把额度打光,而用户不会立刻意识到。
|
|
73
|
+
* 前端给的就是这几个档位,服务端再卡一道。
|
|
74
|
+
*/
|
|
75
|
+
export const PROGRESS_REPORT_INTERVALS_MS = [0, 5, 10, 15, 30, 60].map(min => min * 60 * 1000);
|
|
76
|
+
|
|
77
|
+
/** 缺省间隔:10 分钟。装完就能用,嫌频繁就在面板上改(改设置不重启服务) */
|
|
78
|
+
export const DEFAULT_PROGRESS_REPORT_INTERVAL_MS = 10 * 60 * 1000;
|
|
79
|
+
|
|
52
80
|
/**
|
|
53
81
|
* 「真相源文件」清单 —— 注入给模型、让它需要细节时自己去读全文的那几个文件。
|
|
54
82
|
*
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
cancelledJobs,
|
|
51
51
|
publish,
|
|
52
52
|
flushJobsSaveNow,
|
|
53
|
+
scheduleActiveJobsSave,
|
|
53
54
|
} from './jobStore.js';
|
|
54
55
|
|
|
55
56
|
// ── 运行环境上下文的提供者 ────────────────────────────────────────────────
|
|
@@ -915,6 +916,10 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
915
916
|
};
|
|
916
917
|
jobs.set(jobId, job);
|
|
917
918
|
publish('job:update', job);
|
|
919
|
+
// 跨实例可见性:别的 g ui 的看板列完全由执行记录推导,「进行中」必须能让它们
|
|
920
|
+
// 读到一个 running/pending 记录 —— 终态以前从来不落盘,所以这里(以及下面的
|
|
921
|
+
// 流式 chunk 与终态 flush)是运行中 job 唯一的对外通路,见 liveJobs.js。
|
|
922
|
+
scheduleActiveJobsSave();
|
|
918
923
|
|
|
919
924
|
// 声明在 try 之外:启动器就抛错时 finally 也要能安全收口(见 toolTracker?.seal)
|
|
920
925
|
let toolTracker = null;
|
|
@@ -930,6 +935,11 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
930
935
|
// 保存 child 引用,供 cancel 接口调用 kill
|
|
931
936
|
job.child = child;
|
|
932
937
|
job.startedAt = nowIso();
|
|
938
|
+
// 最后一次产出的时刻(正文 / 思考 / 工具调用,见下面三处赋值)。
|
|
939
|
+
// 进度报告靠它说"已经 N 分钟没动静了" —— 光看工具调用次数看不出"卡住",
|
|
940
|
+
// 次数多也可能是一小时前那一批。起跑时先按开始时间记账,
|
|
941
|
+
// 这样"跑了 5 分钟一个事件都没出"本身就是可判定的静默。
|
|
942
|
+
job.lastActivityAt = job.startedAt;
|
|
933
943
|
job.status = 'running';
|
|
934
944
|
publish('job:update', job);
|
|
935
945
|
|
|
@@ -947,6 +957,7 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
947
957
|
// 追加正文(尾部截断保底 + 增量推送给前端)
|
|
948
958
|
const appendOutput = (text) => {
|
|
949
959
|
if (!text) return;
|
|
960
|
+
job.lastActivityAt = nowIso();
|
|
950
961
|
const prevLen = job.output.length;
|
|
951
962
|
job.output = (job.output + text).slice(-MAX_OUTPUT);
|
|
952
963
|
const delta = job.output.slice(prevLen);
|
|
@@ -963,6 +974,7 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
963
974
|
};
|
|
964
975
|
const appendThinking = (text) => {
|
|
965
976
|
if (!text) return;
|
|
977
|
+
job.lastActivityAt = nowIso();
|
|
966
978
|
const prevLen = job.thinking.length;
|
|
967
979
|
job.thinking = (job.thinking + text).slice(-MAX_THINKING);
|
|
968
980
|
thinkingBatch += job.thinking.slice(prevLen);
|
|
@@ -971,6 +983,8 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
971
983
|
// 工具调用:写进 job.toolCalls(随 job 落盘),并按批推 job:toolcalls 给前端。
|
|
972
984
|
// 收口(把没收尾的调用标终态)在 finally 里做,见下方 toolTracker.seal()。
|
|
973
985
|
toolTracker = createToolCallTracker(job, (updates) => {
|
|
986
|
+
// 工具调用也算"有产出":一次 Bash 跑十分钟不吐字,中间不该被判成静默
|
|
987
|
+
job.lastActivityAt = nowIso();
|
|
974
988
|
publish('job:toolcalls', { id: job.id, updates });
|
|
975
989
|
});
|
|
976
990
|
|
|
@@ -1001,6 +1015,9 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
|
1001
1015
|
for (const line of lines) handleLine(line, channel);
|
|
1002
1016
|
flushThinkingBatch();
|
|
1003
1017
|
toolTracker?.flush();
|
|
1018
|
+
// 每批 chunk 后防抖写一次 live-jobs(1.5s 内只写一次),让别的实例看得到进度。
|
|
1019
|
+
// 不写 jobs.json —— 那份是历史档案,几十 MB 级别的东西不该被 1.5s 一次重写。
|
|
1020
|
+
scheduleActiveJobsSave();
|
|
1004
1021
|
};
|
|
1005
1022
|
if (child.stdout) child.stdout.on('data', (buf) => parseLines('stdout', buf));
|
|
1006
1023
|
if (child.stderr) child.stderr.on('data', (buf) => parseLines('stderr', buf));
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
// 发布后自更新「这一轮要不要真调 npm install」的判定 —— 纯函数,不碰网络/IO,便于单测。
|
|
16
|
+
//
|
|
17
|
+
// 为什么要把这个判定单独抽出来(2026-09-29 v2.17.21 事故):
|
|
18
|
+
// 原来的写法是 `if (probe.ok) { 真装 }` —— 探针(直接 GET 一次 tarball URL)是**硬闸门**。
|
|
19
|
+
// 那次探针连着 35 轮全报 `HTTP 404`,于是 600s 里 `npm install` **一次都没被执行**,
|
|
20
|
+
// 唯一发生的事就是每 15s 打印一行"还取不到";而发布脚本放弃后,用户手动
|
|
21
|
+
// `npm install -g zen-gitsync@2.17.21` 16s 就装上了。
|
|
22
|
+
//
|
|
23
|
+
// 结论:探针只是"省一次注定失败的调用"的**优化**,不是事实。真正的判据只有 npm 自己。
|
|
24
|
+
// 所以判定改为「两个就绪信号任一为真就真试 + 探针不可信时按节奏强制真试」,
|
|
25
|
+
// 超时前还会补最后一次真试(见 scripts/release.js: selfUpdateGlobal)。
|
|
26
|
+
//
|
|
27
|
+
// 两个就绪信号各自对应一条能绕开对方缓存的安装路径(这就是为什么两条都要看):
|
|
28
|
+
// - packument 里已有这个版本 → `npm install -g <pkg>@<ver>` 能解析;
|
|
29
|
+
// - tarball 可取 → `npm install -g <tarball URL>` 直连能装(不经过 packument 缓存)。
|
|
30
|
+
|
|
31
|
+
// 探针一直说"取不到"时,每隔几轮强制真试一次。
|
|
32
|
+
// 4 轮 × 前段 15s ≈ 每 1 分钟一次真试;窗口有 30 分钟,能拿到二十几次真装机会,
|
|
33
|
+
// 而不是 0 次;单次失败(npm 报 404/ETARGET)只花几秒,不心疼。
|
|
34
|
+
export const FORCE_INSTALL_EVERY = 4
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 判定这一轮是否真的调用 npm 安装。
|
|
38
|
+
*
|
|
39
|
+
* @param {object} input
|
|
40
|
+
* @param {number} input.attempt - 从 1 开始的轮次
|
|
41
|
+
* @param {{ok: boolean, status?: number, error?: string, unknown?: boolean}} input.probe
|
|
42
|
+
* isTarballFetchable() 的结果:`ok=true/status=0(网络错)/status=404` 三种形态
|
|
43
|
+
* @param {boolean} input.packumentHasVersion - `npm view <pkg>@<ver> version` 是否拿得到
|
|
44
|
+
* @param {number} [input.forceEvery] - 强制真试的间隔轮数,0/负数 = 不强制
|
|
45
|
+
* @returns {{attempt: boolean, reason: string}}
|
|
46
|
+
* reason: packument-ready | tarball-ready | probe-failed | forced | probe-says-missing
|
|
47
|
+
*/
|
|
48
|
+
export function shouldAttemptInstall({ attempt, probe, packumentHasVersion, forceEvery = FORCE_INSTALL_EVERY }) {
|
|
49
|
+
// ① 任何一条路已被证实可用 → 立刻真装,不浪费窗口
|
|
50
|
+
if (packumentHasVersion) return { attempt: true, reason: 'packument-ready' }
|
|
51
|
+
if (probe?.ok) return { attempt: true, reason: 'tarball-ready' }
|
|
52
|
+
|
|
53
|
+
// ② 探针"没结论"(网络抖动 / 拿不到 fetch / 超时返回 status=0)→ 当成可用,由 npm 自己去判。
|
|
54
|
+
// 探针失败 ≠ tarball 不存在;把它当"取不到"会平白削掉一整轮机会。
|
|
55
|
+
if (!probe || probe.status === 0 || probe.error) return { attempt: true, reason: 'probe-failed' }
|
|
56
|
+
|
|
57
|
+
// ③ 探针明确说"这个路径 404":这是唯一允许跳过的情形,但必须按期强制真试 ——
|
|
58
|
+
// 404 可能来自边缘节点的负缓存 / 透明代理,而不是 registry 真的没有这个对象。
|
|
59
|
+
if (Number.isFinite(forceEvery) && forceEvery > 0 && attempt % forceEvery === 0) {
|
|
60
|
+
return { attempt: true, reason: 'forced' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { attempt: false, reason: 'probe-says-missing' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 轮询窗口用尽时,还要不要补最后一次真试。
|
|
68
|
+
*
|
|
69
|
+
* 只在这一整轮"从头到尾没真装过"时才补:那种情况下窗口的内容是纯等待,
|
|
70
|
+
* 一次事实判据都没取到(探针误报时就是这个形态,等于白等 600s)。
|
|
71
|
+
* 若最后一轮刚试过(只差几秒),再试一次没意义。
|
|
72
|
+
*
|
|
73
|
+
* @param {{installAttempts: number, lastRoundAttempted: boolean}} input
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function shouldFinalAttempt({ installAttempts, lastRoundAttempted }) {
|
|
77
|
+
if (lastRoundAttempted) return false
|
|
78
|
+
// installAttempts === 0 是主目标(整轮零真试);> 0 但最后一轮在跳过,也顺手补一次。
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 这一轮的轮询间隔:前段密集、后段退避。
|
|
84
|
+
*
|
|
85
|
+
* 为什么窗口加长后必须退避(v2.17.21 之后):registry 元数据/对象就绪最长见过
|
|
86
|
+
* 十几分钟(比原来 600s 的上限还长),所以上限提到 30 分钟。但 30 分钟里仍按 15s
|
|
87
|
+
* 一跳 = 一百多轮,既是上百次 `npm view` + 上百次探针,也把日志刷成噪声。
|
|
88
|
+
* 前段(默认 600s,原来就是"整段窗口")保持 15s 的灵敏度,之后退到 60s 慢炖。
|
|
89
|
+
*
|
|
90
|
+
* @param {object} input
|
|
91
|
+
* @param {number} input.elapsedMs - 从轮询开始到现在
|
|
92
|
+
* @param {number} input.fastWindowMs - 前段长度,超过它就开始退避
|
|
93
|
+
* @param {number} input.fastIntervalMs - 前段间隔
|
|
94
|
+
* @param {number} input.slowIntervalMs - 后段间隔
|
|
95
|
+
* @returns {number} 毫秒
|
|
96
|
+
*/
|
|
97
|
+
export function nextPollIntervalMs({ elapsedMs, fastWindowMs, fastIntervalMs, slowIntervalMs }) {
|
|
98
|
+
return elapsedMs < fastWindowMs ? fastIntervalMs : slowIntervalMs
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 把 npm 的失败输出归成一个短码,用来在放弃时汇总"到底卡在哪"。
|
|
103
|
+
*
|
|
104
|
+
* 为什么需要:原先只打最后一次的失败原文,于是"E404(对象/元数据还没就绪)"
|
|
105
|
+
* 与"EPERM(文件被占)"分不清 —— 两者的处理方式完全相反(一个只能等,一个要杀实例)。
|
|
106
|
+
* 汇总成 `E404 ×8 / ETARGET ×2` 这样一行,下一次事故就不用再靠猜。
|
|
107
|
+
*
|
|
108
|
+
* @param {string} output - npm install 的 stdout+stderr
|
|
109
|
+
* @returns {string} E404 | ETARGET | EPERM | EBUSY | ENOTFOUND | ETIMEDOUT | ENETUNREACH | '其他'
|
|
110
|
+
*/
|
|
111
|
+
export function classifyInstallError(output) {
|
|
112
|
+
const s = String(output || '')
|
|
113
|
+
if (!s) return '其他'
|
|
114
|
+
// 顺序有讲究:E404 / ETARGET 是"registry 还没就绪",EPERM / EBUSY 是"文件被占",
|
|
115
|
+
// 两类混在一起时优先报前者(它是自更新这一段的主题),但两者都会各自计数。
|
|
116
|
+
if (/EPERM|EBUSY|operation not permitted|Failed to remove/i.test(s)) return 'EPERM'
|
|
117
|
+
if (/\bETARGET\b/.test(s)) return 'ETARGET'
|
|
118
|
+
if (/\bE404\b|404 Not Found/i.test(s)) return 'E404'
|
|
119
|
+
if (/\bENOTFOUND\b/.test(s)) return 'ENOTFOUND'
|
|
120
|
+
if (/\bETIMEDOUT\b|timed out/i.test(s)) return 'ETIMEDOUT'
|
|
121
|
+
if (/\bENETUNREACH\b|ECONNRESET|ECONNREFUSED/i.test(s)) return 'ENETUNREACH'
|
|
122
|
+
return '其他'
|
|
123
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
// src/utils/selfUpdatePolicy.js 纯函数单元测试(node:test 内置)。
|
|
16
|
+
//
|
|
17
|
+
// 回归的核心一条:v2.17.21 那次 release 里 35 轮探针全报 404,结果是
|
|
18
|
+
// "npm install 一次都没真跑过"。这里用同样的输入序列断言"必须拿到真装机会"。
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
import {
|
|
22
|
+
shouldAttemptInstall,
|
|
23
|
+
shouldFinalAttempt,
|
|
24
|
+
nextPollIntervalMs,
|
|
25
|
+
classifyInstallError,
|
|
26
|
+
FORCE_INSTALL_EVERY,
|
|
27
|
+
} from './selfUpdatePolicy.js'
|
|
28
|
+
|
|
29
|
+
const MISSING = { ok: false, status: 404 }
|
|
30
|
+
const NET_ERROR = { ok: false, status: 0, error: 'fetch failed' }
|
|
31
|
+
const OK = { ok: true, status: 206 }
|
|
32
|
+
|
|
33
|
+
// ========== 两个就绪信号 ==========
|
|
34
|
+
|
|
35
|
+
test('shouldAttemptInstall: packument 已有该版本 → 真装(即使探针说 404)', () => {
|
|
36
|
+
const plan = shouldAttemptInstall({ attempt: 1, probe: MISSING, packumentHasVersion: true })
|
|
37
|
+
assert.deepEqual(plan, { attempt: true, reason: 'packument-ready' })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('shouldAttemptInstall: tarball 已可取 → 真装', () => {
|
|
41
|
+
const plan = shouldAttemptInstall({ attempt: 1, probe: OK, packumentHasVersion: false })
|
|
42
|
+
assert.deepEqual(plan, { attempt: true, reason: 'tarball-ready' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('shouldAttemptInstall: packument 优先于 tarball(两条路都在时按前者排 spec)', () => {
|
|
46
|
+
const plan = shouldAttemptInstall({ attempt: 1, probe: OK, packumentHasVersion: true })
|
|
47
|
+
assert.equal(plan.reason, 'packument-ready')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ========== 探针"没结论"不许当结果 ==========
|
|
51
|
+
|
|
52
|
+
test('shouldAttemptInstall: 探针网络失败(status=0)→ 真装,不跳过', () => {
|
|
53
|
+
const plan = shouldAttemptInstall({ attempt: 1, probe: NET_ERROR, packumentHasVersion: false })
|
|
54
|
+
assert.deepEqual(plan, { attempt: true, reason: 'probe-failed' })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('shouldAttemptInstall: 探针带 error 字段 → 真装,不跳过', () => {
|
|
58
|
+
const plan = shouldAttemptInstall({
|
|
59
|
+
attempt: 2,
|
|
60
|
+
probe: { ok: false, status: 0, error: 'ECONNRESET' },
|
|
61
|
+
packumentHasVersion: false,
|
|
62
|
+
})
|
|
63
|
+
assert.equal(plan.attempt, true)
|
|
64
|
+
assert.equal(plan.reason, 'probe-failed')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('shouldAttemptInstall: 拿不到探针结果(undefined/null)→ 真装,不跳过', () => {
|
|
68
|
+
for (const probe of [undefined, null]) {
|
|
69
|
+
const plan = shouldAttemptInstall({ attempt: 1, probe, packumentHasVersion: false })
|
|
70
|
+
assert.equal(plan.attempt, true, `probe=${probe} 时必须真装`)
|
|
71
|
+
assert.equal(plan.reason, 'probe-failed')
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('shouldAttemptInstall: 老 Node 拿不到 fetch 时的 unknown 形态 → 真装', () => {
|
|
76
|
+
const plan = shouldAttemptInstall({
|
|
77
|
+
attempt: 1,
|
|
78
|
+
probe: { ok: true, unknown: true },
|
|
79
|
+
packumentHasVersion: false,
|
|
80
|
+
})
|
|
81
|
+
assert.deepEqual(plan, { attempt: true, reason: 'tarball-ready' })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ========== 明确 404:可以跳过,但必须按期强制真试 ==========
|
|
85
|
+
|
|
86
|
+
test('shouldAttemptInstall: 探针 404 且未到强制轮 → 跳过', () => {
|
|
87
|
+
for (const attempt of [1, 2, 3]) {
|
|
88
|
+
const plan = shouldAttemptInstall({ attempt, probe: MISSING, packumentHasVersion: false })
|
|
89
|
+
assert.equal(plan.attempt, false, `第 ${attempt} 轮不该跳过以外的行为`)
|
|
90
|
+
assert.equal(plan.reason, 'probe-says-missing')
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('shouldAttemptInstall: 探针 404,第 4/8 轮强制真试', () => {
|
|
95
|
+
for (const attempt of [4, 8, 12]) {
|
|
96
|
+
const plan = shouldAttemptInstall({ attempt, probe: MISSING, packumentHasVersion: false })
|
|
97
|
+
assert.deepEqual(plan, { attempt: true, reason: 'forced' }, `第 ${attempt} 轮应强制`)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('shouldAttemptInstall: 探针回 403/500 等非 404 状态 → 同样按强制节奏,不当成"可取"', () => {
|
|
102
|
+
const early = shouldAttemptInstall({ attempt: 1, probe: { ok: false, status: 500 }, packumentHasVersion: false })
|
|
103
|
+
assert.equal(early.attempt, false)
|
|
104
|
+
const forced = shouldAttemptInstall({ attempt: 4, probe: { ok: false, status: 500 }, packumentHasVersion: false })
|
|
105
|
+
assert.equal(forced.reason, 'forced')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('shouldAttemptInstall: forceEvery=0 关闭强制 → 404 一直跳过(只有探针/超时兜底)', () => {
|
|
109
|
+
for (const attempt of [1, 4, 8, 40]) {
|
|
110
|
+
const plan = shouldAttemptInstall({
|
|
111
|
+
attempt,
|
|
112
|
+
probe: MISSING,
|
|
113
|
+
packumentHasVersion: false,
|
|
114
|
+
forceEvery: 0,
|
|
115
|
+
})
|
|
116
|
+
assert.equal(plan.attempt, false)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('shouldAttemptInstall: forceEvery 可覆盖(测试/调参用)', () => {
|
|
121
|
+
const plan = shouldAttemptInstall({
|
|
122
|
+
attempt: 2,
|
|
123
|
+
probe: MISSING,
|
|
124
|
+
packumentHasVersion: false,
|
|
125
|
+
forceEvery: 2,
|
|
126
|
+
})
|
|
127
|
+
assert.equal(plan.reason, 'forced')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ========== 事故回归:35 轮全 404,不许零真试 ==========
|
|
131
|
+
|
|
132
|
+
test('回归 v2.17.21:探针连续 35 轮 404 时,真试次数必须 ≥ 8 且第 4 轮就开始', () => {
|
|
133
|
+
const attempts = []
|
|
134
|
+
for (let attempt = 1; attempt <= 35; attempt += 1) {
|
|
135
|
+
const plan = shouldAttemptInstall({ attempt, probe: MISSING, packumentHasVersion: false })
|
|
136
|
+
if (plan.attempt) attempts.push(attempt)
|
|
137
|
+
}
|
|
138
|
+
// 旧实现:attempts 为空(probe.ok 恒 false → 一次 npm 都没调)
|
|
139
|
+
assert.ok(attempts.length > 0, '不允许出现"整轮零真试"')
|
|
140
|
+
assert.equal(attempts[0], FORCE_INSTALL_EVERY, '第 4 轮就必须开始真试,而不是等满 600s')
|
|
141
|
+
assert.ok(attempts.length >= 8, `600s(35 轮)里至少 8 次真装机会,实际 ${attempts.length}`)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ========== 超时前的最后一次真试 ==========
|
|
145
|
+
|
|
146
|
+
test('shouldFinalAttempt: 整轮没真装过 → 补最后一次', () => {
|
|
147
|
+
assert.equal(shouldFinalAttempt({ installAttempts: 0, lastRoundAttempted: false }), true)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('shouldFinalAttempt: 最后一轮刚试过 → 不补(几秒前才失败,再试无意义)', () => {
|
|
151
|
+
assert.equal(shouldFinalAttempt({ installAttempts: 9, lastRoundAttempted: true }), false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('shouldFinalAttempt: 试过但最后一轮在跳过 → 仍补一次', () => {
|
|
155
|
+
assert.equal(shouldFinalAttempt({ installAttempts: 8, lastRoundAttempted: false }), true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ========== 轮询间隔退避(窗口从 600s 提到 30 分钟后必须有) ==========
|
|
159
|
+
|
|
160
|
+
const INTERVAL_CFG = { fastWindowMs: 600_000, fastIntervalMs: 15_000, slowIntervalMs: 60_000 }
|
|
161
|
+
|
|
162
|
+
test('nextPollIntervalMs: 前段(600s 内)保持密集 15s', () => {
|
|
163
|
+
for (const elapsedMs of [0, 1_000, 599_999]) {
|
|
164
|
+
assert.equal(nextPollIntervalMs({ ...INTERVAL_CFG, elapsedMs }), 15_000, `elapsed=${elapsedMs}`)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('nextPollIntervalMs: 到点即切到慢炖 60s(边界取后段)', () => {
|
|
169
|
+
assert.equal(nextPollIntervalMs({ ...INTERVAL_CFG, elapsedMs: 600_000 }), 60_000)
|
|
170
|
+
assert.equal(nextPollIntervalMs({ ...INTERVAL_CFG, elapsedMs: 1_500_000 }), 60_000)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('nextPollIntervalMs: 30 分钟窗口的总轮数可控(不是"15s 跳 120 轮")', () => {
|
|
174
|
+
// 前 600s / 15s = 40 轮;剩下 1200s / 60s = 20 轮 → 60 轮封顶
|
|
175
|
+
const total = 600_000 / 15_000 + (1_800_000 - 600_000) / 60_000
|
|
176
|
+
assert.equal(total, 60)
|
|
177
|
+
assert.ok(total <= 60, '长窗口不许把日志刷成上百轮')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// ========== 失败归类(放弃时汇总"卡在哪") ==========
|
|
181
|
+
|
|
182
|
+
test('classifyInstallError: E404 / ETARGET → registry 还没就绪', () => {
|
|
183
|
+
assert.equal(classifyInstallError('npm error code E404\nnpm error 404 Not Found - GET https://...'), 'E404')
|
|
184
|
+
assert.equal(classifyInstallError('npm error code ETARGET\nnpm error No matching version found'), 'ETARGET')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('classifyInstallError: EPERM / EBUSY → 文件被占(和"还没就绪"完全不同的处置)', () => {
|
|
188
|
+
assert.equal(classifyInstallError('npm warn cleanup Failed to remove some directories: EPERM'), 'EPERM')
|
|
189
|
+
assert.equal(classifyInstallError('npm error EBUSY: resource busy or locked'), 'EPERM')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('classifyInstallError: 网络类与空输入不炸,各有短码', () => {
|
|
193
|
+
assert.equal(classifyInstallError('npm error code ENOTFOUND registry.npmjs.org'), 'ENOTFOUND')
|
|
194
|
+
assert.equal(classifyInstallError('npm error code ETIMEDOUT'), 'ETIMEDOUT')
|
|
195
|
+
assert.equal(classifyInstallError('npm error ECONNRESET'), 'ENETUNREACH')
|
|
196
|
+
assert.equal(classifyInstallError(''), '其他')
|
|
197
|
+
assert.equal(classifyInstallError(undefined), '其他')
|
|
198
|
+
assert.equal(classifyInstallError('some unknown failure'), '其他')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('classifyInstallError: 两类同时出现时优先报文件占用(它是唯一要"动手"的那类)', () => {
|
|
202
|
+
const mixed = 'npm error code E404\nnpm error EPERM: operation not permitted'
|
|
203
|
+
assert.equal(classifyInstallError(mixed), 'EPERM')
|
|
204
|
+
})
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./rolldown-runtime-C0FnF6B9.js";import{$ as e,C as t,Cn as n,Dn as r,En as i,Fr as a,Jn as o,Jr as s,K as c,Kn as l,Mn as u,Pn as d,Qn as f,Rn as p,S as m,Tn as h,Un as g,Xn as _,br as v,cr as y,ei as b,pr as x,q as S,w as C,wn as w,yn as T}from"./element-plus-AaVXaSqv.js";import{n as E,t as D}from"./_plugin-vue_export-helper-o8uPiAjs.js";import{t as O}from"./SvgIcon-z4ExzvGR.js";import{t as k}from"./toolsStore-DgqU92bI.js";import{a as A,m as j}from"./index-D_Pwvey4.js";import{c as M,d as N,l as P,u as F}from"./agentConversations-BmOPFH_t.js";p(),x();function I(e,t=680){let r=v(0),i=null;function a(){let t=e.value;return t?(r.value=t.clientWidth,typeof ResizeObserver>`u`||(i=new ResizeObserver(e=>{let t=e[0]?.contentRect?.width;typeof t==`number`&&(r.value=t)}),i.observe(t),!0)):!1}return o(()=>{a()||g(()=>a())}),l(()=>{i?.disconnect(),i=null}),{width:r,narrow:n(()=>r.value>0&&r.value<t)}}p(),x();var L=d({__name:`AgentEngineSelector`,props:{engine:{type:String,required:!0},locked:{type:Boolean,required:!1}},emits:[`select`],setup(r,{expose:i,emit:a}){i();let o=r,s=a,l=k(),u=n(()=>F(o.engine)),d=n(()=>({gai:!0,claude:l.claudeAvailable,opencode:l.opencodeAvailable,codex:l.codexAvailable})),f=v(!1),p=v(null);function h(e){if(!o.locked){if(!d.value[e]){if(!N(e))return;p.value=e,f.value=!0;return}s(`select`,e)}}let g={props:o,emit:s,toolsStore:l,engineName:u,engineAvailability:d,installDialogVisible:f,installTool:p,onCommand:h,get $t(){return E},get ElIcon(){return c},get ElDropdown(){return m},get ElDropdownMenu(){return C},get ElDropdownItem(){return t},get ArrowDown(){return S},get Check(){return e},get AGENT_ENGINE_OPTIONS(){return M},get BUILTIN_ENGINE_ICON(){return P},get isExternalAgentEngine(){return N},SvgIcon:O,TaskExecutorIcon:A,ToolInstallDialog:j};return Object.defineProperty(g,"__isScriptSetup",{enumerable:!1,value:!0}),g}});p(),a();var R={class:`agent-engine`},z=[`disabled`,`title`,`aria-label`],B={class:`agent-engine__name`},V={class:`agent-engine__item`},H={class:`agent-engine__item-name`},U={key:3,class:`agent-engine__item-missing`};function W(e,t,n,a,o,c){return _(),r(T,null,[i(` 单根包一层:父级传下来的 class(Tab 行的 margin-left:auto、面板的 flex:none)
|
|
2
|
-
才能可靠落到这里 —— 多根组件会丢掉 fallthrough class。 `),w(`div`,R,[u(a.ElDropdown,{trigger:`click`,onCommand:a.onCommand},{dropdown:y(()=>[u(a.ElDropdownMenu,null,{default:y(()=>[(_(!0),r(T,null,f(a.AGENT_ENGINE_OPTIONS,e=>(_(),h(a.ElDropdownItem,{key:e.id,command:e.id},{default:y(()=>[w(`span`,V,[a.isExternalAgentEngine(e.id)?(_(),h(a.TaskExecutorIcon,{key:0,executor:e.id,class:`agent-engine__item-icon`},null,8,[`executor`])):(_(),h(a.SvgIcon,{key:1,"icon-class":a.BUILTIN_ENGINE_ICON,class:`agent-engine__item-icon`},null,8,[`icon-class`])),w(`span`,H,b(e.name),1),n.engine===e.id?(_(),h(a.ElIcon,{key:2,class:`agent-engine__item-check`},{default:y(()=>[u(a.Check)]),_:1})):a.engineAvailability[e.id]?i(`v-if`,!0):(_(),r(`span`,U,b(a.$t(`@42BB9:未安装`)),1))])]),_:2},1032,[`command`]))),128))]),_:1})]),default:y(()=>[w(`button`,{type:`button`,class:s([`agent-engine__btn`,{"is-locked":n.locked}]),disabled:n.locked,title:n.locked?a.$t(`@AGENT:本会话的引擎已锁定;要换引擎请新建会话`):a.$t(`@AGENT:新建会话使用的引擎`),"aria-label":a.$t(`@AGENT:智能体引擎`)},[a.isExternalAgentEngine(n.engine)?(_(),h(a.TaskExecutorIcon,{key:0,executor:n.engine,class:`agent-engine__icon`},null,8,[`executor`])):(_(),h(a.SvgIcon,{key:1,"icon-class":a.BUILTIN_ENGINE_ICON,class:`agent-engine__icon`},null,8,[`icon-class`])),w(`span`,B,b(a.engineName),1),u(a.ElIcon,{class:`agent-engine__caret`},{default:y(()=>[u(a.ArrowDown)]),_:1})],10,z)]),_:1}),u(a.ToolInstallDialog,{modelValue:a.installDialogVisible,"onUpdate:modelValue":t[0]||=e=>a.installDialogVisible=e,tool:a.installTool},null,8,[`modelValue`,`tool`])])],2112)}var G=D(L,[[`render`,W],[`__scopeId`,`data-v-c67a4d14`],[`__file`,`C:/workspace/github_workspace/xz333221/zen-gitsync/src/ui/client/src/components/AgentEngineSelector.vue`]]);export{I as n,G as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.agent-engine[data-v-c67a4d14]{vertical-align:middle}.agent-engine__btn[data-v-c67a4d14]{border:1px solid var(--border-color);border-radius:var(--radius-base);color:var(--text-secondary);font-size:var(--font-size-sm);cursor:pointer;transition:border-color var(--transition-fast) ease, color var(--transition-fast) ease;background:0 0;align-items:center;gap:6px;padding:4px 8px;font-family:inherit;line-height:1.5;display:inline-flex}.agent-engine__btn[data-v-c67a4d14]:hover:not(:disabled){border-color:var(--color-primary);color:var(--text-primary)}.agent-engine__btn[data-v-c67a4d14]:disabled{cursor:not-allowed;color:var(--text-tertiary)}.agent-engine__icon[data-v-c67a4d14],.agent-engine__item-icon[data-v-c67a4d14]{flex:none;font-size:14px}.agent-engine__caret[data-v-c67a4d14]{color:var(--text-tertiary);font-size:12px}.agent-engine__item[data-v-c67a4d14]{align-items:center;gap:8px;min-width:148px;display:inline-flex}.agent-engine__item-name[data-v-c67a4d14]{flex:1}.agent-engine__item-check[data-v-c67a4d14]{color:var(--color-primary);font-size:13px}.agent-engine__item-missing[data-v-c67a4d14]{font-size:var(--font-size-sm);color:var(--text-tertiary)}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import"./rolldown-runtime-C0FnF6B9.js";import{$ as e,A as t,Bt as n,Cn as r,Ct as i,Dn as a,Dt as o,En as s,Fr as c,H as l,Jr as u,K as d,Mn as f,Ot as p,Pn as m,Q as h,Qn as g,Rn as _,Tn as v,Un as y,Ut as ee,Xn as b,Xr as x,a as S,at as C,br as w,cr as T,ei as E,ft as D,hn as O,i as k,j as A,jn as j,ln as M,lr as N,o as P,or as F,pr as I,qt as te,rn as L,st as R,tr as z,vr as B,wn as V,yn as H}from"./element-plus-AaVXaSqv.js";import{r as ne,t as re}from"./vendor-CPpc-p6F.js";import{n as U,t as W}from"./_plugin-vue_export-helper-o8uPiAjs.js";import{t as ie}from"./configStore-DrMJDegh.js";import{t as G}from"./CommonDialog-CZ5McwHT.js";import{a as ae,i as oe,n as se,o as ce,r as le,s as ue,t as de}from"./agentConversations-BmOPFH_t.js";import{n as fe,t as pe}from"./AgentEngineSelector-DIn2efUS.js";_(),I();var K=m({__name:`MarketplacePanel`,props:{type:{type:String,required:!0}},setup(t,{expose:i}){i();let a=t,s=ie(),c=w([]),l=w([]),u=w(!1),d=w(``),f=w(``),p=w([]),m=w(`project`),h=w(new Set),g=w(new Set),_=r(()=>s.currentDirectory||``),v=null;async function y(){if(!u.value){u.value=!0;try{let e=new URLSearchParams({type:a.type,cwd:_.value});f.value&&e.set(`q`,f.value),p.value.length&&e.set(`sources`,p.value.join(`,`));let t=await(await fetch(`/api/agent/marketplace/catalog?${e}`)).json();t?.success?(c.value=t.groups||[],l.value=t.installed||[]):S.error(t?.error||U(`@MKT:加载失败`))}catch{S.error(U(`@MKT:加载失败`))}finally{u.value=!1}}}F(d,e=>{v&&clearTimeout(v),v=setTimeout(()=>{f.value=e.trim(),y()},400)}),F(p,()=>y()),F(()=>a.type,()=>{p.value=[],f.value=``,d.value=``,y()}),F(_,()=>y()),y();let b=r(()=>{let e=_.value?_.value.split(/[\\/]/).filter(Boolean).pop():``;return m.value===`project`?e||U(`@MKT:当前项目`):U(`@MKT:g ai 智能体`)});function x(e){return e.stars?`${C(e.stars)} ★`:e.downloads?`${C(e.downloads)} ${U(`@MKT:周下载`)}`:e.uses?`${C(e.uses)} ${U(`@MKT:次使用`)}`:``}function C(e){return e>=1e3?`${(e/1e3).toFixed(e>=1e4?0:1)}k`:String(e)}function T(e){return`${e.id}`}function E(e){return h.value.has(T(e))}let O=r(()=>{let e={project:[],global:[]};for(let t of l.value)(e[t.target]||=[]).push(t);return e}),A=B({visible:!1,item:null,env:{},args:``});function j(e){return a.type===`mcp`&&(!!e.envKeys?.length||!!e.args?.length)}function M(e){A.item=e,A.env=Object.fromEntries((e.envKeys||[]).map(e=>[e,``])),A.args=(e.args||[]).join(` `),A.visible=!0}function N(){let e=A.item;if(A.visible=!1,!e)return;let t=Object.fromEntries(Object.entries(A.env).filter(([,e])=>e.trim()!==``)),n=A.args.trim()?A.args.trim().match(/(?:[^\s"]+|"[^"]*")+/g)?.map(e=>e.replace(/^"|"$/g,``))??void 0:void 0;I(e,{env:t,...n?{args:n}:{}})}async function P(e){if(!e.installed||await z(e)){if(j(e)){M(e);return}await I(e,{})}}async function I(e,t){let n=T(e);h.value.add(n);try{let n=await fetch(`/api/agent/marketplace/install`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:a.type,target:m.value,cwd:_.value,item:{...e,...t}})}),r=await n.json().catch(()=>null);if(!n.ok||!r?.success){S.error(r?.error||U(`@MKT:安装失败`));return}r.item?.warning&&S.warning(r.item.warning),r.item?.envKeys?.length?S.warning(`${U(`@MKT:还缺环境变量`)}: ${r.item.envKeys.join(`, `)}`):S.success(U(`@MKT:安装成功`)),await y()}catch{S.error(U(`@MKT:安装失败`))}finally{h.value.delete(n)}}async function z(e){try{return await k.confirm(`${e.name} ${U(`@MKT:已安装,要重新安装吗?`)}`,U(`@MKT:重新安装`),{confirmButtonText:U(`@MKT:重装`),cancelButtonText:U(`@MKT:取消`),type:`warning`}),!0}catch{return!1}}async function V(e){try{await k.confirm(`${U(`@MKT:确定卸载`)} ${e.name}?`,U(`@MKT:卸载`),{confirmButtonText:U(`@MKT:卸载`),cancelButtonText:U(`@MKT:取消`),type:`warning`})}catch{return}let t=`${e.target}:${e.id}`;g.value.add(t);try{let t=new URLSearchParams({target:e.target,cwd:_.value}),n=await fetch(`/api/agent/marketplace/item/${a.type}/${encodeURIComponent(e.id)}?${t}`,{method:`DELETE`}),r=await n.json().catch(()=>null);if(!n.ok||!r?.success){S.error(r?.error||U(`@MKT:卸载失败`));return}S.success(U(`@MKT:已卸载`)),await y()}catch{S.error(U(`@MKT:卸载失败`))}finally{g.value.delete(t)}}let H={props:a,configStore:s,groups:c,installed:l,loading:u,searchInput:d,query:f,selectedSources:p,installTarget:m,installing:h,removing:g,cwd:_,get searchTimer(){return v},set searchTimer(e){v=e},loadCatalog:y,targetLabel:b,metricText:x,formatCount:C,cardKey:T,isBusy:E,installedByTarget:O,configDialog:A,needsConfig:j,openConfigDialog:M,confirmConfig:N,onInstall:P,doInstall:I,confirmReinstall:z,onRemove:V,get $t(){return U},get Search(){return ee},get Refresh(){return n},get Delete(){return R},get Link(){return o},get Check(){return e},get Star(){return te},get Download(){return D},get Warning(){return L},CommonDialog:G};return Object.defineProperty(H,"__isScriptSetup",{enumerable:!1,value:!0}),H}});_(),M(),c();var q={class:`marketplace-panel`},J={class:`mp-toolbar`},Y={class:`mp-search`},X=[`placeholder`],me={class:`mp-toolbar-sub`},Z={class:`mp-target-hint`},Q={class:`mp-source-filter`},he=[`onClick`],$={class:`mp-body`},ge={class:`mp-group-header`},_e={class:`mp-group-title`},ve={key:0,class:`mp-group-count`},ye={key:1,class:`mp-group-error`},be=[`href`,`title`],xe={key:0,class:`mp-grid`},Se={class:`mp-card-head`},Ce=[`title`],we={key:0,class:`mp-card-metric`},Te=[`title`],Ee={class:`mp-card-meta`},De=[`title`],Oe=[`title`],ke={key:2,class:`mp-tag remote`},Ae={class:`mp-card-foot`},je=[`href`],Me={key:1,class:`mp-installed-chip`},Ne=[`disabled`,`onClick`],Pe={key:3,class:`mp-not-installable`},Fe={key:1,class:`mp-empty`},Ie={key:0,class:`mp-group installed`},Le={class:`mp-group-header`},Re={class:`mp-group-title`},ze={class:`mp-group-count`},Be={key:0,class:`mp-installed-block`},Ve={class:`mp-installed-target`},He={class:`mp-installed-list`},Ue={class:`mp-installed-name`},We={key:0,class:`mp-meta-pkg`},Ge={key:1,class:`mp-tag warn`},Ke=[`disabled`,`onClick`],qe={key:1,class:`mp-empty all`},Je={key:0,class:`mkt-form`},Ye={class:`mkt-form-name`},Xe=[`onUpdate:modelValue`,`placeholder`],Ze={key:0,class:`mkt-field`},Qe={class:`mkt-hint`};function $e(e,n,r,i,o,c){let p=d,m=t,h=A,_=l,y=P;return b(),a(`div`,q,[s(` ── 工具栏 ───────────────────────────────── `),V(`div`,J,[V(`div`,Y,[f(p,{class:`mp-search-icon`},{default:T(()=>[f(i.Search)]),_:1}),N(V(`input`,{"onUpdate:modelValue":n[0]||=e=>i.searchInput=e,class:`mp-search-input`,type:`text`,placeholder:r.type===`skill`?i.$t(`@MKT:搜索 Skill...`):i.$t(`@MKT:搜索 MCP 服务...`)},null,8,X),[[O,i.searchInput]])]),f(h,{modelValue:i.installTarget,"onUpdate:modelValue":n[1]||=e=>i.installTarget=e,size:`small`,class:`mp-target`},{default:T(()=>[f(m,{value:`project`},{default:T(()=>[j(E(i.$t(`@MKT:当前项目`)),1)]),_:1}),f(m,{value:`global`},{default:T(()=>[j(E(i.$t(`@MKT:g ai 智能体`)),1)]),_:1})]),_:1},8,[`modelValue`]),f(_,{content:i.$t(`@MKT:刷新`),placement:`top`,"show-after":300},{default:T(()=>[V(`button`,{class:u([`mp-refresh`,{spinning:i.loading}]),onClick:i.loadCatalog},[f(p,null,{default:T(()=>[f(i.Refresh)]),_:1})],2)]),_:1},8,[`content`])]),V(`div`,me,[V(`span`,Z,[j(E(i.$t(`@MKT:安装到`))+` `,1),V(`b`,null,E(i.targetLabel),1)]),V(`div`,Q,[(b(!0),a(H,null,g(i.groups,e=>(b(),a(`button`,{key:e.id,class:u([`mp-chip`,{active:i.selectedSources.includes(e.id)}]),onClick:()=>{let t=i.selectedSources.indexOf(e.id);t>=0?i.selectedSources.splice(t,1):i.selectedSources.push(e.id)}},E(e.label),11,he))),128)),i.selectedSources.length?(b(),a(`button`,{key:0,class:`mp-chip clear`,onClick:n[2]||=e=>i.selectedSources=[]},E(i.$t(`@MKT:全部来源`)),1)):s(`v-if`,!0)])]),s(` ── 内容 ─────────────────────────────────── `),N((b(),a(`div`,$,[(b(!0),a(H,null,g(i.groups,e=>(b(),a(`section`,{key:e.id,class:`mp-group`},[V(`header`,ge,[V(`h3`,_e,E(e.label),1),e.status===`ok`?(b(),a(`span`,ve,E(e.items.length),1)):(b(),a(`span`,ye,[f(p,null,{default:T(()=>[f(i.Warning)]),_:1}),j(` `+E(e.error||i.$t(`@MKT:来源不可用`)),1)])),e.homepage?(b(),a(`a`,{key:2,class:`mp-group-link`,href:e.homepage,target:`_blank`,rel:`noopener noreferrer`,title:e.homepage},[f(p,null,{default:T(()=>[f(i.Link)]),_:1})],8,be)):s(`v-if`,!0)]),e.status===`ok`&&e.items.length?(b(),a(`div`,xe,[(b(!0),a(H,null,g(e.items,e=>(b(),a(`article`,{key:i.cardKey(e),class:`mp-card`},[V(`div`,Se,[V(`span`,{class:`mp-card-name`,title:e.name},E(e.name),9,Ce),i.metricText(e)?(b(),a(`span`,we,[f(p,null,{default:T(()=>[e.stars?(b(),v(i.Star,{key:0})):(b(),v(i.Download,{key:1}))]),_:2},1024),j(` `+E(i.metricText(e)),1)])):s(`v-if`,!0)]),V(`p`,{class:`mp-card-desc`,title:e.description},E(e.description||i.$t(`@MKT:暂无描述`)),9,Te),V(`div`,Ee,[e.package?(b(),a(`code`,{key:0,class:`mp-meta-pkg`,title:e.package},E(e.package),9,De)):e.repository?(b(),a(`span`,{key:1,class:`mp-meta-repo`,title:e.repository+(e.subpath?` · `+e.subpath:``)},[j(E(e.repository),1),e.subpath?(b(),a(H,{key:0},[j(` · `+E(e.subpath),1)],64)):s(`v-if`,!0)],8,Oe)):s(`v-if`,!0),e.transport===`remote`?(b(),a(`span`,ke,E(i.$t(`@MKT:远程服务`)),1)):s(`v-if`,!0),(b(!0),a(H,null,g((e.tags||[]).slice(0,2),e=>(b(),a(`span`,{key:e,class:`mp-tag`},E(e),1))),128))]),V(`div`,Ae,[e.upstream?(b(),a(`a`,{key:0,class:`mp-card-link`,href:e.upstream,target:`_blank`,rel:`noopener noreferrer`},E(i.$t(`@MKT:查看来源`)),9,je)):s(`v-if`,!0),n[6]||=V(`span`,{class:`mp-foot-spacer`},null,-1),e.installed?(b(),a(`span`,Me,[f(p,null,{default:T(()=>[f(i.Check)]),_:1}),j(E(i.$t(`@MKT:已安装`)),1)])):e.installable===!1?(b(),a(`span`,Pe,E(i.$t(`@MKT:暂不支持一键安装`)),1)):(b(),a(`button`,{key:2,class:`mp-install-btn`,disabled:i.isBusy(e),onClick:t=>i.onInstall(e)},E(i.isBusy(e)?i.$t(`@MKT:安装中...`):i.$t(`@MKT:安装`)),9,Ne))])]))),128))])):e.status===`ok`?(b(),a(`div`,Fe,E(i.$t(`@MKT:该来源暂无结果`)),1)):s(`v-if`,!0)]))),128)),s(` ── 已安装 ─────────────────────────────── `),i.installed.length?(b(),a(`section`,Ie,[V(`header`,Le,[V(`h3`,Re,E(i.$t(`@MKT:已安装`)),1),V(`span`,ze,E(i.installed.length),1)]),(b(),a(H,null,g([`project`,`global`],e=>V(`div`,{key:e},[i.installedByTarget[e]?.length?(b(),a(`div`,Be,[V(`div`,Ve,E(e===`project`?i.$t(`@MKT:当前项目`):i.$t(`@MKT:g ai 智能体`)),1),V(`div`,He,[(b(!0),a(H,null,g(i.installedByTarget[e],t=>(b(),a(`div`,{key:e+t.id,class:`mp-installed-row`},[V(`span`,Ue,E(t.name),1),t.package?(b(),a(`code`,We,E(t.package),1)):s(`v-if`,!0),t.requiredEnv?.length?(b(),a(`span`,Ge,E(i.$t(`@MKT:待配置`))+`: `+E(t.requiredEnv.join(`, `)),1)):s(`v-if`,!0),n[7]||=V(`span`,{class:`mp-foot-spacer`},null,-1),V(`button`,{class:`mp-remove-btn`,disabled:i.removing.has(e+t.id),onClick:e=>i.onRemove(t)},[f(p,null,{default:T(()=>[f(i.Delete)]),_:1}),j(` `+E(i.removing.has(e+t.id)?i.$t(`@MKT:卸载中...`):i.$t(`@MKT:卸载`)),1)],8,Ke)]))),128))])])):s(`v-if`,!0)])),64))])):s(`v-if`,!0),!i.loading&&!i.groups.length?(b(),a(`div`,qe,E(i.$t(`@MKT:加载失败`)),1)):s(`v-if`,!0)])),[[y,i.loading]]),s(` ── MCP 安装前配置 ───────────────────────── `),f(i.CommonDialog,{modelValue:i.configDialog.visible,"onUpdate:modelValue":n[5]||=e=>i.configDialog.visible=e,title:i.$t(`@MKT:配置 MCP`),width:`460px`,type:`flex`,"close-on-click-modal":!1,"append-to-body":``},{footer:T(()=>[V(`button`,{class:`mkt-cancel`,onClick:n[4]||=e=>i.configDialog.visible=!1},E(i.$t(`@MKT:取消`)),1),V(`button`,{class:`mkt-confirm`,onClick:i.confirmConfig},E(i.$t(`@MKT:安装`)),1)]),default:T(()=>[i.configDialog.item?(b(),a(`div`,Je,[V(`p`,Ye,E(i.configDialog.item.name),1),(b(!0),a(H,null,g(Object.keys(i.configDialog.env),e=>(b(),a(`div`,{key:e,class:`mkt-field`},[V(`label`,null,E(e),1),N(V(`input`,{"onUpdate:modelValue":t=>i.configDialog.env[e]=t,type:`text`,autocomplete:`off`,placeholder:i.$t(`@MKT:环境变量值`),spellcheck:`false`},null,8,Xe),[[O,i.configDialog.env[e]]])]))),128)),i.configDialog.item.args?.length?(b(),a(`div`,Ze,[V(`label`,null,E(i.$t(`@MKT:启动参数`)),1),N(V(`input`,{"onUpdate:modelValue":n[3]||=e=>i.configDialog.args=e,type:`text`,autocomplete:`off`,spellcheck:`false`},null,512),[[O,i.configDialog.args]]),V(`p`,Qe,E(i.$t(`@MKT:参数提示`)),1)])):s(`v-if`,!0)])):s(`v-if`,!0)]),_:1},8,[`modelValue`,`title`])])}var et=W(K,[[`render`,$e],[`__scopeId`,`data-v-e9f9e851`],[`__file`,`C:/workspace/github_workspace/xz333221/zen-gitsync/src/ui/client/src/components/MarketplacePanel.vue`]]);_(),I();var tt=m({__name:`AgentView`,setup(e,{expose:t}){t();let n=ie(),a=w(null),{narrow:o}=fe(a),s=w(`chat`),c=w(`chat`),l=r(()=>c.value===`mcp`?`mcp`:`skill`),u=r(()=>[{id:`chat`,label:U(`@AGENT:对话`),icon:h},{id:`skill`,label:U(`@AGENT:Skill 广场`),icon:i},{id:`mcp`,label:U(`@AGENT:MCP 广场`),icon:C}]),{sessions:f,sessionsLoading:m,currentSessionId:g,messages:_,isStreaming:v,sessionLoading:ee,loadSessions:b,loadSession:x,deleteSession:S,renameSession:T,newSession:E,sendMessage:D,pendingQuestion:O,answeringQuestion:k,answerQuestion:A,isSessionGenerating:j,currentEngine:M,pendingEngine:N,isEngineLocked:P,pickEngine:I,stop:te}=ue(),L=r(()=>P.value?M.value:N.value);function R(e){P.value||I(e)}let z=r(()=>{let e=n.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),B=r(()=>ae(f.value,j)),V=le(),H=r(()=>B.value.find(e=>e.id===g.value)?.title||U(`@AGENT:对话`)),W=r(()=>[{id:`p1`,label:U(`@AGENT:查看项目结构`),prompt:U(`@AGENT:prompt_p1`)},{id:`p2`,label:U(`@AGENT:分析代码质量`),prompt:U(`@AGENT:prompt_p2`)},{id:`p3`,label:U(`@AGENT:帮我写测试`),prompt:U(`@AGENT:prompt_p3`)},{id:`p4`,label:U(`@AGENT:Git 状态检查`),prompt:U(`@AGENT:prompt_p4`)},{id:`p5`,label:U(`@AGENT:帮我启动项目`),prompt:U(`@AGENT:prompt_p5`)}]);async function G(e){await D(e.text,e.files),await y(),Y()}async function K(e){await D(e.prompt),await y(),Y()}let q=oe(),J=w(null);function Y(e=!0){J.value?.scrollToBottom(e)}F(()=>_.value.length,()=>{y(()=>Y(!1))});function X(e){x(e),s.value=`chat`}function me(){E(),s.value=`chat`}let Z=w(280),Q=w(!1);function he(e){Q.value=!0;let t=e.clientX,n=Z.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Z.value=i},i=()=>{Q.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}F(o,e=>{e||(s.value=`chat`)}),F(()=>[n.currentDirectory,v.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await b(),g.value&&!v.value&&!f.value.some(e=>e.sessionId===g.value)&&E()},{immediate:!0});let $={configStore:n,rootRef:a,narrow:o,chatPage:s,activeTab:c,marketplaceType:l,tabs:u,sessions:f,sessionsLoading:m,currentSessionId:g,messages:_,isStreaming:v,sessionLoading:ee,loadSessions:b,loadSession:x,deleteSession:S,renameSession:T,newSession:E,sendMessage:D,pendingQuestion:O,answeringQuestion:k,answerQuestion:A,isSessionGenerating:j,currentEngine:M,pendingEngine:N,isEngineLocked:P,pickEngine:I,stop:te,displayEngine:L,handleEngineSelect:R,chatTheme:z,conversationItems:B,conversationLabels:V,currentSessionTitle:H,presetQuestions:W,onSend:G,onSelectPreset:K,questionLabels:q,chatContainerRef:J,scrollToBottom:Y,selectSession:X,handleNewSession:me,sidebarWidth:Z,isResizing:Q,startResize:he,get $t(){return U},get ElIcon(){return d},get Loading(){return p},get ChatContainer(){return re},get ConversationList(){return ne},get AGENT_UPLOAD_ACCEPT(){return ce},get AGENT_ASSISTANT_NAME(){return se},get AGENT_ASSISTANT_AVATAR(){return de},MarketplacePanel:et,AgentEngineSelector:pe};return Object.defineProperty($,"__isScriptSetup",{enumerable:!1,value:!0}),$}});_(),c();var nt={ref:`rootRef`,class:`agent-view`},rt={class:`agent-chat-area`},it={class:`agent-tabs`,role:`tablist`},at=[`aria-selected`,`onClick`],ot={class:`agent-tab-body`},st={key:0,class:`agent-chat-pane`},ct={key:0,class:`agent-list-page`},lt={key:0,class:`agent-page-bar`},ut=[`title`,`aria-label`],dt={class:`agent-page-title`},ft={class:`agent-chat-host`},pt={key:0,class:`chat-loading`};function mt(e,t,n,r,i,o){return b(),a(`div`,nt,[s(` ═══ 宽屏:左侧会话列表(只在「对话」Tab 显示)═══
|
|
2
|
-
窄屏时这一整块不渲染,会话列表改成 body 里的一个整页(见 .agent-list-page) `),r.activeTab===`chat`&&!r.narrow?(b(),a(H,{key:0},[V(`aside`,{class:`agent-sidebar`,style:x({width:r.sidebarWidth+`px`})},[s(` 会话列表:UI 与交互都在组件库的 ConversationList 里 `),f(r.ConversationList,{class:`agent-conversations`,items:r.conversationItems,"active-id":r.currentSessionId,loading:r.sessionsLoading,labels:r.conversationLabels,onSelect:r.selectSession,onNew:r.handleNewSession,onRename:r.renameSession,onDelete:r.deleteSession},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])],4),s(` 拖拽分隔条 `),V(`div`,{class:u([`sidebar-resizer`,{active:r.isResizing}]),onMousedown:r.startResize},null,34)],64)):s(`v-if`,!0),s(` ═══ 右侧:Tab 栏 + 内容区 ═══ `),V(`main`,rt,[V(`nav`,it,[(b(!0),a(H,null,g(r.tabs,e=>(b(),a(`button`,{key:e.id,type:`button`,class:u([`agent-tab`,{active:r.activeTab===e.id}]),role:`tab`,"aria-selected":r.activeTab===e.id,onClick:t=>r.activeTab=e.id},[f(r.ElIcon,null,{default:T(()=>[(b(),v(z(e.icon)))]),_:2},1024),V(`span`,null,E(e.label),1)],10,at))),128)),s(` ═══ 引擎选择器 ═══
|
|
3
|
-
与工作台执行器下拉同一套交互(点击展开、未安装标出来、点了直接开安装弹窗)。
|
|
4
|
-
下拉本体收口在 AgentEngineSelector,文件空间 g ai 面板复用同一份实现。
|
|
5
|
-
会话一旦落盘就置灰:引擎在会话建立时锁定(三家续聊标识互不通用),要换请新建。 `),r.activeTab===`chat`?(b(),v(r.AgentEngineSelector,{key:0,class:`agent-engine`,engine:r.displayEngine,locked:r.isEngineLocked,onSelect:r.handleEngineSelect},null,8,[`engine`,`locked`])):s(`v-if`,!0)]),V(`div`,ot,[s(` ── 对话 ── `),r.activeTab===`chat`?(b(),a(`div`,st,[s(` 窄屏 · 会话列表页:列表独占整页 `),r.narrow&&r.chatPage===`list`?(b(),a(`div`,ct,[f(r.ConversationList,{class:`agent-conversations`,items:r.conversationItems,"active-id":r.currentSessionId,loading:r.sessionsLoading,labels:r.conversationLabels,onSelect:r.selectSession,onNew:r.handleNewSession,onRename:r.renameSession,onDelete:r.deleteSession},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])])):(b(),a(H,{key:1},[s(` 对话页(窄屏时顶上多一条返回条;宽屏左侧就摆着列表,不需要) `),r.narrow?(b(),a(`div`,lt,[V(`button`,{type:`button`,class:`agent-page-back`,title:r.$t(`@AGENT:返回会话列表`),"aria-label":r.$t(`@AGENT:返回会话列表`),onClick:t[0]||=e=>r.chatPage=`list`},[...t[1]||=[V(`svg`,{viewBox:`0 0 24 24`,width:`14`,height:`14`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`},[V(`line`,{x1:`19`,y1:`12`,x2:`5`,y2:`12`}),V(`polyline`,{points:`12 19 5 12 12 5`})],-1)]],8,ut),V(`span`,dt,E(r.currentSessionTitle),1)])):s(`v-if`,!0),s(`
|
|
6
|
-
对话宿主:单独开一层裁剪边界。
|
|
7
|
-
欢迎页在矮/窄窗口里会比可用高度还高,而它是垂直居中的 —— 溢出的部分
|
|
8
|
-
会**向上**跑出 .acu-chat,盖到刚加的返回条上(把返回条的点击也一起吃掉,
|
|
9
|
-
e2e 实测就是这个症状)。宿主的 overflow:hidden 把它裁在自己这一格里。
|
|
10
|
-
`),V(`div`,ft,[s(` 加载中 `),r.sessionLoading?(b(),a(`div`,pt,[f(r.ElIcon,{class:`is-loading`,size:32},{default:T(()=>[f(r.Loading)]),_:1}),V(`span`,null,E(r.$t(`@AGENT:加载会话中...`)),1)])):(b(),a(H,{key:1},[s(` ChatContainer `),f(r.ChatContainer,{ref:`chatContainerRef`,messages:r.messages,"preset-questions":r.presetQuestions,"welcome-title":r.$t(`@AGENT:智能体助手`),"welcome-description":r.$t(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":r.AGENT_ASSISTANT_NAME,"assistant-avatar":r.AGENT_ASSISTANT_AVATAR,theme:r.chatTheme,disabled:r.isStreaming,generating:r.isStreaming,"upload-config":{accept:r.AGENT_UPLOAD_ACCEPT},placeholder:r.isStreaming?r.$t(`@AGENT:正在生成中...`):r.$t(`@AGENT:输入消息,Enter 发送`),question:r.pendingQuestion,"question-submitting":r.answeringQuestion,"question-labels":r.questionLabels,onSend:r.onSend,onSelect:r.onSelectPreset,onStop:r.stop,onAnswer:r.answerQuestion},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`assistant-name`,`assistant-avatar`,`theme`,`disabled`,`generating`,`upload-config`,`placeholder`,`question`,`question-submitting`,`question-labels`,`onStop`,`onAnswer`])],2112))])],64))])):(b(),a(H,{key:1},[s(` ── Skill 广场 / MCP 广场 ── `),f(r.MarketplacePanel,{type:r.marketplaceType},null,8,[`type`])],2112))])])],512)}var ht=W(tt,[[`render`,mt],[`__scopeId`,`data-v-bd681ddd`],[`__file`,`C:/workspace/github_workspace/xz333221/zen-gitsync/src/ui/client/src/views/AgentView.vue`]]);export{ht as default};
|