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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// aiContext/render.js 单元测试(纯函数)。
|
|
2
|
+
//
|
|
3
|
+
// 钉住两条核心契约:
|
|
4
|
+
// 1. 注入给模型的块里**只有摘要,板块正文一行都不能进** —— 破了不会报错,
|
|
5
|
+
// 只会静默烧 token 并把用户真正要办的那句话淹掉(envContext.js 头注释记的是同一个坑)。
|
|
6
|
+
// 2. **不论快照有没有生成,块里都必须有"七个板块各自怎么查实时值"** ——
|
|
7
|
+
// 快照天生会旧,"实时"这件事最终靠这句话兜底,所以它不许被截断、不许为空。
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {
|
|
11
|
+
AI_CONTEXT_SECTIONS,
|
|
12
|
+
MAX_CONTEXT_BLOCK_CHARS,
|
|
13
|
+
MAX_SUMMARY_CHARS,
|
|
14
|
+
buildContextBlock,
|
|
15
|
+
formatStamp,
|
|
16
|
+
joinSnapshotPath,
|
|
17
|
+
oneLine,
|
|
18
|
+
renderIndexFile,
|
|
19
|
+
renderSectionFile,
|
|
20
|
+
sectionForceTtl,
|
|
21
|
+
sectionQueryHint,
|
|
22
|
+
sectionTtl,
|
|
23
|
+
truncate,
|
|
24
|
+
} from './render.js';
|
|
25
|
+
|
|
26
|
+
const okResult = (over = {}) => ({
|
|
27
|
+
id: 'git',
|
|
28
|
+
title: '当前项目 Git 状态',
|
|
29
|
+
file: 'git-current.md',
|
|
30
|
+
ok: true,
|
|
31
|
+
summary: '分支 main,工作区干净',
|
|
32
|
+
lines: ['- 分支: main'],
|
|
33
|
+
error: '',
|
|
34
|
+
collectedAt: '2026-09-28 10:15:32',
|
|
35
|
+
...over,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('板块清单:文件带 .md 后缀且 id 唯一(文件名就是模型要读的路径)', () => {
|
|
39
|
+
const ids = AI_CONTEXT_SECTIONS.map(s => s.id);
|
|
40
|
+
assert.equal(new Set(ids).size, ids.length, 'id 必须唯一');
|
|
41
|
+
for (const s of AI_CONTEXT_SECTIONS) {
|
|
42
|
+
assert.match(s.file, /^[a-z0-9-]+\.md$/, `${s.id} 的文件名形状不对`);
|
|
43
|
+
assert.ok(s.title, `${s.id} 缺标题`);
|
|
44
|
+
assert.ok(s.queryHint && s.queryHint.length > 20, `${s.id} 缺"实时怎么查"的提示`);
|
|
45
|
+
assert.ok(Number.isFinite(s.ttlMs) && s.ttlMs > 0, `${s.id} 缺 ttlMs`);
|
|
46
|
+
assert.ok(Number.isFinite(s.forceTtlMs) && s.forceTtlMs >= 0, `${s.id} 缺 forceTtlMs`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('板块时间参数:联网板块(github/gitee)必须留强制刷新地板,本地板块不必', () => {
|
|
51
|
+
// 联网板块被前端"切面板就刷"打到会疯狂拉网络,必须有地板
|
|
52
|
+
assert.ok(sectionForceTtl('github') >= 30_000, 'github 要有 force 地板');
|
|
53
|
+
assert.ok(sectionForceTtl('gitee') >= 30_000, 'gitee 要有 force 地板');
|
|
54
|
+
// 本地板块刷一次是毫秒级,不留地板
|
|
55
|
+
for (const id of ['git', 'commands', 'tasks', 'system', 'mindmap']) {
|
|
56
|
+
assert.equal(sectionForceTtl(id), 0, `${id} 不该有地板`);
|
|
57
|
+
}
|
|
58
|
+
// 软 TTL:git 必须比仓库列表短(本地状态变化快、取数便宜)
|
|
59
|
+
assert.ok(sectionTtl('git') < sectionTtl('github'), 'git 的 TTL 该比仓库列表短');
|
|
60
|
+
// 未知 id 的回落行为
|
|
61
|
+
assert.equal(sectionForceTtl('nope'), 0);
|
|
62
|
+
assert.ok(sectionTtl('nope') > 0);
|
|
63
|
+
assert.equal(sectionQueryHint('nope'), '');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('oneLine / truncate:压成单行并截断,摘要里绝不能出换行', () => {
|
|
67
|
+
assert.equal(oneLine(' a\n\n b '), 'a b');
|
|
68
|
+
assert.equal(oneLine(null), '');
|
|
69
|
+
const long = 'x'.repeat(MAX_SUMMARY_CHARS + 50);
|
|
70
|
+
const cut = truncate(long);
|
|
71
|
+
assert.equal(cut.length, MAX_SUMMARY_CHARS);
|
|
72
|
+
assert.ok(cut.endsWith('…'));
|
|
73
|
+
assert.equal(truncate('短'), '短');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('renderSectionFile:文件头必须写采集时间、"这是快照"、以及该块怎么查实时值', () => {
|
|
77
|
+
const text = renderSectionFile({
|
|
78
|
+
id: 'git',
|
|
79
|
+
title: '当前项目 Git 状态',
|
|
80
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
81
|
+
scope: '当前项目 D:/ws/x',
|
|
82
|
+
lines: ['- 分支: main'],
|
|
83
|
+
});
|
|
84
|
+
assert.match(text, /^# 当前项目 Git 状态/);
|
|
85
|
+
assert.match(text, /采集时间: 2026-09-28 10:15:32/);
|
|
86
|
+
assert.match(text, /快照/);
|
|
87
|
+
assert.match(text, /要实时值: .*git status --porcelain/);
|
|
88
|
+
assert.match(text, /- 分支: main/);
|
|
89
|
+
assert.match(text, /覆盖范围: 当前项目 D:\/ws\/x/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('renderSectionFile:取数失败也要落盘并写明原因,不能只留一片空白', () => {
|
|
93
|
+
const text = renderSectionFile({
|
|
94
|
+
id: 'github',
|
|
95
|
+
title: 'GitHub 仓库',
|
|
96
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
97
|
+
error: 'gh CLI 未安装',
|
|
98
|
+
});
|
|
99
|
+
assert.match(text, /取数失败: gh CLI 未安装/);
|
|
100
|
+
assert.match(text, /不要据此推断/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('buildContextBlock:摘要内联、正文不进 —— 这是整个设计最关键的一条', () => {
|
|
104
|
+
const block = buildContextBlock({
|
|
105
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
106
|
+
dirPath: 'C:/Users/x/.zen-gitsync/ai-context',
|
|
107
|
+
results: [
|
|
108
|
+
okResult({ lines: ['- BODY_MARKER_SHOULD_NOT_APPEAR'] }),
|
|
109
|
+
okResult({ id: 'system', title: '系统状态', file: 'system.md', summary: 'CPU 7.7% · 内存 62.3%', lines: ['- BODY_MARKER_SHOULD_NOT_APPEAR'] }),
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
assert.ok(block.includes('分支 main,工作区干净'), '摘要必须在');
|
|
113
|
+
assert.ok(block.includes('git-current.md'), '文件名必须在(模型据此去读)');
|
|
114
|
+
assert.ok(block.includes('C:/Users/x/.zen-gitsync/ai-context'), '目录必须在');
|
|
115
|
+
assert.ok(!block.includes('BODY_MARKER_SHOULD_NOT_APPEAR'), '正文一行都不许进来');
|
|
116
|
+
assert.match(block, /不要回答"我看不到/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('buildContextBlock:每个板块带各自的采集时间(各块独立刷新,共用一个时间就是撒谎)', () => {
|
|
120
|
+
const block = buildContextBlock({
|
|
121
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
122
|
+
dirPath: '/tmp/x',
|
|
123
|
+
results: [
|
|
124
|
+
okResult({ collectedAt: '2026-09-28 10:15:30' }),
|
|
125
|
+
okResult({ id: 'github', title: 'GitHub 仓库', file: 'remote-github.md', summary: '65 个仓库', collectedAt: '2026-09-28 10:05:00' }),
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
assert.match(block, /采集于 2026-09-28 10:15:30/);
|
|
129
|
+
assert.match(block, /采集于 2026-09-28 10:05:00/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('joinSnapshotPath:按目录自身的分隔符风格拼接(单测跨平台稳定)', () => {
|
|
133
|
+
assert.equal(joinSnapshotPath('C:\\Users\\x\\ai-context', 'git-current.md'), 'C:\\Users\\x\\ai-context\\git-current.md');
|
|
134
|
+
assert.equal(joinSnapshotPath('/home/x/ai-context', 'git-current.md'), '/home/x/ai-context/git-current.md');
|
|
135
|
+
assert.equal(joinSnapshotPath('/home/x/ai-context/', 'git-current.md'), '/home/x/ai-context/git-current.md', '尾部斜杠不许重复');
|
|
136
|
+
assert.equal(joinSnapshotPath('', 'git-current.md'), 'git-current.md', '没目录时退化成裸文件名');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('buildContextBlock:表头要给出"目录 + 文件名"的绝对路径示例', () => {
|
|
140
|
+
// 为什么钉这条:摘要行里只有裸文件名,模型要自己拼目录。它有一半概率拿 cwd 当基准
|
|
141
|
+
// 拼成 <项目目录>/git-current.md,读不到,然后回一句"我看不到" —— 而这正是整个块
|
|
142
|
+
// 结尾反复叮嘱不要出现的结果。给全示例就没有这层推断。
|
|
143
|
+
const block = buildContextBlock({
|
|
144
|
+
generatedAt: 'x',
|
|
145
|
+
dirPath: 'C:\\Users\\x\\.zen-gitsync\\ai-context',
|
|
146
|
+
results: [okResult()],
|
|
147
|
+
});
|
|
148
|
+
assert.match(block, /完整路径例:C:\\Users\\x\\.zen-gitsync\\ai-context\\git-current\.md/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('buildContextBlock:冷启动没有摘要行时不该出现半截的表头', () => {
|
|
152
|
+
const block = buildContextBlock({ dirPath: '/tmp/x', results: [] });
|
|
153
|
+
assert.ok(!block.includes('完整路径例'), '没有摘要就不该留下孤零零的路径示例');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('buildContextBlock:冷启动(一份快照都没有)时仍要给出实时查询方法', () => {
|
|
157
|
+
const block = buildContextBlock({
|
|
158
|
+
dirPath: 'C:/Users/x/.zen-gitsync/ai-context',
|
|
159
|
+
results: [],
|
|
160
|
+
});
|
|
161
|
+
assert.ok(block.length > 0, '空快照不等于空块 —— 实时查询方法必须还在');
|
|
162
|
+
assert.match(block, /要查实时值就照下面来/);
|
|
163
|
+
assert.match(block, /gh repo list/);
|
|
164
|
+
assert.match(block, /gitee auth status --json/);
|
|
165
|
+
assert.match(block, /git status --porcelain/);
|
|
166
|
+
assert.match(block, /本轮快照还没生成好/);
|
|
167
|
+
assert.match(block, /C:\/Users\/x\/\.zen-gitsync\/ai-context/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('buildContextBlock:摘要再长也不许把实时查询方法挤掉', () => {
|
|
171
|
+
const results = Array.from({ length: 40 }, (_, i) => okResult({
|
|
172
|
+
id: `s${i}`, title: `板块${i}`, file: `s${i}.md`, summary: '很长的摘要'.repeat(80),
|
|
173
|
+
}));
|
|
174
|
+
const block = buildContextBlock({ generatedAt: 'x', dirPath: '/tmp', results });
|
|
175
|
+
assert.ok(block.length <= MAX_CONTEXT_BLOCK_CHARS, `块长 ${block.length} 超过上限`);
|
|
176
|
+
assert.match(block, /要查实时值就照下面来/, '查询方法那一段永远不能被切掉');
|
|
177
|
+
assert.match(block, /gh repo list/);
|
|
178
|
+
assert.match(block, /不要回答"我看不到/, '收尾的约束也不能被切掉');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('buildContextBlock:真相源文件路径要报给模型', () => {
|
|
182
|
+
const block = buildContextBlock({
|
|
183
|
+
generatedAt: 'x',
|
|
184
|
+
dirPath: '/tmp/x',
|
|
185
|
+
results: [okResult()],
|
|
186
|
+
truthFiles: {
|
|
187
|
+
tasksFile: 'C:/u/.zen-gitsync/tasks.json',
|
|
188
|
+
jobsFile: 'C:/u/.zen-gitsync/jobs.json',
|
|
189
|
+
// 编排台指令流水:与 envContext(派发任务时的运行环境块)报同一份清单。
|
|
190
|
+
// 两个页面各自承认哪些文件存在不该有差别,所以这里也钉一条。
|
|
191
|
+
orchestratorFile: 'C:/u/.zen-gitsync/orchestrator.json',
|
|
192
|
+
configFile: 'C:/u/.zen-gitsync/config.json',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
assert.match(block, /tasks\.json/);
|
|
196
|
+
assert.match(block, /jobs\.json/);
|
|
197
|
+
assert.match(block, /orchestrator\.json/);
|
|
198
|
+
assert.match(block, /config\.json/);
|
|
199
|
+
assert.match(block, /真相源文件/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('buildContextBlock:truthFiles 缺哪条就少哪行(不写空路径)', () => {
|
|
203
|
+
const block = buildContextBlock({
|
|
204
|
+
generatedAt: 'x',
|
|
205
|
+
dirPath: '/tmp/x',
|
|
206
|
+
results: [okResult()],
|
|
207
|
+
truthFiles: { tasksFile: 'C:/u/.zen-gitsync/tasks.json', orchestratorFile: '', configFile: '' },
|
|
208
|
+
});
|
|
209
|
+
assert.match(block, /tasks\.json/);
|
|
210
|
+
assert.doesNotMatch(block, /- C:\/u\/\.zen-gitsync\/ ——/);
|
|
211
|
+
assert.doesNotMatch(block, /undefined/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('buildContextBlock:失败的板块要写成"暂时取不到"而不是留空', () => {
|
|
215
|
+
const block = buildContextBlock({
|
|
216
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
217
|
+
dirPath: '/tmp/x',
|
|
218
|
+
results: [okResult({ id: 'gitee', title: 'Gitee 仓库', file: 'remote-gitee.md', ok: false, error: '未检测到 gitee CLI' })],
|
|
219
|
+
});
|
|
220
|
+
assert.match(block, /暂时取不到: 未检测到 gitee CLI/);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('buildContextBlock:英文 locale 用英文说明(文件正文仍为中文)', () => {
|
|
224
|
+
const block = buildContextBlock({
|
|
225
|
+
generatedAt: '2026-09-28 10:15:32',
|
|
226
|
+
dirPath: '/tmp/x',
|
|
227
|
+
locale: 'en',
|
|
228
|
+
results: [okResult()],
|
|
229
|
+
});
|
|
230
|
+
assert.match(block, /Workspace state/);
|
|
231
|
+
assert.match(block, /How to get LIVE values/);
|
|
232
|
+
assert.match(block, /SNAPSHOT, not live/);
|
|
233
|
+
assert.match(block, /Section summaries/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('renderIndexFile:列出每个板块并标明采集时间与失败项', () => {
|
|
237
|
+
const text = renderIndexFile({
|
|
238
|
+
generatedAt: '2026-09-28 10:20:00',
|
|
239
|
+
results: [
|
|
240
|
+
okResult(),
|
|
241
|
+
okResult({ id: 'gitee', title: 'Gitee 仓库', file: 'remote-gitee.md', ok: false, error: '未登录', collectedAt: '2026-09-28 10:05:00' }),
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
assert.match(text, /- \[2026-09-28 10:15:32\] 当前项目 Git 状态(git-current.md): 分支 main/);
|
|
245
|
+
assert.match(text, /- \[2026-09-28 10:05:00\] Gitee 仓库(remote-gitee.md): 取数失败: 未登录/);
|
|
246
|
+
// 实时查询方法一节只列**这份 INDEX 里实际有的**板块(这里只有 git / gitee)
|
|
247
|
+
assert.match(text, /## 实时查询方法/);
|
|
248
|
+
assert.match(text, /git status --porcelain/);
|
|
249
|
+
assert.match(text, /gitee auth status --json/);
|
|
250
|
+
assert.ok(!text.includes('gh repo list'), '没采集到的板块不该出现在 INDEX 里');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('formatStamp:补零到秒,模型靠它判断新旧', () => {
|
|
254
|
+
assert.equal(formatStamp(new Date(2026, 8, 28, 9, 5, 3)), '2026-09-28 09:05:03');
|
|
255
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
// 生产环境的接线:把真实的取数实现装配进 createAiContextSnapshotter。
|
|
16
|
+
//
|
|
17
|
+
// 单独一个文件的原因(与 collectors.js 的分层对应):index.js 与 collectors.js
|
|
18
|
+
// 保持"零重量级 import",单测只碰纯函数;**只有这里**才 import 那些带着副作用或
|
|
19
|
+
// 拉子进程的模块。这样"这个快照会不会在 import 时就写用户数据"这个问题,看一个
|
|
20
|
+
// 文件就能回答。
|
|
21
|
+
//
|
|
22
|
+
// 两个刻意的取舍:
|
|
23
|
+
//
|
|
24
|
+
// 1) 所有取数都**复用已有实现**,不新写第二份:
|
|
25
|
+
// · git 状态 → directoryGitState.probeDirectoryGitState(自带 15s TTL 缓存)
|
|
26
|
+
// · 远程仓库 → remoteRepos.loadRemoteReposState(与界面同一份装了/登录了判断)
|
|
27
|
+
// · 系统状态 → monitor.getSystemOverview / listPorts(与监控页同源)
|
|
28
|
+
// · 思维导图 → mindmap.listMindmapFiles(与列表页同源)
|
|
29
|
+
// 否则快照里的数字和界面上的对不上,用户第一个就会问为什么。
|
|
30
|
+
//
|
|
31
|
+
// 2) jobStore **动态 import**,不放在顶层:
|
|
32
|
+
// jobStore.js 在模块加载时就会 hydrateJobs() 并回写 ~/.zen-gitsync/jobs.json
|
|
33
|
+
// (见 promptParts.js 头注释对这件事的处理)。顶层 import 会让"谁 import 了
|
|
34
|
+
// aiContext 就在加载期写盘"变成隐性副作用;放到真正要读执行记录的那一刻,
|
|
35
|
+
// 副作用就落在"确实有一轮对话在跑"这个前提下,边界清楚。
|
|
36
|
+
|
|
37
|
+
import { createAiContextSnapshotter } from './index.js';
|
|
38
|
+
import { readJson, TASKS_FILE, TRUTH_FILES } from '../workbench/shared.js';
|
|
39
|
+
import { probeDirectoryGitState } from '../../utils/directoryGitState.js';
|
|
40
|
+
import { loadRemoteReposState } from '../remoteRepos.js';
|
|
41
|
+
import { getSystemOverview, listPorts } from '../monitor.js';
|
|
42
|
+
import { listMindmapFiles } from '../mindmap.js';
|
|
43
|
+
|
|
44
|
+
/** tasks.json 里的任务数组(容错:文件损坏/不存在时回空数组,由 collector 的计数体现) */
|
|
45
|
+
async function readTasks() {
|
|
46
|
+
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
47
|
+
return Array.isArray(data?.tasks) ? data.tasks : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 执行记录:先刷一遍磁盘,避免 Agent 看到的是本进程启动那一刻的旧状态 */
|
|
51
|
+
async function readJobs() {
|
|
52
|
+
const { refreshJobsFromDisk, snapshotJobs } = await import('../workbench/jobStore.js');
|
|
53
|
+
await refreshJobsFromDisk();
|
|
54
|
+
return snapshotJobs();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 建一个面向生产的快照生成器。
|
|
59
|
+
* @param {object} deps
|
|
60
|
+
* @param {object} deps.configManager
|
|
61
|
+
* @param {Function} deps.getCurrentProjectPath
|
|
62
|
+
*/
|
|
63
|
+
export function createWorkspaceSnapshotter({ configManager, getCurrentProjectPath } = {}) {
|
|
64
|
+
return createAiContextSnapshotter({
|
|
65
|
+
configManager,
|
|
66
|
+
getCurrentProjectPath,
|
|
67
|
+
readTasks,
|
|
68
|
+
readJobs,
|
|
69
|
+
probeGit: (dir) => probeDirectoryGitState(dir),
|
|
70
|
+
loadRemoteReposState: (provider) => loadRemoteReposState(provider),
|
|
71
|
+
getMonitorOverview: () => getSystemOverview(),
|
|
72
|
+
// 只取监听端口:快照里列全量 ESTABLISHED 连接会把文件撑爆且毫无信息量
|
|
73
|
+
listPorts: () => listPorts({ all: false }),
|
|
74
|
+
listMindmapFiles: (dir) => listMindmapFiles(dir),
|
|
75
|
+
// 真相源路径:注入块里直接报给模型,它就能自己 read_file 拿全文,
|
|
76
|
+
// 不必再多一轮"我看看有哪些文件"。清单只有一份 —— 见 workbench/shared.js
|
|
77
|
+
// 的 TRUTH_FILES:envContext(编排台视角)用的是同一个常量,不会再加一处漏一处。
|
|
78
|
+
truthFiles: TRUTH_FILES,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -996,4 +996,47 @@ export function registerFsRoutes({
|
|
|
996
996
|
res.status(status).json({ success: false, error: error.message });
|
|
997
997
|
}
|
|
998
998
|
});
|
|
999
|
+
|
|
1000
|
+
// ── 编辑器:用系统浏览器打开文件(HTML)──────────────────────
|
|
1001
|
+
// 和上面的 VSCode 路线分工不同:HTML 的诉求是"看渲染结果",必须落到浏览器。
|
|
1002
|
+
// 不能图省事用 open(resolved) 走系统默认程序 —— .html 的默认关联完全可能
|
|
1003
|
+
// 是编辑器(装过 VSCode / 各种 IDE 后很常见),那样右键"用浏览器打开"就名不
|
|
1004
|
+
// 副实,还难排查。
|
|
1005
|
+
//
|
|
1006
|
+
// open 的 app: 'browser' 会先用 default-browser 解析系统默认浏览器(Windows
|
|
1007
|
+
// 读注册表 UserChoice、macOS 走 LaunchServices、Linux 走 xdg),再用它打开:
|
|
1008
|
+
// - 命中 chrome / edge / firefox / brave 之一 → 直接在默认浏览器里开
|
|
1009
|
+
// - 默认浏览器不在 open 的已知清单里 → open 抛错,退化为系统默认程序,
|
|
1010
|
+
// 至少不会静默失败(比"报错但什么都没发生"体验好)
|
|
1011
|
+
// 传文件路径而不是 http URL:file:// 下同目录的相对资源(样式 / 图片)能正常解析,
|
|
1012
|
+
// 不需要把整个工作目录再挂一个静态服务出去。
|
|
1013
|
+
app.post('/api/editor/open-in-browser', express.json(), async (req, res) => {
|
|
1014
|
+
try {
|
|
1015
|
+
const targetPath = req.body?.path;
|
|
1016
|
+
if (!targetPath) throw new HttpError(400, '缺少 path 参数');
|
|
1017
|
+
|
|
1018
|
+
const safe = await safePathInProject(targetPath);
|
|
1019
|
+
if (!safe) throw new HttpError(403, '禁止访问工作目录以外的内容');
|
|
1020
|
+
const resolved = safe.safePath;
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
await fs.access(resolved);
|
|
1024
|
+
} catch {
|
|
1025
|
+
throw new HttpError(404, '目标不存在');
|
|
1026
|
+
}
|
|
1027
|
+
const stat = await fs.stat(resolved);
|
|
1028
|
+
if (stat.isDirectory()) throw new HttpError(400, '只能对文件用浏览器打开');
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
await open(resolved, { app: 'browser', wait: false });
|
|
1032
|
+
} catch {
|
|
1033
|
+
await open(resolved, { wait: false });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
res.json({ success: true });
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const status = error?.status || 500;
|
|
1039
|
+
res.status(status).json({ success: false, error: error.message });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
999
1042
|
}
|
|
@@ -96,6 +96,41 @@ function isInside(childResolved, dirResolved) {
|
|
|
96
96
|
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* 列出某个目录下的思维导图文件(按修改时间倒序)。
|
|
101
|
+
*
|
|
102
|
+
* 抽成导出函数是为了给 aiContext 的「思维导图」快照复用 —— 快照里数出来的文件
|
|
103
|
+
* 必须和界面列表是同一批、同一排序,所以两边共用这一份实现,不做第二遍 readdir。
|
|
104
|
+
* 校验 / 抛错(目录不存在、不是目录)仍留在路由层,这里只做"已知是个目录"之后的活。
|
|
105
|
+
*/
|
|
106
|
+
export async function listMindmapFiles(dir) {
|
|
107
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
108
|
+
const files = []
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
// 只收 .mindmap.json 文件,跳过目录/隐藏文件
|
|
111
|
+
if (!e.isFile()) continue
|
|
112
|
+
if (e.name.startsWith('.')) continue
|
|
113
|
+
if (!e.name.toLowerCase().endsWith(MINDMAP_EXT)) continue
|
|
114
|
+
const fullPath = path.join(dir, e.name)
|
|
115
|
+
try {
|
|
116
|
+
const st = await fs.stat(fullPath)
|
|
117
|
+
files.push({
|
|
118
|
+
name: e.name,
|
|
119
|
+
path: fullPath,
|
|
120
|
+
size: st.size,
|
|
121
|
+
mtime: st.mtimeMs,
|
|
122
|
+
// 展示用名(去掉扩展名)
|
|
123
|
+
title: e.name.slice(0, -MINDMAP_EXT.length)
|
|
124
|
+
})
|
|
125
|
+
} catch {
|
|
126
|
+
// stat 失败的条目跳过,不阻塞整个列表
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 按修改时间倒序(最新在上)
|
|
130
|
+
files.sort((a, b) => b.mtime - a.mtime)
|
|
131
|
+
return files
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
export function registerMindmapRoutes({ app }) {
|
|
100
135
|
// ── 列出目录下的思维导图文件 ──────────────────────────────────────
|
|
101
136
|
app.get(
|
|
@@ -113,30 +148,7 @@ export function registerMindmapRoutes({ app }) {
|
|
|
113
148
|
}
|
|
114
149
|
if (!stat.isDirectory()) throw new HttpError(400, '目标不是目录')
|
|
115
150
|
|
|
116
|
-
const
|
|
117
|
-
const files = []
|
|
118
|
-
for (const e of entries) {
|
|
119
|
-
// 只收 .mindmap.json 文件,跳过目录/隐藏文件
|
|
120
|
-
if (!e.isFile()) continue
|
|
121
|
-
if (e.name.startsWith('.')) continue
|
|
122
|
-
if (!e.name.toLowerCase().endsWith(MINDMAP_EXT)) continue
|
|
123
|
-
const fullPath = path.join(dir, e.name)
|
|
124
|
-
try {
|
|
125
|
-
const st = await fs.stat(fullPath)
|
|
126
|
-
files.push({
|
|
127
|
-
name: e.name,
|
|
128
|
-
path: fullPath,
|
|
129
|
-
size: st.size,
|
|
130
|
-
mtime: st.mtimeMs,
|
|
131
|
-
// 展示用名(去掉扩展名)
|
|
132
|
-
title: e.name.slice(0, -MINDMAP_EXT.length)
|
|
133
|
-
})
|
|
134
|
-
} catch {
|
|
135
|
-
// stat 失败的条目跳过,不阻塞整个列表
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// 按修改时间倒序(最新在上)
|
|
139
|
-
files.sort((a, b) => b.mtime - a.mtime)
|
|
151
|
+
const files = await listMindmapFiles(dir)
|
|
140
152
|
res.json({ success: true, dir, files })
|
|
141
153
|
})
|
|
142
154
|
)
|
|
@@ -46,7 +46,9 @@ function sampleCpu() {
|
|
|
46
46
|
return { total, idle }
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// 导出给 aiContext 的「系统状态」快照复用 —— 快照里的 CPU/内存/磁盘/端口
|
|
50
|
+
// 必须与监控页看到的是同一份实现,否则两处数字对不上,用户第一个就会问为什么。
|
|
51
|
+
export async function getCpuUsage(sampleDelayMs = 100) {
|
|
50
52
|
const a = sampleCpu()
|
|
51
53
|
await new Promise((r) => setTimeout(r, sampleDelayMs))
|
|
52
54
|
const b = sampleCpu()
|
|
@@ -122,7 +124,7 @@ async function getDiskUsageUnix() {
|
|
|
122
124
|
]
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
async function getDiskUsage() {
|
|
127
|
+
export async function getDiskUsage() {
|
|
126
128
|
const now = Date.now()
|
|
127
129
|
if (_diskCache && now - _diskCacheTime < DISK_CACHE_TTL_MS) {
|
|
128
130
|
return _diskCache
|
|
@@ -144,8 +146,46 @@ async function getDiskUsage() {
|
|
|
144
146
|
return result
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/**
|
|
150
|
+
* 系统概览的完整组装(CPU + 内存 + 磁盘 + 系统信息)。
|
|
151
|
+
*
|
|
152
|
+
* 抽成导出函数是为了让 aiContext 的「系统状态」快照复用同一份 —— 快照里的数字
|
|
153
|
+
* 必须和监控页一致,两处各拼一遍迟早一个改了另一个没改。
|
|
154
|
+
* 磁盘查询失败降级为 disks: null,不拖垮整个概览。
|
|
155
|
+
*/
|
|
156
|
+
export async function getSystemOverview() {
|
|
157
|
+
const cpu = await getCpuUsage(100)
|
|
158
|
+
const totalMem = os.totalmem()
|
|
159
|
+
const freeMem = os.freemem()
|
|
160
|
+
const usedMem = totalMem - freeMem
|
|
161
|
+
let disks = null
|
|
162
|
+
try {
|
|
163
|
+
disks = await getDiskUsage()
|
|
164
|
+
} catch (e) {
|
|
165
|
+
logger.warn(`[monitor] 获取磁盘占用失败: ${e?.message || e}`)
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
cpu,
|
|
169
|
+
memory: {
|
|
170
|
+
total: totalMem,
|
|
171
|
+
free: freeMem,
|
|
172
|
+
used: usedMem,
|
|
173
|
+
usagePercent: totalMem > 0 ? (usedMem / totalMem) * 100 : 0
|
|
174
|
+
},
|
|
175
|
+
disks,
|
|
176
|
+
system: {
|
|
177
|
+
platform: process.platform,
|
|
178
|
+
arch: process.arch,
|
|
179
|
+
hostname: os.hostname(),
|
|
180
|
+
uptime: os.uptime(),
|
|
181
|
+
nodeVersion: process.version
|
|
182
|
+
},
|
|
183
|
+
timestamp: Date.now()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
// ── 端口列表 ────────────────────────────────────────────────────────────
|
|
148
|
-
async function listPorts({ all = false } = {}) {
|
|
188
|
+
export async function listPorts({ all = false } = {}) {
|
|
149
189
|
if (process.platform === 'win32') {
|
|
150
190
|
return listPortsWindows({ all })
|
|
151
191
|
}
|
|
@@ -372,38 +412,7 @@ export function registerMonitorRoutes({ app }) {
|
|
|
372
412
|
app.get(
|
|
373
413
|
'/api/monitor/system',
|
|
374
414
|
asyncRoute(async (req, res) => {
|
|
375
|
-
|
|
376
|
-
const totalMem = os.totalmem()
|
|
377
|
-
const freeMem = os.freemem()
|
|
378
|
-
const usedMem = totalMem - freeMem
|
|
379
|
-
// 磁盘查询失败不应拖垮整个概览接口,降级为 disks: null
|
|
380
|
-
let disks = null
|
|
381
|
-
try {
|
|
382
|
-
disks = await getDiskUsage()
|
|
383
|
-
} catch (e) {
|
|
384
|
-
logger.warn(`[monitor] 获取磁盘占用失败: ${e?.message || e}`)
|
|
385
|
-
}
|
|
386
|
-
res.json({
|
|
387
|
-
success: true,
|
|
388
|
-
data: {
|
|
389
|
-
cpu,
|
|
390
|
-
memory: {
|
|
391
|
-
total: totalMem,
|
|
392
|
-
free: freeMem,
|
|
393
|
-
used: usedMem,
|
|
394
|
-
usagePercent: totalMem > 0 ? (usedMem / totalMem) * 100 : 0
|
|
395
|
-
},
|
|
396
|
-
disks,
|
|
397
|
-
system: {
|
|
398
|
-
platform: process.platform,
|
|
399
|
-
arch: process.arch,
|
|
400
|
-
hostname: os.hostname(),
|
|
401
|
-
uptime: os.uptime(),
|
|
402
|
-
nodeVersion: process.version
|
|
403
|
-
},
|
|
404
|
-
timestamp: Date.now()
|
|
405
|
-
}
|
|
406
|
-
})
|
|
415
|
+
res.json({ success: true, data: await getSystemOverview() })
|
|
407
416
|
})
|
|
408
417
|
)
|
|
409
418
|
|
|
@@ -557,6 +557,58 @@ async function listRepos(provider, executable) {
|
|
|
557
557
|
return { repos: all, error: null, truncated };
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
/**
|
|
561
|
+
* 完整探测一个平台的状态:装没装 → 登没登录 → 拉仓库列表。
|
|
562
|
+
*
|
|
563
|
+
* 抽成导出函数,让**两条链路共用同一份判断**:HTTP 路由(界面)与
|
|
564
|
+
* aiContext 的「GitHub / Gitee 仓库」快照(喂给模型)。这份判断里全是踩过的坑
|
|
565
|
+
* (gitee 未登录退出码仍是 0、CLI 可能不在 PATH、超时只回空列表不抛),抄成第二份
|
|
566
|
+
* 迟早分叉 —— 而分叉的表现是"界面说未登录、模型说你有 65 个仓库"。
|
|
567
|
+
*
|
|
568
|
+
* 永远不抛:四种状态(未装 / 未登录 / 已登录 / 拉取失败)都是**正常状态**,
|
|
569
|
+
* 由调用方决定怎么呈现。
|
|
570
|
+
*
|
|
571
|
+
* @param {'github'|'gitee'} provider
|
|
572
|
+
* @param {object} [impls] 只为测试留的注入口
|
|
573
|
+
* @returns {Promise<{installed:boolean, version:string|null, authenticated:boolean, user:string|null,
|
|
574
|
+
* repos:object[], truncated:boolean, error:string|null}>}
|
|
575
|
+
*/
|
|
576
|
+
export async function loadRemoteReposState(provider, {
|
|
577
|
+
resolveCliImpl = resolveCli,
|
|
578
|
+
detectCliImpl = detectCli,
|
|
579
|
+
listReposImpl = listRepos,
|
|
580
|
+
} = {}) {
|
|
581
|
+
const cli = PROVIDERS[provider]?.cli || provider;
|
|
582
|
+
|
|
583
|
+
const resolved = await resolveCliImpl(provider);
|
|
584
|
+
if (!resolved) {
|
|
585
|
+
return { installed: false, version: null, authenticated: false, user: null, repos: [], truncated: false, error: null };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let detected;
|
|
589
|
+
try {
|
|
590
|
+
detected = await detectCliImpl(provider, resolved.executable, resolved.version);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
logger.info(`[remote-repos] ${provider} 检测失败: ${error.message}`);
|
|
593
|
+
return {
|
|
594
|
+
installed: true,
|
|
595
|
+
version: resolved.version,
|
|
596
|
+
authenticated: false,
|
|
597
|
+
user: null,
|
|
598
|
+
repos: [],
|
|
599
|
+
truncated: false,
|
|
600
|
+
error: `检测 ${cli} 状态失败: ${error.message}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!detected.authenticated) {
|
|
605
|
+
return { ...detected, repos: [], truncated: false, error: null };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const { repos, error, truncated } = await listReposImpl(provider, resolved.executable);
|
|
609
|
+
return { ...detected, repos, truncated: !!truncated, error: error || null };
|
|
610
|
+
}
|
|
611
|
+
|
|
560
612
|
// ── 路由 ────────────────────────────────────────────────────────────────────
|
|
561
613
|
|
|
562
614
|
/**
|
|
@@ -603,55 +655,12 @@ export function registerRemoteReposRoutes({
|
|
|
603
655
|
installer: installer ? publicInstallerInfo({ [config.toolId]: installer })[config.toolId] : null,
|
|
604
656
|
};
|
|
605
657
|
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
installed: false,
|
|
611
|
-
version: null,
|
|
612
|
-
authenticated: false,
|
|
613
|
-
user: null,
|
|
614
|
-
repos: [],
|
|
615
|
-
truncated: false,
|
|
616
|
-
error: null,
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
let detected;
|
|
621
|
-
try {
|
|
622
|
-
detected = await detectCliImpl(provider, resolved.executable, resolved.version);
|
|
623
|
-
} catch (error) {
|
|
624
|
-
logger.info(`[remote-repos] ${provider} 检测失败: ${error.message}`);
|
|
625
|
-
return res.json({
|
|
626
|
-
...base,
|
|
627
|
-
installed: true,
|
|
628
|
-
version: resolved.version,
|
|
629
|
-
authenticated: false,
|
|
630
|
-
user: null,
|
|
631
|
-
repos: [],
|
|
632
|
-
truncated: false,
|
|
633
|
-
error: `检测 ${config.cli} 状态失败: ${error.message}`,
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (!detected.authenticated) {
|
|
638
|
-
return res.json({
|
|
639
|
-
...base,
|
|
640
|
-
...detected,
|
|
641
|
-
repos: [],
|
|
642
|
-
truncated: false,
|
|
643
|
-
error: null,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const { repos, error, truncated } = await listReposImpl(provider, resolved.executable);
|
|
648
|
-
res.json({
|
|
649
|
-
...base,
|
|
650
|
-
...detected,
|
|
651
|
-
repos,
|
|
652
|
-
truncated: !!truncated,
|
|
653
|
-
error: error || null,
|
|
658
|
+
const state = await loadRemoteReposState(provider, {
|
|
659
|
+
resolveCliImpl,
|
|
660
|
+
detectCliImpl,
|
|
661
|
+
listReposImpl,
|
|
654
662
|
});
|
|
663
|
+
res.json({ ...base, ...state });
|
|
655
664
|
}));
|
|
656
665
|
|
|
657
666
|
/**
|