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
|
@@ -12,33 +12,37 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
//
|
|
15
|
-
// 执行时注入给 Agent
|
|
15
|
+
// 执行时注入给 Agent 的「运行环境上下文」——**多项目编排台视角**。
|
|
16
16
|
//
|
|
17
17
|
// 解决什么问题:从主 Agent 调度台发出去的指令,最终 prompt 只是用户原文
|
|
18
18
|
// + 附件清单(拼装见 promptParts.js),cwd 之外的项目、看板进度、历史指令
|
|
19
19
|
// 一概不在里面。于是用户问"我的项目有哪些",Agent 只能就着当前目录猜 ——
|
|
20
20
|
// 而数据其实全在本机,只是没人告诉它路径。
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
22
|
+
// ── 与 aiContext 的关系(两条视图、一个口径)────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// 数据**不在这里取**。项目清单 / 各列计数 / 合计 / 真相源路径全部来自
|
|
25
|
+
// aiContext 的 `tasks` 板块(collectors.js 的 collectWorkbenchTasks),本文件
|
|
26
|
+
// 只负责把它渲染成"编排台视角"该有的样子:
|
|
27
|
+
// · 标出**当前任务属于哪个项目**(`← 当前`)—— 派发出来的任务知道自己是谁,
|
|
28
|
+
// 这一点在七个板块那份视图里没有;
|
|
29
|
+
// · 带上**用户偏好**(克隆优先 SSH);
|
|
30
|
+
// · 真相源清单里多一个 orchestrator.json(调度台指令流水)。
|
|
31
|
+
//
|
|
32
|
+
// 为什么不各自算一遍:以前这里自己读 tasks.json + 最近目录 + 任务分列,与
|
|
33
|
+
// aiContext 的 tasks 板块是**同一份统计的第二份实现**。两份实现走同一天开始分叉:
|
|
34
|
+
// 改一处漏一处不会报错,只会让 Agent 在编排台里和在智能体页里看到**不同的项目数**。
|
|
35
|
+
// 现在只有 collectWorkbenchTasks 一份。
|
|
30
36
|
//
|
|
31
37
|
// ⚠️ 文案为什么可以是中文硬编码(与 orchestratorStore 的「服务端不写死中文」不冲突):
|
|
32
38
|
// 那一条约束针对的是**回给 UI 渲染的文案**,而这里是喂给模型的 prompt 内容,
|
|
33
39
|
// 不经过前端 $t(),也不会显示在界面上。反过来说,它也不该跟着界面语言变 ——
|
|
34
40
|
// 中文指令配中文上下文,模型理解得更稳。
|
|
35
41
|
//
|
|
36
|
-
//
|
|
42
|
+
// 纯函数:只吃入参、不读文件(取数在 workbench/index.js,数据在快照里)。
|
|
37
43
|
// 这样单测不用碰真实用户数据 —— 与 promptParts.js 同一个理由。
|
|
38
|
-
// 统计口径复用 projectRegistry 的 summarizeProjectTasks:「任务算哪一列」
|
|
39
|
-
// 全局只有这一个实现,看板怎么分列,这里就怎么统计。
|
|
40
44
|
|
|
41
|
-
import {
|
|
45
|
+
import { canonicalProjectPath, TASK_COLUMNS } from './projectRegistry.js';
|
|
42
46
|
|
|
43
47
|
/** 项目清单最多列几个:再多就该让 Agent 自己去读文件,而不是往 prompt 里堆 */
|
|
44
48
|
export const ENV_CONTEXT_MAX_PROJECTS = 30;
|
|
@@ -60,45 +64,25 @@ const columnLabels = TASK_COLUMNS.map(k => COLUMN_LABELS[k] || k);
|
|
|
60
64
|
*
|
|
61
65
|
* @param {object} input
|
|
62
66
|
* @param {string} input.currentProjectPath 本次任务的工作目录(= Agent 的 cwd)
|
|
63
|
-
* @param {object[
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @param {
|
|
67
|
-
*
|
|
68
|
-
* @param {string} input.orchestratorFile
|
|
69
|
-
* @param {string} input.configFile
|
|
67
|
+
* @param {object} [input.board] aiContext `tasks` 板块的结构化数据
|
|
68
|
+
* ({ projects, statsByKey, total, runningJobs })。
|
|
69
|
+
* 取不到时项目那一段整体省掉,但偏好与真相源照给。
|
|
70
|
+
* @param {object} [input.truthFiles] { tasksFile, jobsFile, orchestratorFile, configFile }
|
|
71
|
+
* 路径口径的唯一来源见 workbench/shared.js 的 TRUTH_FILES
|
|
70
72
|
* @param {number} [input.maxProjects] 只影响列出条数,不影响合计统计
|
|
71
|
-
* @returns {string
|
|
73
|
+
* @returns {string} 上下文块。**不返回 null** —— 见下面的注释。
|
|
72
74
|
*/
|
|
73
75
|
export function buildEnvContextBlock({
|
|
74
76
|
currentProjectPath = '',
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
jobs = [],
|
|
78
|
-
tasksFile = '',
|
|
79
|
-
jobsFile = '',
|
|
80
|
-
orchestratorFile = '',
|
|
81
|
-
configFile = '',
|
|
77
|
+
board = null,
|
|
78
|
+
truthFiles = {},
|
|
82
79
|
maxProjects = ENV_CONTEXT_MAX_PROJECTS,
|
|
83
80
|
} = {}) {
|
|
84
|
-
const list = Array.isArray(projects) ? projects.filter(p => p && p.key) : [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
Array.isArray(tasks) ? tasks : [],
|
|
89
|
-
Array.isArray(jobs) ? jobs : [],
|
|
90
|
-
);
|
|
81
|
+
const list = Array.isArray(board?.projects) ? board.projects.filter(p => p && p.key) : [];
|
|
82
|
+
const statsByKey = (board?.statsByKey && typeof board.statsByKey === 'object') ? board.statsByKey : {};
|
|
83
|
+
const total = board?.total || {};
|
|
84
|
+
const runningJobs = Number(board?.runningJobs) || 0;
|
|
91
85
|
|
|
92
|
-
// 合计按全部任务算(含「未关联项目」那一桶),所以不能用 list 求和
|
|
93
|
-
const total = { total: 0 };
|
|
94
|
-
for (const key of TASK_COLUMNS) total[key] = 0;
|
|
95
|
-
for (const s of stats.values()) {
|
|
96
|
-
total.total += s.total;
|
|
97
|
-
for (const key of TASK_COLUMNS) total[key] += s[key] || 0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const limit = Number.isFinite(maxProjects) && maxProjects > 0 ? Math.floor(maxProjects) : ENV_CONTEXT_MAX_PROJECTS;
|
|
101
|
-
const shown = list.slice(0, limit);
|
|
102
86
|
// 当前项目的比对**必须归一化后再比**:条目 key 是 canonicalProjectPath 的结果
|
|
103
87
|
// (Windows 形式小写 + 斜杠归一如 d:\ws\x),而调用方给的 currentProjectPath 是原始写法
|
|
104
88
|
// (盘符/目录段大小写都不一定一致)。直接拿 path 跟 key 比会永远不等 —— 同款口径分叉
|
|
@@ -110,22 +94,35 @@ export function buildEnvContextBlock({
|
|
|
110
94
|
lines.push('');
|
|
111
95
|
lines.push(`当前任务所在项目(也是你的工作目录): ${currentProjectPath || '(未指定)'}`);
|
|
112
96
|
lines.push('');
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
lines.push(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
97
|
+
|
|
98
|
+
if (list.length > 0) {
|
|
99
|
+
const limit = Number.isFinite(maxProjects) && maxProjects > 0 ? Math.floor(maxProjects) : ENV_CONTEXT_MAX_PROJECTS;
|
|
100
|
+
const shown = list.slice(0, limit);
|
|
101
|
+
lines.push(`项目清单(共 ${list.length} 个;格式: 名称 | 路径 | ${columnLabels.join('/')}):`);
|
|
102
|
+
for (const p of shown) {
|
|
103
|
+
const s = statsByKey[p.key] || {};
|
|
104
|
+
const mark = currentKey && p.key === currentKey ? ' ← 当前' : '';
|
|
105
|
+
lines.push(`- ${p.name || p.key} | ${p.path} | ${TASK_COLUMNS.map(k => s[k] || 0).join('/')}${mark}`);
|
|
106
|
+
}
|
|
107
|
+
if (list.length > shown.length) {
|
|
108
|
+
lines.push(`- …还有 ${list.length - shown.length} 个未列出(读下面的文件可以看全)`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push(
|
|
112
|
+
`看板任务概览:全部项目合计 ${total.total || 0} 条 —— ` +
|
|
113
|
+
TASK_COLUMNS.map(k => `${COLUMN_LABELS[k]} ${total[k] || 0}`).join(' / ') +
|
|
114
|
+
(runningJobs > 0 ? `,正在执行 ${runningJobs} 个` : ',当前无执行中的任务'),
|
|
115
|
+
);
|
|
116
|
+
lines.push('');
|
|
117
|
+
} else {
|
|
118
|
+
// 快照还没生成好(或 tasks 板块取数失败)。项目那一段省掉,**但偏好与真相源照给** ——
|
|
119
|
+
// 它们不需要取数,而且 SSH 那条少一次,任务就会在克隆仓库时退回 https、
|
|
120
|
+
// 弹出凭据窗口把任务停住(这正是文件头第 3 条要防的)。
|
|
121
|
+
lines.push('项目清单:本次未能取到(看板快照还没生成好)。需要时直接读下面的 tasks.json。');
|
|
122
|
+
lines.push('');
|
|
121
123
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`看板任务概览:全部项目合计 ${total.total} 条 —— ` +
|
|
125
|
-
TASK_COLUMNS.map(k => `${COLUMN_LABELS[k]} ${total[k] || 0}`).join(' / '),
|
|
126
|
-
);
|
|
127
|
-
lines.push('');
|
|
128
|
-
// 用户偏好(见文件头第 3 条)。放在"读文件"之前、"看板概览"之后 ——
|
|
124
|
+
|
|
125
|
+
// 用户偏好(见文件头)。放在"读文件"之前、"看板概览"之后 ——
|
|
129
126
|
// 它是**行为**约定,与上面的事实清单分开;压尾的那句仍是"去读文件",
|
|
130
127
|
// 那是整块上下文里最需要被记住的动作。
|
|
131
128
|
lines.push(
|
|
@@ -137,10 +134,10 @@ export function buildEnvContextBlock({
|
|
|
137
134
|
);
|
|
138
135
|
lines.push('');
|
|
139
136
|
lines.push('需要细节时直接读这些文件(本机绝对路径,你有读取权限,不必先问用户):');
|
|
140
|
-
if (tasksFile) lines.push(`- ${tasksFile} —— 全部任务全文(标题/描述/子任务状态/报错/projectPath)`);
|
|
141
|
-
if (jobsFile) lines.push(`- ${jobsFile} —— 历次执行记录(状态与输出)`);
|
|
142
|
-
if (orchestratorFile) lines.push(`- ${orchestratorFile} —— 用户在调度台发过的指令流水`);
|
|
143
|
-
if (configFile) lines.push(`- ${configFile} —— 应用配置(projects / recentDirectories)`);
|
|
137
|
+
if (truthFiles.tasksFile) lines.push(`- ${truthFiles.tasksFile} —— 全部任务全文(标题/描述/子任务状态/报错/projectPath)`);
|
|
138
|
+
if (truthFiles.jobsFile) lines.push(`- ${truthFiles.jobsFile} —— 历次执行记录(状态与输出)`);
|
|
139
|
+
if (truthFiles.orchestratorFile) lines.push(`- ${truthFiles.orchestratorFile} —— 用户在调度台发过的指令流水`);
|
|
140
|
+
if (truthFiles.configFile) lines.push(`- ${truthFiles.configFile} —— 应用配置(projects / recentDirectories)`);
|
|
144
141
|
lines.push('');
|
|
145
142
|
lines.push(
|
|
146
143
|
'用户问到"我有哪些项目""某件事进展如何"这类问题时,先读上面的文件再回答;' +
|
|
@@ -14,23 +14,32 @@
|
|
|
14
14
|
//
|
|
15
15
|
// 运行环境上下文(envContext.js)纯函数单测。
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// 这里守的是三件事:
|
|
18
18
|
// 1. 注入出去的块里**真的**有项目名/路径/计数与真相源文件路径 ——
|
|
19
19
|
// 否则"Agent 知道我有哪些项目"就是空话(旧行为:prompt 里只有用户原话);
|
|
20
|
-
// 2.
|
|
20
|
+
// 2. 计数口径与看板列一致 —— 而且**数据是从 aiContext 的 tasks 板块来的**
|
|
21
|
+
// (不再有第二份 buildProjectEntries + summarizeProjectTasks 实现);
|
|
22
|
+
// 3. 快照取不到时**降级但不丢关键信息**:项目清单可以没有,
|
|
23
|
+
// 但"克隆优先 SSH"这条偏好必须还在。
|
|
24
|
+
//
|
|
25
|
+
// ⚠️ board 一律用**真实的** collectWorkbenchTasks 产出,不手搓:
|
|
26
|
+
// 手搓的话,第 2 条就变成了"测试自己跟自己对",把口径分叉放走。
|
|
21
27
|
|
|
22
28
|
import { test } from 'node:test';
|
|
23
29
|
import assert from 'node:assert/strict';
|
|
24
30
|
|
|
25
31
|
import { buildEnvContextBlock, ENV_CONTEXT_MAX_PROJECTS } from './envContext.js';
|
|
26
|
-
import {
|
|
32
|
+
import { TASK_COLUMNS } from './projectRegistry.js';
|
|
33
|
+
import { TRUTH_FILES } from './shared.js';
|
|
34
|
+
import { collectWorkbenchTasks } from '../aiContext/collectors.js';
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
/** 用真实的 collector 造 board —— envContext 在生产里吃到的就是它 */
|
|
37
|
+
const boardOf = ({ tasks = [], jobs = [], recentDirs = [] } = {}) => {
|
|
38
|
+
const r = collectWorkbenchTasks({ tasks, jobs, recentDirs });
|
|
39
|
+
assert.equal(r.ok, true, 'collectWorkbenchTasks 应当成功');
|
|
40
|
+
assert.ok(r.data, 'tasks 板块必须带上结构化 data(envContext 的唯一数据来源)');
|
|
41
|
+
return r.data;
|
|
42
|
+
};
|
|
34
43
|
|
|
35
44
|
const job = (id, taskId, status) => ({
|
|
36
45
|
id, taskId, subId: `${taskId}__x`, title: '', status,
|
|
@@ -38,39 +47,41 @@ const job = (id, taskId, status) => ({
|
|
|
38
47
|
endedAt: status === 'running' ? null : '2026-09-18T02:01:00.000Z',
|
|
39
48
|
});
|
|
40
49
|
|
|
41
|
-
const
|
|
42
|
-
tasksFile: '/home/u/.zen-gitsync/tasks.json',
|
|
43
|
-
jobsFile: '/home/u/.zen-gitsync/jobs.json',
|
|
44
|
-
orchestratorFile: '/home/u/.zen-gitsync/orchestrator.json',
|
|
45
|
-
configFile: '/home/u/.zen-gitsync/config.json',
|
|
46
|
-
};
|
|
50
|
+
const build = (opts) => buildEnvContextBlock({ truthFiles: TRUTH_FILES, ...opts });
|
|
47
51
|
|
|
48
|
-
test('
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
test('取不到 board 时**不返回 null**:偏好与真相源照给(项目清单整体省掉)', () => {
|
|
53
|
+
// 这一条是刻意的行为变更。老版本"没有项目就返回 null",但块里那两条**不需要取数**
|
|
54
|
+
// 的内容(克隆优先 SSH、真相源路径)恰恰是最不该丢的 —— SSH 那条少一次,
|
|
55
|
+
// 任务克隆仓库时就会退回 https、弹出凭据窗口把自己停在半路。
|
|
56
|
+
for (const board of [null, undefined, {}, { projects: [] }, { projects: [{ name: 'x' }] }]) {
|
|
57
|
+
const block = build({ currentProjectPath: 'D:\\ws\\a', board });
|
|
58
|
+
assert.ok(block, 'board 缺失也必须产出块');
|
|
59
|
+
assert.match(block, /优先使用 SSH/);
|
|
60
|
+
for (const p of Object.values(TRUTH_FILES)) assert.ok(block.includes(p), `缺少路径 ${p}`);
|
|
61
|
+
// 但项目那段不能硬编一个空清单(模型会把"没列出来"读成"没有项目")
|
|
62
|
+
assert.doesNotMatch(block, /项目清单(共 0 个/);
|
|
63
|
+
if (!board || !board.projects || board.projects.length === 0) {
|
|
64
|
+
assert.match(block, /项目清单:本次未能取到/);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
54
67
|
});
|
|
55
68
|
|
|
56
69
|
test('项目清单带名称、路径与三列计数,并标出当前项目', () => {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const block = buildEnvContextBlock({
|
|
69
|
-
currentProjectPath: 'D:\\ws\\zen-gitsync',
|
|
70
|
-
projects, tasks, jobs, ...PATHS,
|
|
70
|
+
const board = boardOf({
|
|
71
|
+
recentDirs: ['D:\\ws\\zen-gitsync', 'D:\\ws\\article-generator'],
|
|
72
|
+
tasks: [
|
|
73
|
+
// 已完成:simple 任务跑过一次且 job 是 done
|
|
74
|
+
{ id: 't1', projectPath: 'D:\\ws\\zen-gitsync', type: 'simple' },
|
|
75
|
+
// 进行中:有 job 处于 running
|
|
76
|
+
{ id: 't2', projectPath: 'D:\\ws\\article-generator', type: 'simple' },
|
|
77
|
+
// 待处理:没子任务也没执行过
|
|
78
|
+
{ id: 't3', projectPath: 'D:\\ws\\article-generator', type: 'simple' },
|
|
79
|
+
],
|
|
80
|
+
jobs: [job('j1', 't1', 'done'), job('j2', 't2', 'running')],
|
|
71
81
|
});
|
|
72
82
|
|
|
73
|
-
|
|
83
|
+
const block = build({ currentProjectPath: 'D:\\ws\\zen-gitsync', board });
|
|
84
|
+
|
|
74
85
|
// 项目名 + 路径都在
|
|
75
86
|
assert.match(block, /zen-gitsync/);
|
|
76
87
|
assert.match(block, /article-generator/);
|
|
@@ -78,10 +89,11 @@ test('项目清单带名称、路径与三列计数,并标出当前项目', ()
|
|
|
78
89
|
// 三列计数:zen-gitsync 1 条已完成 → 0/0/1;article-generator 1 进行中 1 待处理 → 1/1/0
|
|
79
90
|
assert.match(block, /- zen-gitsync \| D:\\ws\\zen-gitsync \| 0\/0\/1\s+← 当前/);
|
|
80
91
|
assert.match(block, /- article-generator \| D:\\ws\\article-generator \| 1\/1\/0/);
|
|
81
|
-
// 合计:3
|
|
92
|
+
// 合计:3 条,各列各一条;顺带带上"正在执行 1 个"
|
|
82
93
|
assert.match(block, /全部项目合计 3 条 —— 待处理 1 \/ 进行中 1 \/ 已完成 1/);
|
|
94
|
+
assert.match(block, /正在执行 1 个/);
|
|
83
95
|
// 真相源路径:这是"够得着"的关键,缺了 Agent 还是只能猜
|
|
84
|
-
for (const p of Object.values(
|
|
96
|
+
for (const p of Object.values(TRUTH_FILES)) assert.ok(block.includes(p), `缺少路径 ${p}`);
|
|
85
97
|
// 明确要求它去读文件,而不是回"我看不到"
|
|
86
98
|
assert.match(block, /不要回答"我看不到\/无法访问"/);
|
|
87
99
|
});
|
|
@@ -89,29 +101,31 @@ test('项目清单带名称、路径与三列计数,并标出当前项目', ()
|
|
|
89
101
|
test('当前项目标记必须归一化后比较(大小写不同也算同一个)', () => {
|
|
90
102
|
// 条目 key 是归一化过的(小写 + 反斜杠),而 cwd 常常是另一种写法 ——
|
|
91
103
|
// 直接拿 path 跟 key 比会永远不相等,表现为"当前项目"标记丢失。
|
|
92
|
-
const
|
|
93
|
-
|
|
104
|
+
const board = boardOf({
|
|
105
|
+
recentDirs: ['C:\\Users\\xuze3', 'D:\\ws\\other'],
|
|
106
|
+
tasks: [],
|
|
107
|
+
jobs: [],
|
|
108
|
+
});
|
|
109
|
+
const block = build({
|
|
94
110
|
// 不只盘符:目录段大小写也不一样(左栏重复项那个 bug 的同款写法差异)
|
|
95
111
|
currentProjectPath: 'c:/users/XUZE3',
|
|
96
|
-
|
|
112
|
+
board,
|
|
97
113
|
});
|
|
98
114
|
assert.match(block, /- xuze3 \| C:\\Users\\xuze3 \| 0\/0\/0\s+← 当前/);
|
|
99
115
|
assert.match(block, /- other \| D:\\ws\\other \| 0\/0\/0\n/);
|
|
100
116
|
});
|
|
101
117
|
|
|
102
118
|
test('项目数超过上限时只列前 N 个,但合计仍按全量算', () => {
|
|
103
|
-
const projects = [];
|
|
104
|
-
const tasks = [];
|
|
105
119
|
const total = ENV_CONTEXT_MAX_PROJECTS + 5;
|
|
120
|
+
const recentDirs = [];
|
|
121
|
+
const tasks = [];
|
|
106
122
|
for (let i = 0; i < total; i += 1) {
|
|
107
123
|
const p = `D:\\ws\\p${i}`;
|
|
108
|
-
|
|
124
|
+
recentDirs.push(p);
|
|
109
125
|
tasks.push({ id: `t${i}`, projectPath: p, type: 'simple' });
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
const block =
|
|
113
|
-
currentProjectPath: '', projects, tasks, jobs: [], ...PATHS,
|
|
114
|
-
});
|
|
128
|
+
const block = build({ currentProjectPath: '', board: boardOf({ recentDirs, tasks }) });
|
|
115
129
|
|
|
116
130
|
const listed = block.split('\n').filter(l => l.startsWith('- p') && l.includes('| D:'));
|
|
117
131
|
assert.equal(listed.length, ENV_CONTEXT_MAX_PROJECTS);
|
|
@@ -122,21 +136,23 @@ test('项目数超过上限时只列前 N 个,但合计仍按全量算', () =>
|
|
|
122
136
|
});
|
|
123
137
|
|
|
124
138
|
test('maxProjects 可覆盖,且非法值回落到默认上限', () => {
|
|
125
|
-
const
|
|
126
|
-
const short =
|
|
139
|
+
const board = boardOf({ recentDirs: ['D:\\ws\\a', 'D:\\ws\\b', 'D:\\ws\\c'], tasks: [] });
|
|
140
|
+
const short = build({ board, maxProjects: 1 });
|
|
127
141
|
assert.equal(short.split('\n').filter(l => l.startsWith('- a') || l.startsWith('- b') || l.startsWith('- c')).length, 1);
|
|
128
142
|
|
|
129
|
-
const bad =
|
|
143
|
+
const bad = build({ board, maxProjects: 0 });
|
|
130
144
|
assert.equal(bad.split('\n').filter(l => l.startsWith('- ') && l.includes('| D:')).length, 3);
|
|
131
145
|
});
|
|
132
146
|
|
|
133
147
|
test('没关联项目的任务不计入项目行,但计入合计', () => {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
const board = boardOf({
|
|
149
|
+
recentDirs: ['D:\\ws\\a'],
|
|
150
|
+
tasks: [
|
|
151
|
+
{ id: 't1', projectPath: 'D:\\ws\\a', type: 'simple' },
|
|
152
|
+
{ id: 't2', projectPath: '', type: 'simple' },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
const block = build({ board });
|
|
140
156
|
// 项目行只认 key 匹配得上的那条(t1)→ 待处理 1
|
|
141
157
|
assert.match(block, /- a \| D:\\ws\\a \| 1\/0\/0/);
|
|
142
158
|
// 而合计走 NO_PROJECT_KEY 那桶一起算 → 2 条;两者不相等正是这段断言要守的
|
|
@@ -151,12 +167,14 @@ test('没关联项目的任务不计入项目行,但计入合计', () => {
|
|
|
151
167
|
// 只顺手改了最显眼的一行 → 另外三处照红。下面这条不比对具体数字,只守"段数 == 看板列数",
|
|
152
168
|
// 下次增删列时它和上面几条会一起红,而不是让某一条孤零零地报一个对不上的字符串。
|
|
153
169
|
test('项目行/合计的段数必须与 TASK_COLUMNS 一致,且每列都有中文标签', () => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
const board = boardOf({
|
|
171
|
+
recentDirs: ['D:\\ws\\a'],
|
|
172
|
+
tasks: [
|
|
173
|
+
{ id: 't1', projectPath: 'D:\\ws\\a', type: 'simple' },
|
|
174
|
+
{ id: 't2', projectPath: 'D:\\ws\\a', type: 'simple' },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
const block = build({ board });
|
|
160
178
|
|
|
161
179
|
// 表头里的「待处理/进行中/已完成」:段数 = 列数,且不能有一列回落成 key 本身
|
|
162
180
|
const headerLabels = block.match(/格式: 名称 \| 路径 \| ([^)]+)/)[1].split('/');
|
|
@@ -177,16 +195,68 @@ test('项目行/合计的段数必须与 TASK_COLUMNS 一致,且每列都有
|
|
|
177
195
|
assert.equal(rowCounts[TASK_COLUMNS.indexOf('todo')], '2');
|
|
178
196
|
});
|
|
179
197
|
|
|
198
|
+
// ── 口径同源:这一条守的是"别再长出第二份实现"────────────────────────
|
|
199
|
+
//
|
|
200
|
+
// 改动前 envContext 自己读 tasks.json、自己做 buildProjectEntries + summarizeProjectTasks,
|
|
201
|
+
// 与 aiContext 的 tasks 板块是同一口径的两个副本。两份实现分叉时**不会报错**,
|
|
202
|
+
// 只会让 Agent 在编排台里和智能体页里看到不同的项目数。这条断言把两份绑在一起:
|
|
203
|
+
// envContext 渲染出来的每个数字,都必须能在 collector 的摘要里找到。
|
|
204
|
+
test('口径同源:envContext 的数字必须与 tasks 板块的摘要逐项对上', () => {
|
|
205
|
+
const tasks = [
|
|
206
|
+
{ id: 't1', projectPath: 'D:\\ws\\a', type: 'simple' },
|
|
207
|
+
{ id: 't2', projectPath: 'D:\\ws\\a', type: 'simple' },
|
|
208
|
+
{ id: 't3', projectPath: 'D:\\ws\\b', type: 'simple' },
|
|
209
|
+
{ id: 't4', projectPath: '', type: 'simple' },
|
|
210
|
+
];
|
|
211
|
+
const jobs = [job('j1', 't3', 'done')];
|
|
212
|
+
const section = collectWorkbenchTasks({ tasks, jobs, recentDirs: ['D:\\ws\\a', 'D:\\ws\\b'] });
|
|
213
|
+
const block = buildEnvContextBlock({
|
|
214
|
+
currentProjectPath: 'D:\\ws\\b',
|
|
215
|
+
board: section.data,
|
|
216
|
+
truthFiles: TRUTH_FILES,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ① 合计那一句:collector 的 summary 里是「全部项目合计 4 条 —— 待处理 3 / 进行中 0 / 已完成 1」,
|
|
220
|
+
// envContext 必须复现出同样的数字(前缀文案相同,直接整句比对最省事)。
|
|
221
|
+
const totals = section.summary.match(/全部项目合计 \d+ 条 —— [^,]+/)[0];
|
|
222
|
+
assert.ok(block.includes(totals), `envContext 的合计与板块摘要不一致:\n${block}`);
|
|
223
|
+
|
|
224
|
+
// ② 项目数:摘要里的项目行数与 envContext 列出的行数一致
|
|
225
|
+
const collectorRows = section.lines.filter(l => /^- .+ \| .+ \| \d+\/\d+\/\d+$/.test(l));
|
|
226
|
+
const envRows = block.split('\n').filter(l => /^- .+ \| .+ \| \d+\/\d+\/\d+/.test(l));
|
|
227
|
+
assert.equal(envRows.length, collectorRows.length, '两份视图列出的项目数应一致');
|
|
228
|
+
assert.ok(collectorRows.length > 0, '这个用例应当至少有一个项目');
|
|
229
|
+
|
|
230
|
+
// ③ 结构化 data 自己也得自洽:statsByKey 的合计 == total
|
|
231
|
+
const sum = { total: 0 };
|
|
232
|
+
for (const k of TASK_COLUMNS) sum[k] = 0;
|
|
233
|
+
for (const s of Object.values(section.data.statsByKey)) {
|
|
234
|
+
sum.total += s.total || 0;
|
|
235
|
+
for (const k of TASK_COLUMNS) sum[k] += s[k] || 0;
|
|
236
|
+
}
|
|
237
|
+
assert.equal(sum.total, section.data.total.total);
|
|
238
|
+
for (const k of TASK_COLUMNS) assert.equal(sum[k], section.data.total[k]);
|
|
239
|
+
});
|
|
240
|
+
|
|
180
241
|
// ── 用户偏好:克隆仓库优先 SSH ────────────────────────────────────────
|
|
181
242
|
//
|
|
182
243
|
// 这条只对**工作台任务**起作用(执行器是外部 CLI claude / opencode,系统提示词不归我们写,
|
|
183
244
|
// envContext 是唯一能把偏好递进去的地方)。断言钉住两个平台的地址都写全 —— 用户实际踩的
|
|
184
245
|
// 那个坑就是 Gitee 走 https 弹了凭据窗口;只留 GitHub 一条等于只覆盖一半场景。
|
|
185
246
|
test('注入块带上「克隆优先 SSH」这条用户偏好', () => {
|
|
186
|
-
const block =
|
|
247
|
+
const block = build({ board: boardOf({ recentDirs: ['D:\\ws\\a'], tasks: [] }) });
|
|
187
248
|
assert.match(block, /优先使用 SSH/);
|
|
188
249
|
assert.match(block, /git@github\.com:owner\/repo\.git/);
|
|
189
250
|
assert.match(block, /git@gitee\.com:owner\/repo\.git/);
|
|
190
251
|
// 退回条件也得在:没写清"什么时候可以退回",模型会在 SSH 失败后来回重试
|
|
191
252
|
assert.match(block, /Permission denied \(publickey\)/);
|
|
192
253
|
});
|
|
254
|
+
|
|
255
|
+
test('真相源清单来自唯一的 TRUTH_FILES(四个路径一个不落,且不多写)', () => {
|
|
256
|
+
const block = build({ board: boardOf({ recentDirs: ['D:\\ws\\a'], tasks: [] }) });
|
|
257
|
+
const listed = block.split('\n').filter(l => l.startsWith('- ') && l.includes(' —— ') && l.includes('json'));
|
|
258
|
+
assert.equal(listed.length, Object.keys(TRUTH_FILES).length);
|
|
259
|
+
// 编排台指令流水必须在这份清单里 —— 它是 envContext 相对七个板块视图**多**出来的那一条
|
|
260
|
+
assert.ok(block.includes(TRUTH_FILES.orchestratorFile));
|
|
261
|
+
assert.match(block, /调度台发过的指令流水/);
|
|
262
|
+
});
|