zen-gitsync 2.16.49 → 2.16.51
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 +8 -6
- package/package.json +5 -2
- package/src/ui/public/assets/AgentView-DcTGw8-j.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CA5-POGA.js +2 -0
- package/src/ui/public/assets/BranchSelector-qn-iGHOI.js +1 -0
- package/src/ui/public/assets/CommitForm-DHfyJ331.js +6 -0
- package/src/ui/public/assets/CommonDialog-DJBixJcI.js +1 -0
- package/src/ui/public/assets/EditorView-BRWoa7--.js +1 -0
- package/src/ui/public/assets/{FlowExecutionViewer-CuID17Kg.js → FlowExecutionViewer-YoPgld-R.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DyEjlCBW.js +21 -0
- package/src/ui/public/assets/LogList-_z4dIgvS.js +7 -0
- package/src/ui/public/assets/MindmapView-BrkcAcDh.js +2 -0
- package/src/ui/public/assets/MonitorView-8TG_K4m3.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BqLyrik7.js +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-LOVV6KrB.js +4 -0
- package/src/ui/public/assets/RemoteRepoCard-SQyKcXDG.js +7 -0
- package/src/ui/public/assets/SourceMapView-DckLynaL.js +3 -0
- package/src/ui/public/assets/{SvgIcon-koi3MuB8.js → SvgIcon-CvIX1KL7.js} +1 -1
- package/src/ui/public/assets/UserInputNode-Bc124-BK.js +1 -0
- package/src/ui/public/assets/WorkbenchView-BtQkd9Oy.js +37 -0
- package/src/ui/public/assets/WorkbenchView-FdhmMI-b.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-VkarRGOe.js → _plugin-vue_export-helper-N7dqbp6x.js} +5 -5
- package/src/ui/public/assets/configStore-NaVpBxPo.js +1 -0
- package/src/ui/public/assets/{element-plus-C3mePHEv.js → element-plus-Csu3CJKj.js} +3 -3
- package/src/ui/public/assets/{flow-mindmap-BzX-PWap.js → flow-mindmap-wqtwJcDW.js} +12 -12
- package/src/ui/public/assets/index-Ci3lbjk2.css +1 -0
- package/src/ui/public/assets/{index-Cn4CA40E.js → index-D9u0mCSi.js} +49 -48
- package/src/ui/public/assets/{office-docx-pY9m7PMg.js → office-docx-BKrAsewH.js} +6 -6
- package/src/ui/public/assets/{office-excel-F21cTf8V.js → office-excel-5lGzd5Yy.js} +1 -1
- package/src/ui/public/assets/{office-pptx-XHr0prIp.js → office-pptx-DKtkU_7r.js} +1 -1
- package/src/ui/public/assets/toolsStore-CNDLLgUG.js +1 -0
- package/src/ui/public/assets/{vendor-EBzJAuo1.js → vendor-C6ntdXkR.js} +43 -43
- package/src/ui/public/assets/{vendor-BkgN6wPK.css → vendor-_6pgW9Z2.css} +1 -1
- package/src/ui/public/assets/vue-flow-D7IxiTyP.js +39 -0
- package/src/ui/public/index.html +12 -12
- package/src/ui/server/routes/workbench/index.js +206 -5
- package/src/ui/server/routes/workbench/orchestratorStore.js +260 -0
- package/src/ui/server/routes/workbench/projectRegistry.js +385 -0
- package/src/ui/server/routes/workbench/projectRegistry.test.js +339 -0
- package/src/ui/server/routes/workbench/promptParts.js +51 -0
- package/src/ui/server/routes/workbench/promptParts.test.js +82 -0
- package/src/ui/server/routes/workbench/shared.js +5 -0
- package/src/ui/server/routes/workbench/taskRunner.js +3 -2
- package/src/ui/server/utils/directoryGitState.js +92 -7
- package/src/ui/server/utils/directoryGitState.test.js +135 -2
- package/src/ui/public/assets/AgentView-CZhZ-RtZ.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CGLBfOhF.js +0 -2
- package/src/ui/public/assets/BranchSelector-DO0xkvYF.js +0 -1
- package/src/ui/public/assets/CommitForm-ickhPe9o.js +0 -6
- package/src/ui/public/assets/CommonDialog-CliNZp_i.js +0 -1
- package/src/ui/public/assets/EditorView-Bj5Buc8r.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BWynSUhQ.js +0 -21
- package/src/ui/public/assets/LogList-C8fYaGlz.js +0 -7
- package/src/ui/public/assets/MindmapView-Co2ys9e3.js +0 -2
- package/src/ui/public/assets/MonitorView-BxFwNYjB.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DKspkGJC.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-CRq596ya.js +0 -4
- package/src/ui/public/assets/RemoteRepoCard-9_Y3iDT6.js +0 -7
- package/src/ui/public/assets/SourceMapView-CFv9JUGZ.js +0 -3
- package/src/ui/public/assets/UserInputNode-CUTCIcEc.js +0 -1
- package/src/ui/public/assets/WorkbenchView-Bp2YaSGS.css +0 -1
- package/src/ui/public/assets/WorkbenchView-COoRqTST.js +0 -28
- package/src/ui/public/assets/configStore-vksEBmTc.js +0 -1
- package/src/ui/public/assets/index-C3zoLat6.css +0 -1
- package/src/ui/public/assets/toolsStore-CHBnAWFM.js +0 -1
- package/src/ui/public/assets/vue-flow-DB6byJul.js +0 -39
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
// 项目登记表:把「常用/最近目录」和「任务里出现过的 projectPath」合并成一份项目清单,
|
|
16
|
+
// 并给每个项目挂上 Git 状态与任务统计,供多项目编排台(看板)使用。
|
|
17
|
+
//
|
|
18
|
+
// 为什么是「合并」而不是新建一张项目表:
|
|
19
|
+
// - 任务早就有 projectPath 字段(建任务时记下当时的项目),只是从来没有被当成一等实体;
|
|
20
|
+
// - 最近目录是应用已经在维护的项目来源,重复维护第二份清单必然对不上。
|
|
21
|
+
// 两者取并集去重后,"有任务但没进过最近目录" 和 "在最近目录但还没建任务" 的项目都能看到。
|
|
22
|
+
//
|
|
23
|
+
// 安全边界(沿用 /api/recent_directories/git-state 的口径):
|
|
24
|
+
// 这里要探测的路径只来自**服务端可信来源** —— configManager.getRecentDirectories()
|
|
25
|
+
// 与 tasks.json 里已有的 projectPath。本模块不接受任何客户端传入的路径,
|
|
26
|
+
// 因此不会被当成「任意路径 git 探测」的入口使用。
|
|
27
|
+
//
|
|
28
|
+
// 设计要点:
|
|
29
|
+
// - 路径归一与前端 utils/path.ts 的 canonicalProjectPath 必须**逐字一致**
|
|
30
|
+
// (盘符转大写 + Windows 斜杠归一);两边口径一旦分叉,
|
|
31
|
+
// 同一个目录会在两侧落到不同 key 上,表现为项目凭空分裂成两个。
|
|
32
|
+
// - 任务落在哪一列由 deriveTaskColumn 统一推导,是纯函数、单测覆盖;
|
|
33
|
+
// 前端不重复实现这套规则,避免两边口径漂移。
|
|
34
|
+
// - Git 状态复用 utils/directoryGitState.js 的批量探测(带并发上限 + TTL 缓存 + 超时)。
|
|
35
|
+
|
|
36
|
+
import { probeDirectoryGitStates } from '../../utils/directoryGitState.js';
|
|
37
|
+
|
|
38
|
+
/** 看板列,数组顺序即列顺序 */
|
|
39
|
+
export const TASK_COLUMNS = ['todo', 'doing', 'review', 'done'];
|
|
40
|
+
|
|
41
|
+
/** 没有关联任何项目的任务归到这个 key(与前端 useWorkbenchProjectGroups 一致) */
|
|
42
|
+
export const NO_PROJECT_KEY = '__no_project__';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 项目路径归一。与前端 src/ui/client/src/utils/path.ts 的 canonicalProjectPath 同口径:
|
|
46
|
+
* Windows 形式(带盘符)→ 盘符转大写 + 反斜杠归一;其余(POSIX)原样返回。
|
|
47
|
+
*
|
|
48
|
+
* 为什么必须归斜杠:常用目录允许用户手输,同一个目录会同时存在 `D:/ws/proj`
|
|
49
|
+
* 与 `D:\ws\proj` 两种写法。文件系统不区分,字符串比较却会 ——
|
|
50
|
+
* 于是项目列表里出现两个同名项目(实测 article-generator 就是如此)。
|
|
51
|
+
* 归一是修一个看得见的重复项,不是顺手清理。
|
|
52
|
+
*
|
|
53
|
+
* 刻意**不**做去尾斜杠 / 全小写 / 展开 `..`:那会把比较规则变得依赖文件系统语义,
|
|
54
|
+
* 而这里只承诺"同一目录的常见书写差异归一到同一个 key"。
|
|
55
|
+
*/
|
|
56
|
+
export function canonicalProjectPath(p) {
|
|
57
|
+
const s = String(p || '').trim();
|
|
58
|
+
if (!/^[a-zA-Z]:/.test(s)) return s;
|
|
59
|
+
return s.replace(/^([a-z])(?=:)/, (m) => m.toUpperCase()).replace(/\//g, '\\');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 目录名(项目显示名),路径为空时返回空串而不是兜底文案(文案归前端 i18n 管) */
|
|
63
|
+
export function projectName(p) {
|
|
64
|
+
const parts = String(p || '').split(/[\\/]/).filter(Boolean);
|
|
65
|
+
return parts.length ? parts[parts.length - 1] : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 任务该在哪个目录里执行。纯函数,单测覆盖。
|
|
70
|
+
*
|
|
71
|
+
* 多项目编排下的关键一环:run* 系列路由过去一律拿 getCurrentProjectPath() 当工作区,
|
|
72
|
+
* 于是「在看板里执行 B 项目的任务」会跑到 A 项目的工作区里去改代码 —— 对多项目看板是致命的。
|
|
73
|
+
* 任务自创建起就记着自己的 projectPath,执行时以它为准;只有它为空
|
|
74
|
+
* (历史数据 / 未关联项目)才回退到调用方给的兜底路径(一般是当前项目)。
|
|
75
|
+
*
|
|
76
|
+
* 刻意**不**在任务目录不存在时回退当前项目:那等于悄悄把改动落到另一个仓库上,
|
|
77
|
+
* 比直接报错危险得多。目录不存在就让执行报错,用户能立刻看见。
|
|
78
|
+
*/
|
|
79
|
+
export function resolveTaskRepoPath(task, fallbackPath = '') {
|
|
80
|
+
const fromTask = task && typeof task.projectPath === 'string' ? task.projectPath.trim() : '';
|
|
81
|
+
if (fromTask) return fromTask;
|
|
82
|
+
return typeof fallbackPath === 'string' ? fallbackPath : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 把 jobs 快照按 taskId 分组,deriveTaskColumn / 统计都要用 */
|
|
86
|
+
export function groupJobsByTask(jobs) {
|
|
87
|
+
const map = new Map();
|
|
88
|
+
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
89
|
+
if (!j || !j.taskId) continue;
|
|
90
|
+
if (!map.has(j.taskId)) map.set(j.taskId, []);
|
|
91
|
+
map.get(j.taskId).push(j);
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** job 的时间戳:优先 startedAt,回落到 endedAt,都没有就空串(用于取"最后一条") */
|
|
97
|
+
function jobTimestamp(j) {
|
|
98
|
+
return String((j && (j.startedAt || j.endedAt)) || '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 取时间上最新的一条 job;时间戳都一样(老数据)时按原顺序取末条 */
|
|
102
|
+
export function latestJob(jobsForTask) {
|
|
103
|
+
const list = Array.isArray(jobsForTask) ? jobsForTask : [];
|
|
104
|
+
if (list.length === 0) return null;
|
|
105
|
+
let best = list[0];
|
|
106
|
+
for (const j of list) {
|
|
107
|
+
if (jobTimestamp(j) >= jobTimestamp(best)) best = j;
|
|
108
|
+
}
|
|
109
|
+
return best;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 把一个任务推导到看板的四列之一。纯函数,单测覆盖。
|
|
114
|
+
*
|
|
115
|
+
* 口径(按优先级,逐条短路):
|
|
116
|
+
* 1. 有 job 处于 running/pending,或子任务状态是 running → `doing`
|
|
117
|
+
* 2. 有子任务且全部 done → `done`
|
|
118
|
+
* 3. 有子任务但没全 done:
|
|
119
|
+
* - 动过(有 done 的子任务 / 执行过 / 有 error 的子任务)→ `review`(跑过但没收尾,等你确认或继续)
|
|
120
|
+
* - 完全没动过 → `todo`
|
|
121
|
+
* 4. 没子任务(简单任务,或还没拆分的复杂任务):
|
|
122
|
+
* - 从没执行过 → `todo`
|
|
123
|
+
* - 最近一条 job 是 done → `done`;其余终态(error/cancelled)→ `review`
|
|
124
|
+
*
|
|
125
|
+
* 之所以不把 error 单列一列:列是"任务处于哪个阶段",error 是"这一轮执行失败了",
|
|
126
|
+
* 失败的任务同样需要人看一眼,归到 `review`,具体错误在前端卡片上另有标记。
|
|
127
|
+
*/
|
|
128
|
+
export function deriveTaskColumn(task, jobsForTask = []) {
|
|
129
|
+
const subs = Array.isArray(task && task.subtasks) ? task.subtasks : [];
|
|
130
|
+
const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
|
|
131
|
+
|
|
132
|
+
if (jobs.some(j => j && (j.status === 'running' || j.status === 'pending'))) return 'doing';
|
|
133
|
+
if (subs.some(s => s && s.status === 'running')) return 'doing';
|
|
134
|
+
|
|
135
|
+
if (subs.length > 0) {
|
|
136
|
+
const doneCount = subs.filter(s => s && s.status === 'done').length;
|
|
137
|
+
if (doneCount === subs.length) return 'done';
|
|
138
|
+
const touched = doneCount > 0 || jobs.length > 0 || subs.some(s => s && s.status === 'error');
|
|
139
|
+
return touched ? 'review' : 'todo';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (jobs.length === 0) return 'todo';
|
|
143
|
+
const last = latestJob(jobs);
|
|
144
|
+
return last && last.status === 'done' ? 'done' : 'review';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 空统计,避免各处手写零值 */
|
|
148
|
+
function emptyStats() {
|
|
149
|
+
return {
|
|
150
|
+
total: 0, todo: 0, doing: 0, review: 0, done: 0,
|
|
151
|
+
progress: 0, runningJobs: 0, errorSubtasks: 0, lastActiveAt: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 取更新的时间戳(都是 ISO 字符串,直接字典序比较即可) */
|
|
156
|
+
function laterOf(a, b) {
|
|
157
|
+
if (!a) return b || null;
|
|
158
|
+
if (!b) return a;
|
|
159
|
+
return String(a) >= String(b) ? a : b;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 按项目 key 汇总任务统计。
|
|
164
|
+
* @returns {Map<string, object>} key 为 canonicalProjectPath(projectPath) || NO_PROJECT_KEY
|
|
165
|
+
*/
|
|
166
|
+
export function summarizeProjectTasks(tasks, jobs) {
|
|
167
|
+
const jobsByTask = groupJobsByTask(jobs);
|
|
168
|
+
const taskProject = new Map(); // taskId -> projectKey
|
|
169
|
+
const byKey = new Map();
|
|
170
|
+
|
|
171
|
+
for (const t of Array.isArray(tasks) ? tasks : []) {
|
|
172
|
+
if (!t) continue;
|
|
173
|
+
const key = canonicalProjectPath(t.projectPath) || NO_PROJECT_KEY;
|
|
174
|
+
taskProject.set(t.id, key);
|
|
175
|
+
if (!byKey.has(key)) byKey.set(key, emptyStats());
|
|
176
|
+
const agg = byKey.get(key);
|
|
177
|
+
const jobsForTask = jobsByTask.get(t.id) || [];
|
|
178
|
+
const column = deriveTaskColumn(t, jobsForTask);
|
|
179
|
+
agg.total += 1;
|
|
180
|
+
agg[column] += 1;
|
|
181
|
+
if (Array.isArray(t.subtasks)) {
|
|
182
|
+
agg.errorSubtasks += t.subtasks.filter(s => s && s.status === 'error').length;
|
|
183
|
+
}
|
|
184
|
+
agg.lastActiveAt = laterOf(agg.lastActiveAt, t.updatedAt || t.createdAt);
|
|
185
|
+
// 最后活跃时间也要把执行记录算进来:任务本身可能很久没改,但刚刚跑过
|
|
186
|
+
for (const j of jobsForTask) {
|
|
187
|
+
agg.lastActiveAt = laterOf(agg.lastActiveAt, j.endedAt || j.startedAt);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 活跃执行数按 job 数(不是任务数):一个任务并行跑 3 个子任务就是 3 个活跃执行
|
|
192
|
+
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
193
|
+
if (!j || (j.status !== 'running' && j.status !== 'pending')) continue;
|
|
194
|
+
const key = taskProject.get(j.taskId);
|
|
195
|
+
if (key && byKey.has(key)) byKey.get(key).runningJobs += 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const agg of byKey.values()) {
|
|
199
|
+
agg.progress = agg.total > 0 ? Math.round((agg.done / agg.total) * 100) : 0;
|
|
200
|
+
}
|
|
201
|
+
return byKey;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 合并最近目录与任务里的 projectPath,得到去重后的项目条目。
|
|
206
|
+
* path 取"首次出现的写法"(先最近目录、后任务),前端可以直接拿它跟其它列表对齐。
|
|
207
|
+
*
|
|
208
|
+
* @param {{ recentDirs?: string[], tasks?: object[] }} input
|
|
209
|
+
* @returns {Array<{ path: string, key: string, name: string, source: 'recent'|'task'|'both' }>}
|
|
210
|
+
*/
|
|
211
|
+
export function buildProjectEntries({ recentDirs = [], tasks = [] } = {}) {
|
|
212
|
+
const map = new Map();
|
|
213
|
+
for (const dir of recentDirs) {
|
|
214
|
+
const key = canonicalProjectPath(dir);
|
|
215
|
+
if (!key) continue;
|
|
216
|
+
map.set(key, { path: String(dir).trim(), key, inRecent: true, fromTasks: false });
|
|
217
|
+
}
|
|
218
|
+
for (const t of tasks) {
|
|
219
|
+
const key = canonicalProjectPath(t && t.projectPath);
|
|
220
|
+
if (!key) continue; // 没关联项目的任务不进项目清单
|
|
221
|
+
const hit = map.get(key);
|
|
222
|
+
if (hit) hit.fromTasks = true;
|
|
223
|
+
else map.set(key, { path: String(t.projectPath).trim(), key, inRecent: false, fromTasks: true });
|
|
224
|
+
}
|
|
225
|
+
return Array.from(map.values()).map(e => ({
|
|
226
|
+
path: e.path,
|
|
227
|
+
key: e.key,
|
|
228
|
+
name: projectName(e.path),
|
|
229
|
+
source: e.inRecent && e.fromTasks ? 'both' : (e.inRecent ? 'recent' : 'task'),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 从探测结果里摘出前端要用的 Git 字段(与原 git-state 接口返回口径一致) */
|
|
234
|
+
function pickGitState(state) {
|
|
235
|
+
if (!state) return null;
|
|
236
|
+
return {
|
|
237
|
+
isGitRepo: state.isGitRepo,
|
|
238
|
+
branch: state.branch || null,
|
|
239
|
+
upstream: state.upstream || null,
|
|
240
|
+
hasUpstream: !!state.hasUpstream,
|
|
241
|
+
detached: !!state.detached,
|
|
242
|
+
ahead: state.ahead || 0,
|
|
243
|
+
behind: state.behind || 0,
|
|
244
|
+
changed: state.changed || 0,
|
|
245
|
+
staged: state.staged || 0,
|
|
246
|
+
unstaged: state.unstaged || 0,
|
|
247
|
+
untracked: state.untracked || 0,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 项目清单(含 Git 状态 + 任务统计)。
|
|
253
|
+
*
|
|
254
|
+
* @param {object} input
|
|
255
|
+
* @param {string[]} input.recentDirs configManager.getRecentDirectories() 的结果
|
|
256
|
+
* @param {object[]} input.tasks tasks.json 里的任务
|
|
257
|
+
* @param {object[]} input.jobs jobStore.snapshotJobs() 的结果
|
|
258
|
+
* @param {string} [input.currentProjectPath] 应用当前所在项目(用于打 isCurrent 标记)
|
|
259
|
+
* @param {boolean} [input.withGit] 是否探测 Git 状态(默认 true)
|
|
260
|
+
* @param {object} [input.probeOptions] 透传给 probeDirectoryGitStates(超时 / TTL / 并发)
|
|
261
|
+
*/
|
|
262
|
+
export async function listProjects({
|
|
263
|
+
recentDirs = [],
|
|
264
|
+
tasks = [],
|
|
265
|
+
jobs = [],
|
|
266
|
+
currentProjectPath = '',
|
|
267
|
+
withGit = true,
|
|
268
|
+
probeOptions,
|
|
269
|
+
} = {}) {
|
|
270
|
+
const entries = buildProjectEntries({ recentDirs, tasks });
|
|
271
|
+
const stats = summarizeProjectTasks(tasks, jobs);
|
|
272
|
+
const currentKey = canonicalProjectPath(currentProjectPath);
|
|
273
|
+
|
|
274
|
+
let states = {};
|
|
275
|
+
if (withGit && entries.length > 0) {
|
|
276
|
+
states = await probeDirectoryGitStates(entries.map(e => e.path), probeOptions);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return entries.map(e => {
|
|
280
|
+
const state = withGit ? states[e.path] : null;
|
|
281
|
+
return {
|
|
282
|
+
path: e.path,
|
|
283
|
+
key: e.key,
|
|
284
|
+
name: e.name,
|
|
285
|
+
source: e.source,
|
|
286
|
+
isCurrent: !!currentKey && e.key === currentKey,
|
|
287
|
+
// exists:true|false|null —— null 表示没探到(withGit=false 或探测超时),
|
|
288
|
+
// 前端对 null 不显示任何"失效"标记,避免把未知谎报成目录不存在
|
|
289
|
+
exists: state ? state.exists : null,
|
|
290
|
+
git: pickGitState(state),
|
|
291
|
+
stats: stats.get(e.key) || emptyStats(),
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 看板任务卡需要的精简字段(完整任务体在 /api/workbench/tasks,这里只给卡片用得到的) */
|
|
297
|
+
export function decorateTaskForBoard(task, jobsForTask = []) {
|
|
298
|
+
const subs = Array.isArray(task && task.subtasks) ? task.subtasks : [];
|
|
299
|
+
const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
|
|
300
|
+
const last = latestJob(jobs);
|
|
301
|
+
return {
|
|
302
|
+
id: task.id,
|
|
303
|
+
title: task.title || '',
|
|
304
|
+
desc: task.desc || '',
|
|
305
|
+
type: task.type === 'simple' ? 'simple' : 'complex',
|
|
306
|
+
projectPath: task.projectPath || '',
|
|
307
|
+
column: deriveTaskColumn(task, jobs),
|
|
308
|
+
subtaskCount: subs.length,
|
|
309
|
+
subtaskDoneCount: subs.filter(s => s && s.status === 'done').length,
|
|
310
|
+
subtaskErrorCount: subs.filter(s => s && s.status === 'error').length,
|
|
311
|
+
attachmentCount: Array.isArray(task.attachments) ? task.attachments.length : 0,
|
|
312
|
+
runningJobs: jobs.filter(j => j && (j.status === 'running' || j.status === 'pending')).length,
|
|
313
|
+
lastJobStatus: last ? last.status : null,
|
|
314
|
+
createdAt: task.createdAt || null,
|
|
315
|
+
updatedAt: task.updatedAt || null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── 任务详情(点卡片弹窗用) ────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 弹窗里最多回多少字符的执行输出。
|
|
323
|
+
*
|
|
324
|
+
* 看板列表刻意只发摘要,因为它是 5s 轮询的:把每个任务的子任务描述、报错、
|
|
325
|
+
* 执行输出全塞进去,十几条任务每 5 秒推一遍纯属浪费(单条输出动辄几十万字符)。
|
|
326
|
+
* 详情改成按需取一次,并且**在服务端截尾**——弹窗要回答的是"最近一次执行干了什么、
|
|
327
|
+
* 卡在哪了",不是把整份日志搬进 DOM。想看全文有编辑器和 job 管理页。
|
|
328
|
+
*/
|
|
329
|
+
export const OUTPUT_TAIL_CHARS = 4000;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 把一条 job 压成弹窗够用的形状:丢掉 prompt/thinking(可能极大),
|
|
333
|
+
* output 只留尾部 OUTPUT_TAIL_CHARS 字符,并用 outputTruncated 明确告知被截过
|
|
334
|
+
* —— 前端据此提示"仅显示末尾",而不是让人以为整个输出就这么点。
|
|
335
|
+
*/
|
|
336
|
+
export function trimJobForDetail(job) {
|
|
337
|
+
if (!job) return null;
|
|
338
|
+
const out = typeof job.output === 'string' ? job.output : '';
|
|
339
|
+
const truncated = out.length > OUTPUT_TAIL_CHARS;
|
|
340
|
+
return {
|
|
341
|
+
id: job.id || '',
|
|
342
|
+
subId: job.subId || '',
|
|
343
|
+
title: job.title || '',
|
|
344
|
+
subTitle: job.subTitle || '',
|
|
345
|
+
status: job.status || '',
|
|
346
|
+
pid: typeof job.pid === 'number' ? job.pid : null,
|
|
347
|
+
startedAt: job.startedAt || null,
|
|
348
|
+
endedAt: job.endedAt || null,
|
|
349
|
+
exitCode: typeof job.exitCode === 'number' ? job.exitCode : null,
|
|
350
|
+
error: job.error || '',
|
|
351
|
+
outputTail: truncated ? out.slice(-OUTPUT_TAIL_CHARS) : out,
|
|
352
|
+
outputTruncated: truncated,
|
|
353
|
+
hasOutput: out.length > 0,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 组装单个任务的详情。纯函数,单测覆盖。
|
|
359
|
+
*
|
|
360
|
+
* @param {object} task 完整任务(含 subtasks / attachments)
|
|
361
|
+
* @param {Array} jobsForTask 属于该任务的 job(顺序不限,内部会按时间排)
|
|
362
|
+
* @param {number} recentJobLimit 最多回带几条 job 明细(默认 10)
|
|
363
|
+
* @returns {object|null} task 缺失时返回 null,由路由转成 404
|
|
364
|
+
*/
|
|
365
|
+
export function buildTaskDetail(task, jobsForTask = [], { recentJobLimit = 10 } = {}) {
|
|
366
|
+
if (!task || !task.id) return null;
|
|
367
|
+
const jobs = (Array.isArray(jobsForTask) ? jobsForTask : []).filter(Boolean);
|
|
368
|
+
|
|
369
|
+
// 按时间倒序:弹窗里第一条就是「最近一次执行」
|
|
370
|
+
const sorted = [...jobs].sort((a, b) => {
|
|
371
|
+
const ta = jobTimestamp(a);
|
|
372
|
+
const tb = jobTimestamp(b);
|
|
373
|
+
if (ta === tb) return 0;
|
|
374
|
+
return ta < tb ? 1 : -1;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const limit = Number.isFinite(recentJobLimit) && recentJobLimit > 0 ? Math.floor(recentJobLimit) : 10;
|
|
378
|
+
return {
|
|
379
|
+
task,
|
|
380
|
+
column: deriveTaskColumn(task, jobs),
|
|
381
|
+
lastJob: trimJobForDetail(sorted[0] || null),
|
|
382
|
+
recentJobs: sorted.slice(0, limit).map(trimJobForDetail),
|
|
383
|
+
jobCount: jobs.length,
|
|
384
|
+
};
|
|
385
|
+
}
|