codebee 0.1.54 → 0.1.56
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/CHANGELOG.md +13 -0
- package/README.md +3 -4
- package/app/core/builtin_agent.py +67 -26
- package/app/core/jobs.py +23 -4
- package/app/core/knowledge.py +26 -4
- package/app/core/modelhub.py +50 -5
- package/app/core/pipeline.py +156 -66
- package/app/core/planner.py +27 -13
- package/app/core/runner.py +398 -121
- package/app/core/store.py +64 -27
- package/app/core/zentao.py +39 -6
- package/app/main.py +43 -3
- package/app/ui/app.js +289 -56
- package/app/ui/board.css +616 -258
- package/app/ui/board.html +119 -41
- package/app/ui/board.js +60 -14
- package/app/ui/i18n.js +2699 -2659
- package/app/ui/index.html +2 -0
- package/app/ui/style.css +8 -6
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -817,6 +817,11 @@ async function api(path, opts) {
|
|
|
817
817
|
}
|
|
818
818
|
}
|
|
819
819
|
if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
820
|
+
const method = String(fetchOpts.method || "GET").toUpperCase();
|
|
821
|
+
if (method !== "GET") {
|
|
822
|
+
if (String(path).startsWith("/api/catalog/")) S._catalogLoaded = false;
|
|
823
|
+
if (String(path).startsWith("/api/models/")) S._modelsLoaded = false;
|
|
824
|
+
}
|
|
820
825
|
operationFinish(operationToken, "done");
|
|
821
826
|
return data;
|
|
822
827
|
} catch (e) {
|
|
@@ -931,13 +936,13 @@ function jsq(s) {
|
|
|
931
936
|
}
|
|
932
937
|
|
|
933
938
|
function statusChip(st) {
|
|
934
|
-
const zh = { queued: t("
|
|
939
|
+
const zh = { queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") };
|
|
935
940
|
return '<span class="chip ' + esc(st) + '">' + (zh[st] || esc(st)) + "</span>";
|
|
936
941
|
}
|
|
937
942
|
|
|
938
943
|
/* 运行状态文案(传 run 对象):退避窗口内的续跑副本写明「将于 HH:MM 自动
|
|
939
944
|
* 续跑」,别让 5 分钟等待看起来像卡死/资源排队(2026-09-18 重写任务误判案)。
|
|
940
|
-
*
|
|
945
|
+
* 到点后翻回「排队中」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
|
|
941
946
|
function runStatusText(run) {
|
|
942
947
|
const st = String((run && run.status) || "");
|
|
943
948
|
if (st === "queued" && run && run.resume_enqueue_at) {
|
|
@@ -945,7 +950,7 @@ function runStatusText(run) {
|
|
|
945
950
|
if (!isNaN(at) && Date.now() < at)
|
|
946
951
|
return t("将于 {0} 自动续跑", String(run.resume_enqueue_at).slice(11, 16));
|
|
947
952
|
}
|
|
948
|
-
return { queued: t("
|
|
953
|
+
return { queued: t("排队中"), running: t("运行中"), done: t("完成"),
|
|
949
954
|
failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") }[st] || st;
|
|
950
955
|
}
|
|
951
956
|
|
|
@@ -1183,7 +1188,46 @@ async function healthDisableProvider(pid) {
|
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1185
1190
|
async function refreshState() {
|
|
1186
|
-
applyState(await api("/api/state"));
|
|
1191
|
+
applyState(await api("/api/state", { timeout: 15000, busy: false, operation: false }));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
let _lookupRefresh = null;
|
|
1195
|
+
async function refreshPollLookups(force) {
|
|
1196
|
+
if (_lookupRefresh) return _lookupRefresh;
|
|
1197
|
+
const now = Date.now();
|
|
1198
|
+
const loadCatalog = !!force || !S._catalogLoaded || now - (S._catalogAt || 0) >= 60000;
|
|
1199
|
+
const loadModels = !!force || !S._modelsLoaded || now - (S._modelsAt || 0) >= 30000;
|
|
1200
|
+
if (!loadCatalog && !loadModels) return;
|
|
1201
|
+
if (loadCatalog) S._catalogAt = now;
|
|
1202
|
+
if (loadModels) S._modelsAt = now;
|
|
1203
|
+
_lookupRefresh = (async () => {
|
|
1204
|
+
try {
|
|
1205
|
+
const [cat, models] = await Promise.all([
|
|
1206
|
+
loadCatalog ? api("/api/catalog", { timeout: 15000 }) : Promise.resolve(null),
|
|
1207
|
+
loadModels ? api("/api/models", { timeout: 15000 }) : Promise.resolve(null),
|
|
1208
|
+
]);
|
|
1209
|
+
if (cat) {
|
|
1210
|
+
S.catalog = cat.catalog;
|
|
1211
|
+
S.catalogChecking = !!cat.checking;
|
|
1212
|
+
S._catalogLoaded = true;
|
|
1213
|
+
}
|
|
1214
|
+
if (models) {
|
|
1215
|
+
S.providers = models.providers; S.bindings = models.bindings;
|
|
1216
|
+
S.modelCatalog = models.catalog || [];
|
|
1217
|
+
S.sourceNames = models.source_names || S.sourceNames || {};
|
|
1218
|
+
S._modelsLoaded = true;
|
|
1219
|
+
renderDirectModelPicker();
|
|
1220
|
+
const provSig = JSON.stringify((S.providers || []).map((p) => [p.id, p.enabled, !!p.api_key]));
|
|
1221
|
+
if (provSig !== S.provSig) { S.provSig = provSig; loadOrchestrator(); }
|
|
1222
|
+
}
|
|
1223
|
+
render();
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
if (loadCatalog) S._catalogAt = 0;
|
|
1226
|
+
if (loadModels) S._modelsAt = 0;
|
|
1227
|
+
}
|
|
1228
|
+
})();
|
|
1229
|
+
try { await _lookupRefresh; }
|
|
1230
|
+
finally { _lookupRefresh = null; }
|
|
1187
1231
|
}
|
|
1188
1232
|
|
|
1189
1233
|
let _pollBusy = false;
|
|
@@ -1195,22 +1239,8 @@ async function poll() {
|
|
|
1195
1239
|
if (_pollBusy) return;
|
|
1196
1240
|
_pollBusy = true;
|
|
1197
1241
|
try {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
api("/api/models", { timeout: 15000 }),
|
|
1201
|
-
]);
|
|
1202
|
-
S.catalog = cat.catalog;
|
|
1203
|
-
S.catalogChecking = !!cat.checking;
|
|
1204
|
-
S.providers = models.providers; S.bindings = models.bindings;
|
|
1205
|
-
S.modelCatalog = models.catalog || [];
|
|
1206
|
-
S.sourceNames = models.source_names || S.sourceNames || {};
|
|
1207
|
-
// /api/models 到手即重建对话模型选项(无条件):旧逻辑只在「更多选项」
|
|
1208
|
-
// 旧块可见时重建——旧块隐藏后选项永远只有「自动推荐」,厂商写值全部
|
|
1209
|
-
// 静默失效(2026-09-21 用户实测「没法选模型」根因)
|
|
1210
|
-
renderDirectModelPicker();
|
|
1211
|
-
// 供应商集合/启停/密钥变了才重拉编排者:否则「生效中」状态点会在停用厂商后失真
|
|
1212
|
-
const provSig = JSON.stringify((S.providers || []).map((p) => [p.id, p.enabled, !!p.api_key]));
|
|
1213
|
-
if (provSig !== S.provSig) { S.provSig = provSig; loadOrchestrator(); }
|
|
1242
|
+
// 状态先独立拉取并渲染;目录/模型是低频数据,后台刷新不能挡住任务树和点击。
|
|
1243
|
+
if (!document.hidden) refreshPollLookups(false);
|
|
1214
1244
|
if (!S.sseLive) await refreshState();
|
|
1215
1245
|
else if (S._lastSseAt && Date.now() - S._lastSseAt > 60000 && !document.hidden) {
|
|
1216
1246
|
// SSE 静默看门狗:连接半死时 EventSource 不报错、sseLive 恒真、上面
|
|
@@ -1234,7 +1264,7 @@ async function poll() {
|
|
|
1234
1264
|
|
|
1235
1265
|
function schedulePolling() {
|
|
1236
1266
|
clearInterval(S.pollTimer);
|
|
1237
|
-
S.pollTimer = setInterval(poll, S.sseLive ? 8000 : 2000);
|
|
1267
|
+
S.pollTimer = setInterval(() => { if (!document.hidden) poll(); }, S.sseLive ? 8000 : 2000);
|
|
1238
1268
|
}
|
|
1239
1269
|
|
|
1240
1270
|
/* SSE:状态变化即时推送;断开自动重连,重连失败降级为 2s 轮询 */
|
|
@@ -2817,12 +2847,16 @@ function showResumeHint() {
|
|
|
2817
2847
|
|
|
2818
2848
|
async function archiveTask(id, archived) {
|
|
2819
2849
|
try {
|
|
2850
|
+
let restored = null;
|
|
2851
|
+
if (!archived && archivedTaskIds().has(id)) {
|
|
2852
|
+
restored = await loadTaskForPrefill(id);
|
|
2853
|
+
}
|
|
2820
2854
|
await api("/api/tasks/" + encodeURIComponent(id) + "/archive",
|
|
2821
2855
|
{ method: "POST", body: JSON.stringify({ archived: !!archived }) });
|
|
2822
2856
|
// 取消归档时把任务工作目录从「已移除目录」里捞回来:用户点时钟开关找回
|
|
2823
2857
|
// 归档任务再取消归档,直觉上文件夹就该回侧栏(否则只能靠在该目录新建任务)。
|
|
2824
2858
|
if (!archived) {
|
|
2825
|
-
const t0 = ((S.state || {}).tasks || []).concat(((S.state || {}).archived_tasks) || [])
|
|
2859
|
+
const t0 = restored || ((S.state || {}).tasks || []).concat(((S.state || {}).archived_tasks) || [])
|
|
2826
2860
|
.find((x) => x.id === id);
|
|
2827
2861
|
if (t0 && t0.workdir) unhideSideDir(t0.workdir);
|
|
2828
2862
|
}
|
|
@@ -2835,9 +2869,11 @@ async function archiveTask(id, archived) {
|
|
|
2835
2869
|
const i0 = from.findIndex((x) => x.id === id);
|
|
2836
2870
|
if (i0 >= 0) {
|
|
2837
2871
|
const t0 = from.splice(i0, 1)[0];
|
|
2838
|
-
|
|
2872
|
+
const moved = restored || t0;
|
|
2873
|
+
moved.archived = !!archived;
|
|
2874
|
+
if (!archived && restored) _taskDetailCache.set(id, restored);
|
|
2839
2875
|
const to = archived ? (st.archived_tasks = st.archived_tasks || []) : (st.tasks = st.tasks || []);
|
|
2840
|
-
to.push(
|
|
2876
|
+
to.push(moved);
|
|
2841
2877
|
}
|
|
2842
2878
|
S.sideSig = "";
|
|
2843
2879
|
}
|
|
@@ -2875,6 +2911,9 @@ async function deleteTask(id) {
|
|
|
2875
2911
|
if (goneRuns.size) st.runs = (st.runs || []).filter((r) => !goneRuns.has(r.id));
|
|
2876
2912
|
S.sideSig = ""; // 任务条目少了签名理应变化;显式清掉,不赌签名算法
|
|
2877
2913
|
}
|
|
2914
|
+
_taskDetailCache.delete(id);
|
|
2915
|
+
_taskRunsCache.delete(id);
|
|
2916
|
+
_taskRunsAt.delete(id);
|
|
2878
2917
|
render();
|
|
2879
2918
|
poll();
|
|
2880
2919
|
if (gone) toast(t("任务已删除"));
|
|
@@ -2949,8 +2988,11 @@ function taskContextItems(det) {
|
|
|
2949
2988
|
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2950
2989
|
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2951
2990
|
const st = det.dataset.status || "";
|
|
2952
|
-
|
|
2953
|
-
|
|
2991
|
+
const latestTaskRun = ((S.state || {}).task_latest || {})[taskId] || {};
|
|
2992
|
+
const retryable = runNeedsRetry(latestTaskRun) ||
|
|
2993
|
+
st === "failed" || st === "cancelled" || st === "timeout";
|
|
2994
|
+
if (retryable) items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2995
|
+
items.push({ label: retryable ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2954
2996
|
fn: () => newFromTask(taskId) });
|
|
2955
2997
|
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2956
2998
|
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
@@ -3195,10 +3237,44 @@ window.continueSerial = continueSerial;
|
|
|
3195
3237
|
/* 基于此任务新建(通用,不限连载):把旧任务的类型/目标/目录/评审设置预填进
|
|
3196
3238
|
* 新建表单,确认或修改后提交——「接着写下一批章节」请用连载任务的「继续连载」,
|
|
3197
3239
|
* 那里才带章节衔接;这里开的是一个全新任务。 */
|
|
3240
|
+
const _taskDetailCache = new Map();
|
|
3241
|
+
const _taskDetailInflight = new Map();
|
|
3242
|
+
const _taskRunsCache = new Map();
|
|
3243
|
+
const _taskRunsInflight = new Map();
|
|
3244
|
+
const _taskRunsAt = new Map();
|
|
3245
|
+
const _runMessagesCache = new Map();
|
|
3246
|
+
const _runMessagesInflight = new Map();
|
|
3247
|
+
|
|
3248
|
+
async function loadTaskForPrefill(id) {
|
|
3249
|
+
const active = ((S.state || {}).tasks || []).find((x) => x.id === id);
|
|
3250
|
+
if (active) return active;
|
|
3251
|
+
const cached = _taskDetailCache.get(id);
|
|
3252
|
+
if (cached) return cached;
|
|
3253
|
+
if (_taskDetailInflight.has(id)) return _taskDetailInflight.get(id);
|
|
3254
|
+
const request = api("/api/tasks/" + encodeURIComponent(id) + "/detail",
|
|
3255
|
+
{ timeout: 15000, busy: true, operation: "读取任务详情" })
|
|
3256
|
+
.then((data) => {
|
|
3257
|
+
if (!data || !data.task) throw new Error(t("任务不存在或已删除"));
|
|
3258
|
+
_taskDetailCache.set(id, data.task);
|
|
3259
|
+
return data.task;
|
|
3260
|
+
})
|
|
3261
|
+
.finally(() => _taskDetailInflight.delete(id));
|
|
3262
|
+
_taskDetailInflight.set(id, request);
|
|
3263
|
+
return request;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3198
3266
|
function newFromTask(id) {
|
|
3267
|
+
newFromTaskAsync(id);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
async function newFromTaskAsync(id) {
|
|
3199
3271
|
// 局部变量不能叫 t:会遮蔽 i18n 函数 t(),下面的文案调用会直接 TypeError
|
|
3200
|
-
|
|
3201
|
-
|
|
3272
|
+
let tk;
|
|
3273
|
+
try {
|
|
3274
|
+
const inState = ((S.state || {}).tasks || []).some((x) => x.id === id);
|
|
3275
|
+
if (!inState && !_taskDetailCache.has(id)) toast(t("正在读取任务详情…"));
|
|
3276
|
+
tk = await loadTaskForPrefill(id);
|
|
3277
|
+
} catch (e) { toast(t("读取任务详情失败:") + e.message, true); return; }
|
|
3202
3278
|
exitSettings(); // 回到新建任务表单
|
|
3203
3279
|
const flow = flowById(tk.type);
|
|
3204
3280
|
if (flow && $("f-type").value !== tk.type) {
|
|
@@ -3446,7 +3522,7 @@ function renderRunList() {
|
|
|
3446
3522
|
const can = runDeletable(r);
|
|
3447
3523
|
// 行结构照参考站的「主副标题 + 右侧状态与相对时间」:标题与摘要竖排成一块,
|
|
3448
3524
|
// 状态点用侧栏同款字形(staskGlyph),时间用相对档(完整时间戳挪进悬停提示)
|
|
3449
|
-
const sub = String(r.summary || r.error || (r.
|
|
3525
|
+
const sub = String(r.summary || r.error || (r.step_count ? r.step_count + t(" 个步骤") : "")).trim();
|
|
3450
3526
|
return '<div class="item" onclick="openRun(\'' + esc(r.id) + '\')">' +
|
|
3451
3527
|
'<div class="t">' +
|
|
3452
3528
|
'<input type="checkbox" class="rcheck"' + (S.selRuns[r.id] ? " checked" : "") + (can ? "" : " disabled") +
|
|
@@ -3802,6 +3878,7 @@ window.sideOpenRun = function (id, n) {
|
|
|
3802
3878
|
/* 打开任务级详情:任务可能被续跑/重试过多次,步骤分散在多条 run 里。
|
|
3803
3879
|
* 按时间顺序列出该任务全部 run 的全部步骤,run 之间加分隔条。 */
|
|
3804
3880
|
window.sideOpenTask = function (key) {
|
|
3881
|
+
S.lastRun = null;
|
|
3805
3882
|
S.detailTaskKey = key;
|
|
3806
3883
|
S.detailRunId = null;
|
|
3807
3884
|
S.focusStep = 0;
|
|
@@ -3812,6 +3889,9 @@ window.sideOpenTask = function (key) {
|
|
|
3812
3889
|
$("run-detail").classList.remove("hidden");
|
|
3813
3890
|
document.querySelector("#sub-runs .panel:first-child").classList.add("hidden");
|
|
3814
3891
|
detailSideReset(); // 换详情目标:任务级 side 缓存作废,等首拉
|
|
3892
|
+
S.rdTab = "steps";
|
|
3893
|
+
S.rdTabSig = "";
|
|
3894
|
+
applyRdTabs();
|
|
3815
3895
|
syncInspectorVis(); // 详情已铺开:检查器让位(选中保留,返回列表自动滑回)
|
|
3816
3896
|
renderTaskDetail();
|
|
3817
3897
|
};
|
|
@@ -3821,7 +3901,7 @@ window.openRunInRuns = function (id) { jumpToRun(id); };
|
|
|
3821
3901
|
* 「编辑重试」——同一预填行为(newFromTask),标签贴合「改完再跑」的意图;
|
|
3822
3902
|
* 其余状态维持原标签。两按钮互斥出现,按钮区不因新增功能多占一行。 */
|
|
3823
3903
|
function setEditRetry(taskId, status, taskExists) {
|
|
3824
|
-
const failedish = !!taskId && (status === "failed" || status === "cancelled");
|
|
3904
|
+
const failedish = !!taskId && (status === "failed" || status === "cancelled" || status === "timeout");
|
|
3825
3905
|
const be = $("btn-editretry");
|
|
3826
3906
|
if (be) be.classList.toggle("hidden", !failedish);
|
|
3827
3907
|
const bn = $("btn-newfrom");
|
|
@@ -3846,21 +3926,46 @@ function syncArchBtn(task, active) {
|
|
|
3846
3926
|
};
|
|
3847
3927
|
}
|
|
3848
3928
|
|
|
3849
|
-
/*
|
|
3929
|
+
/* 质量未通过的代码任务也必须能重试:代码流水线会把「实现完成但验收未通过」
|
|
3930
|
+
* 收在 done + verdict.pass=false,不能因此把唯一的重试入口隐藏。 */
|
|
3931
|
+
function runNeedsRetry(run) {
|
|
3932
|
+
if (!run || !run.task_id) return false;
|
|
3933
|
+
if (["failed", "cancelled", "timeout"].includes(run.status)) return true;
|
|
3934
|
+
if (run.status !== "done") return false;
|
|
3935
|
+
const v = run.verdict || {};
|
|
3936
|
+
return v.pass === false || v.publishable === false ||
|
|
3937
|
+
v.verify_pass === false || v.review_pass === false;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
function retrySnapshotForTask(taskId, task, latestSummary) {
|
|
3941
|
+
const summary = latestSummary || {};
|
|
3942
|
+
if (task && (task.status === "running" || task.status === "queued"))
|
|
3943
|
+
return { task_id: taskId, status: task.status };
|
|
3944
|
+
if (runNeedsRetry(summary)) return summary;
|
|
3945
|
+
if (task && ["failed", "cancelled", "timeout"].includes(task.status))
|
|
3946
|
+
return { task_id: taskId, status: task.status };
|
|
3947
|
+
return summary;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
/* 重试按钮三态:失败/取消/质量未通过 → 「↻ 继续任务」(断点续跑语义);连载任务完整跑完
|
|
3850
3951
|
* 但质量未达标(verdict.publishable=false)→ 「↻ 重写未达标章」——后端 retry
|
|
3851
3952
|
* 会把未过线章从继承清单剔除,只重写重评这几章(store.retry_task 未达标分支);
|
|
3852
3953
|
* 其余状态隐藏。 */
|
|
3853
3954
|
function setRetryBtn(run, task) {
|
|
3854
3955
|
const b = $("btn-retry");
|
|
3855
3956
|
if (!b) return;
|
|
3856
|
-
const
|
|
3957
|
+
const retryable = runNeedsRetry(run);
|
|
3857
3958
|
const rewrite = !!(run.task_id && task && task.serial && run.status === "done"
|
|
3858
3959
|
&& run.verdict && run.verdict.publishable === false);
|
|
3859
|
-
b.classList.toggle("hidden", !(
|
|
3860
|
-
b.textContent = rewrite ? t("↻ 重写未达标章")
|
|
3960
|
+
b.classList.toggle("hidden", !(retryable || rewrite));
|
|
3961
|
+
b.textContent = rewrite ? t("↻ 重写未达标章")
|
|
3962
|
+
: (run.status === "done" ? t("↻ 重试任务")
|
|
3963
|
+
: run.status === "timeout" ? t("↻ 从超时进度继续") : t("↻ 继续任务"));
|
|
3861
3964
|
b.title = rewrite
|
|
3862
3965
|
? t("只重写上一遍未过线的章节,已过线章节沿用成稿与评分")
|
|
3863
|
-
:
|
|
3966
|
+
: run.status === "timeout"
|
|
3967
|
+
? t("从最近一次已保存的大纲和已完成章节断点续跑")
|
|
3968
|
+
: t("再跑一次这个任务;连载任务会断点续跑,已完成章节不重写");
|
|
3864
3969
|
}
|
|
3865
3970
|
|
|
3866
3971
|
/* 任务级详情:聚合该任务所有 run 的步骤。
|
|
@@ -3876,18 +3981,81 @@ function renderTaskDetail() {
|
|
|
3876
3981
|
const bc = $("btn-continue");
|
|
3877
3982
|
if (bc) bc.classList.toggle("hidden",
|
|
3878
3983
|
!(tk && tk.serial && tk.status !== "running" && tk.status !== "queued"));
|
|
3984
|
+
const latestSummary = ((S.state || {}).task_latest || {})[key] || {};
|
|
3985
|
+
setRetryBtn(retrySnapshotForTask(key, tk, latestSummary), tk);
|
|
3879
3986
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
3880
3987
|
syncArchBtn(tk, tk && (tk.status === "running" || tk.status === "queued"));
|
|
3881
3988
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
3882
3989
|
if (isTask) {
|
|
3883
|
-
|
|
3884
|
-
.then((d) => { if (S.detailTaskKey === key) drawTaskDetail(key, d.runs || []); })
|
|
3885
|
-
.catch((e) => { /* 拉取失败静默,等下一轮轮询重试;401 已由 api() 弹令牌门 */ });
|
|
3990
|
+
loadTaskRuns(key, latestSummary);
|
|
3886
3991
|
return;
|
|
3887
3992
|
}
|
|
3888
3993
|
drawTaskDetail(key, ((S.state || {}).runs || []).filter((r) => (r.task_id || r.id) === key));
|
|
3889
3994
|
}
|
|
3890
3995
|
|
|
3996
|
+
function loadTaskRuns(key, latestSummary) {
|
|
3997
|
+
const cached = _taskRunsCache.get(key);
|
|
3998
|
+
if (cached && cached.length && latestSummary && cached[0].id === latestSummary.id) {
|
|
3999
|
+
const detailVerdict = cached[0].verdict;
|
|
4000
|
+
Object.assign(cached[0], latestSummary);
|
|
4001
|
+
if (detailVerdict) cached[0].verdict = Object.assign({}, latestSummary.verdict || {}, detailVerdict);
|
|
4002
|
+
drawTaskDetail(key, cached);
|
|
4003
|
+
}
|
|
4004
|
+
const latest = cached && cached[0];
|
|
4005
|
+
const hasNewRun = !!(latestSummary && latestSummary.id && (!latest || latestSummary.id !== latest.id));
|
|
4006
|
+
const active = !!(latestSummary && ["running", "queued"].includes(latestSummary.status));
|
|
4007
|
+
const minAge = active ? 2000 : 12000;
|
|
4008
|
+
if (_taskRunsInflight.has(key) ||
|
|
4009
|
+
(!hasNewRun && cached && Date.now() - (_taskRunsAt.get(key) || 0) < minAge)) return;
|
|
4010
|
+
if (!cached) {
|
|
4011
|
+
const task = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
4012
|
+
$("rd-title").textContent = (task && task.title) || key;
|
|
4013
|
+
const chip = $("rd-status");
|
|
4014
|
+
chip.className = "chip queued";
|
|
4015
|
+
chip.textContent = t("加载中…");
|
|
4016
|
+
$("rd-steps").innerHTML = '<div class="hint" role="status">' + esc(t("正在加载任务详情…")) + "</div>";
|
|
4017
|
+
}
|
|
4018
|
+
_taskRunsAt.set(key, Date.now());
|
|
4019
|
+
const refreshLatestOnly = !!(cached && cached.length && latestSummary && cached[0].id === latestSummary.id);
|
|
4020
|
+
const url = refreshLatestOnly
|
|
4021
|
+
? "/api/runs/" + encodeURIComponent(latestSummary.id)
|
|
4022
|
+
: "/api/tasks/" + encodeURIComponent(key) + "/runs";
|
|
4023
|
+
const request = api(url, { timeout: 20000 })
|
|
4024
|
+
.then((data) => {
|
|
4025
|
+
let runs = (data && data.runs) || [];
|
|
4026
|
+
if (refreshLatestOnly && data && data.run) {
|
|
4027
|
+
runs = cached.slice();
|
|
4028
|
+
runs[0] = data.run;
|
|
4029
|
+
}
|
|
4030
|
+
_taskRunsCache.delete(key);
|
|
4031
|
+
_taskRunsCache.set(key, runs);
|
|
4032
|
+
while (_taskRunsCache.size > 1) {
|
|
4033
|
+
const oldest = _taskRunsCache.keys().next().value;
|
|
4034
|
+
_taskRunsCache.delete(oldest);
|
|
4035
|
+
_taskRunsAt.delete(oldest);
|
|
4036
|
+
}
|
|
4037
|
+
if (S.detailTaskKey === key) {
|
|
4038
|
+
if (runs.length) drawTaskDetail(key, runs);
|
|
4039
|
+
else {
|
|
4040
|
+
$("rd-status").textContent = t("暂无运行步骤");
|
|
4041
|
+
$("rd-steps").innerHTML = '<div class="empty">' + esc(t("暂无运行步骤")) + "</div>";
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
})
|
|
4045
|
+
.catch((e) => {
|
|
4046
|
+
_taskRunsAt.set(key, 0);
|
|
4047
|
+
if (S.detailTaskKey === key && !_taskRunsCache.has(key)) {
|
|
4048
|
+
const chip = $("rd-status");
|
|
4049
|
+
chip.className = "chip failed";
|
|
4050
|
+
chip.textContent = t("加载失败");
|
|
4051
|
+
$("rd-steps").innerHTML = '<div class="hint">' + esc(t("加载失败:")) + esc(e.message) +
|
|
4052
|
+
' <button type="button" class="ghost small" onclick="renderTaskDetail()">' + esc(t("重试")) + "</button></div>";
|
|
4053
|
+
}
|
|
4054
|
+
})
|
|
4055
|
+
.finally(() => _taskRunsInflight.delete(key));
|
|
4056
|
+
_taskRunsInflight.set(key, request);
|
|
4057
|
+
}
|
|
4058
|
+
|
|
3891
4059
|
function drawTaskDetail(key, runs) {
|
|
3892
4060
|
if (!runs.length) return;
|
|
3893
4061
|
// 消息数/未消费数也入签名:新指令或被 drain 后重画指挥区;步骤状态入签名:
|
|
@@ -3900,7 +4068,9 @@ function drawTaskDetail(key, runs) {
|
|
|
3900
4068
|
Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
|
|
3901
4069
|
const sig = JSON.stringify(runs.map((r) => [r.id, r.status, (r.steps || []).length,
|
|
3902
4070
|
(r.steps || []).map((s) => s.status).join(""),
|
|
3903
|
-
(r.messages || []).length, (r.messages || []).filter((m) => !m.consumed).length
|
|
4071
|
+
(r.messages || []).length, (r.messages || []).filter((m) => !m.consumed).length,
|
|
4072
|
+
r.message_count, r.pending_message_count,
|
|
4073
|
+
r.summary, r.error, r.cost_usd, r.tokens, JSON.stringify(r.verdict || {})])
|
|
3904
4074
|
.concat([JSON.stringify((bmTask || {}).book_meta || null), resumePending]));
|
|
3905
4075
|
if (sig === S.taskSig) return;
|
|
3906
4076
|
S.taskSig = sig;
|
|
@@ -3919,7 +4089,7 @@ function drawTaskDetail(key, runs) {
|
|
|
3919
4089
|
chip.className = "chip " + st;
|
|
3920
4090
|
chip.textContent = resumeIn
|
|
3921
4091
|
? t("将于 ") + resumeIn + t(" 自动续跑(第 ") + (Number(latest.auto_resumes) || 0) + t(" 次)")
|
|
3922
|
-
: ({ queued: t("
|
|
4092
|
+
: ({ queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") }[st] || st);
|
|
3923
4093
|
const bpt = $("btn-pause");
|
|
3924
4094
|
if (bpt) bpt.classList.add("hidden");
|
|
3925
4095
|
$("btn-delete").classList.add("hidden");
|
|
@@ -6377,18 +6547,18 @@ function logLiveBadge(live) {
|
|
|
6377
6547
|
}
|
|
6378
6548
|
}
|
|
6379
6549
|
|
|
6380
|
-
/* 复制抽屉日志全文:报错 JSON 动辄几十行还带横向滚动,手动圈选很折磨 */
|
|
6381
|
-
window.rdLogCopy = function () {
|
|
6382
|
-
const pre = $("rd-log-text");
|
|
6383
|
-
const text = ((pre && pre.textContent) || "").trim();
|
|
6384
|
-
const idle = [t("(等待输出…)"), t("(无输出)")];
|
|
6385
|
-
if (!text || idle.includes(text)) return;
|
|
6386
|
-
const done = () => toast(t("已复制"));
|
|
6387
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6388
|
-
navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, done));
|
|
6389
|
-
} else fallbackCopy(text, done);
|
|
6390
|
-
};
|
|
6391
|
-
|
|
6550
|
+
/* 复制抽屉日志全文:报错 JSON 动辄几十行还带横向滚动,手动圈选很折磨 */
|
|
6551
|
+
window.rdLogCopy = function () {
|
|
6552
|
+
const pre = $("rd-log-text");
|
|
6553
|
+
const text = ((pre && pre.textContent) || "").trim();
|
|
6554
|
+
const idle = [t("(等待输出…)"), t("(无输出)")];
|
|
6555
|
+
if (!text || idle.includes(text)) return;
|
|
6556
|
+
const done = () => toast(t("已复制"));
|
|
6557
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6558
|
+
navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, done));
|
|
6559
|
+
} else fallbackCopy(text, done);
|
|
6560
|
+
};
|
|
6561
|
+
|
|
6392
6562
|
/* 抽屉标题:按 (runId, 日志路径) 反查步骤角色与执行者——不知道在看谁的日志,
|
|
6393
6563
|
* 抽屉就成了无名黑框 */
|
|
6394
6564
|
function rdLogStepLabel(runId, rel) {
|
|
@@ -6803,7 +6973,40 @@ function renderDirector(run, active) {
|
|
|
6803
6973
|
if (!run || !active) { box.classList.add("hidden"); return; }
|
|
6804
6974
|
if (dirRunId !== run.id) { dirRunId = run.id; dirAtts = []; drawDirAtts(); }
|
|
6805
6975
|
box.classList.remove("hidden");
|
|
6806
|
-
const
|
|
6976
|
+
const cached = _runMessagesCache.get(run.id);
|
|
6977
|
+
const expectedCount = run.message_count == null ? null : Number(run.message_count);
|
|
6978
|
+
const expectedPending = run.pending_message_count == null ? null : Number(run.pending_message_count);
|
|
6979
|
+
const runMessages = Array.isArray(run.messages) ? run.messages : null;
|
|
6980
|
+
const runPending = runMessages ? runMessages.filter((m) => !m.consumed).length : null;
|
|
6981
|
+
const runMessagesCurrent = !!runMessages && (expectedCount == null ||
|
|
6982
|
+
(runMessages.length === expectedCount && runPending === expectedPending));
|
|
6983
|
+
const msgs = runMessagesCurrent ? runMessages : (cached ? cached.messages : []);
|
|
6984
|
+
if (runMessagesCurrent) {
|
|
6985
|
+
const pending = msgs.filter((m) => !m.consumed).length;
|
|
6986
|
+
_runMessagesCache.set(run.id, { messages: msgs, count: msgs.length, pending });
|
|
6987
|
+
}
|
|
6988
|
+
if (expectedCount != null) {
|
|
6989
|
+
const messageCache = _runMessagesCache.get(run.id);
|
|
6990
|
+
if (!messageCache || messageCache.count !== expectedCount || messageCache.pending !== expectedPending) {
|
|
6991
|
+
if (!_runMessagesInflight.has(run.id)) {
|
|
6992
|
+
const request = api("/api/runs/" + encodeURIComponent(run.id) + "/messages", { timeout: 10000 })
|
|
6993
|
+
.then((data) => {
|
|
6994
|
+
if (dirRunId !== run.id || !S.lastRun || S.lastRun.id !== run.id) return;
|
|
6995
|
+
const messages = (data && data.messages) || [];
|
|
6996
|
+
_runMessagesCache.clear();
|
|
6997
|
+
_runMessagesCache.set(run.id, {
|
|
6998
|
+
messages, count: messages.length,
|
|
6999
|
+
pending: messages.filter((m) => !m.consumed).length,
|
|
7000
|
+
});
|
|
7001
|
+
renderDirector(Object.assign({}, S.lastRun, { messages }),
|
|
7002
|
+
S.lastRun.status === "running" || S.lastRun.status === "queued");
|
|
7003
|
+
})
|
|
7004
|
+
.catch(() => {})
|
|
7005
|
+
.finally(() => _runMessagesInflight.delete(run.id));
|
|
7006
|
+
_runMessagesInflight.set(run.id, request);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
6807
7010
|
const mb = $("rd-msgs");
|
|
6808
7011
|
mb.innerHTML = msgs.map((m) =>
|
|
6809
7012
|
'<div class="rd-msg' + (m.consumed ? " m-consumed" : "") + '">' +
|
|
@@ -9240,11 +9443,15 @@ function autoTplPace(tp) {
|
|
|
9240
9443
|
});
|
|
9241
9444
|
}
|
|
9242
9445
|
|
|
9243
|
-
/* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed
|
|
9446
|
+
/* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed=黄「已错过」;
|
|
9447
|
+
* done/failed/cancelled=运行终态(2026-09-23 回写接线后会出现,失败必须可见) */
|
|
9244
9448
|
function autoStatusTag(tsk) {
|
|
9245
9449
|
if (tsk.last_status === "error") return '<span class="tag auto-tag-err">' + t("拉起失败") + "</span>";
|
|
9246
9450
|
if (tsk.last_status === "missed") return '<span class="tag auto-tag-miss">' + t("已错过") + "</span>";
|
|
9247
9451
|
if (tsk.last_status === "started" || tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
|
|
9452
|
+
if (tsk.last_status === "failed") return '<span class="tag auto-tag-err">' + t("上次失败") + "</span>";
|
|
9453
|
+
if (tsk.last_status === "cancelled") return '<span class="tag auto-tag-miss">' + t("已取消") + "</span>";
|
|
9454
|
+
if (tsk.last_status === "done") return '<span class="tag">' + t("已完成") + "</span>";
|
|
9248
9455
|
return "";
|
|
9249
9456
|
}
|
|
9250
9457
|
|
|
@@ -9954,6 +10161,14 @@ function renderZentaoStatus() {
|
|
|
9954
10161
|
(v.last_error ? '<span class="zt-err">' + t("最近错误:") + esc(v.last_error) + "</span>" : "");
|
|
9955
10162
|
}
|
|
9956
10163
|
|
|
10164
|
+
/* bug 详情页地址:后端回传探测出的 web 根(含 /zentao 子路径),
|
|
10165
|
+
GET 形态 index.php?m=bug&f=view&bugID=N 对老版/新版、PATH_INFO 开关都通用。 */
|
|
10166
|
+
function zentaoBugUrl(c) {
|
|
10167
|
+
const base = String((S.zentao || {}).bug_base || "").replace(/\/+$/, "");
|
|
10168
|
+
if (!base || !c.bug_id) return "";
|
|
10169
|
+
return base + "/index.php?m=bug&f=view&bugID=" + encodeURIComponent(c.bug_id);
|
|
10170
|
+
}
|
|
10171
|
+
|
|
9957
10172
|
function renderZentaoClaims() {
|
|
9958
10173
|
const box = $("zentao-claims");
|
|
9959
10174
|
if (!box) return;
|
|
@@ -9962,7 +10177,13 @@ function renderZentaoClaims() {
|
|
|
9962
10177
|
const tasks = (c.tasks || []).map((tk) =>
|
|
9963
10178
|
"<span>" + t("【") + (tk.side === "frontend" ? t("前端") : t("后端")) + t("】") +
|
|
9964
10179
|
'<a href="#" onclick="zentaoOpenRun(\'' + esc(tk.run_id || "") + '\');return false;">' + esc(tk.task_id || "?") + "</a></span>").join("");
|
|
9965
|
-
|
|
10180
|
+
const bugUrl = zentaoBugUrl(c);
|
|
10181
|
+
const name = "#" + esc(c.bug_id) + " " + esc(c.title || "");
|
|
10182
|
+
const headName = bugUrl
|
|
10183
|
+
? '<a class="zt-bug-link" href="' + esc(bugUrl) + '" target="_blank" rel="noopener" title="' +
|
|
10184
|
+
esc(t("在禅道中打开 Bug 详情")) + '">' + name + "</a>"
|
|
10185
|
+
: name;
|
|
10186
|
+
return '<div class="card"><div class="head"><span class="name">' + headName + "</span>" +
|
|
9966
10187
|
zentaoStateTag(c.state) + "</div>" +
|
|
9967
10188
|
'<div class="auto-meta">' +
|
|
9968
10189
|
(zentaoTriText(c) ? "<span>" + esc(zentaoTriText(c)) + "</span>" : "") +
|
|
@@ -11327,6 +11548,12 @@ function pickLang(lang) {
|
|
|
11327
11548
|
window.toggleLangMenu = toggleLangMenu;
|
|
11328
11549
|
window.pickLang = pickLang;
|
|
11329
11550
|
|
|
11551
|
+
/* 顶栏大屏入口:新标签页打开任务驾驶舱;同源共享 orch.token,board 页免二次输令牌 */
|
|
11552
|
+
function openBoard() {
|
|
11553
|
+
window.open("/board.html", "_blank", "noopener");
|
|
11554
|
+
}
|
|
11555
|
+
window.openBoard = openBoard;
|
|
11556
|
+
|
|
11330
11557
|
function setLangBtn(lang) {
|
|
11331
11558
|
if (getLang() === lang) return;
|
|
11332
11559
|
setLang(lang);
|
|
@@ -12627,7 +12854,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
12627
12854
|
new MutationObserver(sync).observe(sub, { attributes: true, attributeFilter: ["class"] });
|
|
12628
12855
|
sync();
|
|
12629
12856
|
})();
|
|
12630
|
-
$("btn-retry").addEventListener("click", () => {
|
|
12857
|
+
$("btn-retry").addEventListener("click", () => {
|
|
12858
|
+
const r = S.lastRun;
|
|
12859
|
+
const taskId = (r && r.task_id) ||
|
|
12860
|
+
(((S.state || {}).tasks || []).some((task) => task.id === S.detailTaskKey)
|
|
12861
|
+
? S.detailTaskKey : "");
|
|
12862
|
+
if (taskId) retryTask(taskId);
|
|
12863
|
+
});
|
|
12631
12864
|
/* 日志控制台高度:顶边手柄拖拽调整(localStorage 记忆),双击复位为默认弹性高度 */
|
|
12632
12865
|
(() => {
|
|
12633
12866
|
const grip = $("rd-log-grip"), box = $("rd-log");
|