codebee 0.1.7 → 0.1.8
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 +10 -0
- package/README.md +236 -252
- package/app/core/catalog.py +24 -5
- package/app/core/errorlog.py +179 -0
- package/app/core/flows.py +6 -2
- package/app/core/gitmod.py +45 -1
- package/app/core/health.py +48 -11
- package/app/core/jobs.py +104 -9
- package/app/core/manager.py +21 -0
- package/app/core/modelhub.py +80 -11
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +420 -88
- package/app/core/runner.py +122 -10
- package/app/core/settings.py +9 -2
- package/app/core/step_runner.py +28 -4
- package/app/core/store.py +180 -139
- package/app/core/telemetry.py +291 -0
- package/app/core/token_meter.py +18 -0
- package/app/main.py +162 -16
- package/app/pick_dialog.py +78 -0
- package/app/ui/app.js +978 -601
- package/app/ui/i18n.js +77 -6
- package/app/ui/index.html +161 -80
- package/app/ui/style.css +827 -67
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const $ = (id) => document.getElementById(id);
|
|
5
|
-
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {} };
|
|
5
|
+
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {} };
|
|
6
6
|
|
|
7
7
|
/* ---------------------------------------------------------- 任务类型(流程) */
|
|
8
8
|
async function loadFlows() {
|
|
@@ -27,7 +27,10 @@ function flowIconHtml(f) {
|
|
|
27
27
|
|
|
28
28
|
function flowDesc(f) {
|
|
29
29
|
if (f.engine === "direct") return t("单智能体直达(快)");
|
|
30
|
-
|
|
30
|
+
if (f.engine === "code") return t("实现 → 验证 → 评审");
|
|
31
|
+
// 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
|
|
32
|
+
if (f.serial) return t("大纲 → 逐章起草评审 → 合并(可续跑)");
|
|
33
|
+
return t("起草 → 多维评审 → 门禁");
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
function renderTypeOptions() {
|
|
@@ -57,9 +60,17 @@ function renderTypeMenu() {
|
|
|
57
60
|
(f.builtin ? "" : '<em class="ti-tag">' + t("自定义") + "</em>") + "</span>" +
|
|
58
61
|
'<span class="ti-desc">' + esc(flowDesc(f)) + "</span></span>" +
|
|
59
62
|
'<svg class="ico ti-check" aria-hidden="true"><use href="#i-check"></use></svg>' +
|
|
60
|
-
"</button>").join("")
|
|
63
|
+
"</button>").join("")
|
|
64
|
+
/* 底部固定入口:管理流程替代原工具条上的「管理」按钮(简化 composer 工具行) */
|
|
65
|
+
+ '<div class="type-menu-foot"><button type="button" class="type-item" onclick="manageFlowsFromMenu()">' +
|
|
66
|
+
'<span class="ti-ico" aria-hidden="true"><svg class="ico"><use href="#i-gear"></use></svg></span>' +
|
|
67
|
+
'<span class="ti-body"><span class="ti-name">' + esc(t("管理任务类型…")) + "</span></span></button></div>";
|
|
61
68
|
syncTypeBtn();
|
|
62
69
|
}
|
|
70
|
+
window.manageFlowsFromMenu = function () {
|
|
71
|
+
toggleTypeMenu(false);
|
|
72
|
+
openFlowsManager();
|
|
73
|
+
};
|
|
63
74
|
|
|
64
75
|
function syncTypeBtn() {
|
|
65
76
|
const sel = $("f-type");
|
|
@@ -97,34 +108,35 @@ function onTypeChange() {
|
|
|
97
108
|
const engine = flow ? flow.engine : "code";
|
|
98
109
|
const isReview = engine === "review";
|
|
99
110
|
const isDirect = engine === "direct";
|
|
100
|
-
const codeOnly = $("f-code-only"), reviewOnly = $("f-review-only");
|
|
111
|
+
const codeOnly = $("f-code-only"), reviewOnly = $("f-review-only");
|
|
101
112
|
// 直连:验证命令与评审参数都不适用,两块一起收起(目标+附件即全部输入)
|
|
102
|
-
if (codeOnly) codeOnly.classList.toggle("hidden", isReview || isDirect);
|
|
103
|
-
if (reviewOnly) reviewOnly.classList.toggle("hidden", !isReview);
|
|
104
|
-
const bibleCreate = $("bible-create-field");
|
|
105
|
-
const isSerialReview = isReview && !!(flow && flow.serial);
|
|
106
|
-
if (bibleCreate) bibleCreate.classList.toggle("hidden", !isSerialReview);
|
|
107
|
-
const clearSerialFields = () => ["f-chapters", "f-words-per-ch", "f-variants", "f-bible"].forEach((id) => {
|
|
108
|
-
const el = $(id);
|
|
109
|
-
if (el) el.value = "";
|
|
110
|
-
});
|
|
113
|
+
if (codeOnly) codeOnly.classList.toggle("hidden", isReview || isDirect);
|
|
114
|
+
if (reviewOnly) reviewOnly.classList.toggle("hidden", !isReview);
|
|
115
|
+
const bibleCreate = $("bible-create-field");
|
|
116
|
+
const isSerialReview = isReview && !!(flow && flow.serial);
|
|
117
|
+
if (bibleCreate) bibleCreate.classList.toggle("hidden", !isSerialReview);
|
|
118
|
+
const clearSerialFields = () => ["f-chapters", "f-words-per-ch", "f-variants", "f-bible"].forEach((id) => {
|
|
119
|
+
const el = $(id);
|
|
120
|
+
if (el) el.value = "";
|
|
121
|
+
});
|
|
111
122
|
const goal = $("f-goal");
|
|
112
123
|
if (goal && flow && flow.goal_hint) goal.placeholder = t(flow.goal_hint);
|
|
113
124
|
if (isReview && flow) {
|
|
114
125
|
if (flow.manuscript) $("f-manuscript").value = flow.manuscript;
|
|
115
126
|
if (flow.rounds) $("f-rounds").value = flow.rounds;
|
|
116
127
|
if (flow.threshold) $("f-threshold").value = flow.threshold;
|
|
128
|
+
if (flow.best_of) $("f-bestof").value = flow.best_of;
|
|
117
129
|
const saved = ($("f-rubric").value || "").trim();
|
|
118
130
|
if (!saved && flow.rubric) $("f-rubric").value = flow.rubric.join(", ");
|
|
119
131
|
// 连载参数预填(用户可改/可清空 = 单稿件模式)
|
|
120
|
-
if (flow.serial) {
|
|
121
|
-
if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
|
|
122
|
-
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = flow.serial.words_per_chapter || "";
|
|
123
|
-
} else clearSerialFields();
|
|
124
|
-
} else {
|
|
125
|
-
// 隐藏控件仍保留 DOM 值;换流程时清理,避免章节/圣经状态污染下一次提交。
|
|
126
|
-
clearSerialFields();
|
|
127
|
-
}
|
|
132
|
+
if (flow.serial) {
|
|
133
|
+
if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
|
|
134
|
+
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = flow.serial.words_per_chapter || "";
|
|
135
|
+
} else clearSerialFields();
|
|
136
|
+
} else {
|
|
137
|
+
// 隐藏控件仍保留 DOM 值;换流程时清理,避免章节/圣经状态污染下一次提交。
|
|
138
|
+
clearSerialFields();
|
|
139
|
+
}
|
|
128
140
|
renderImplSelects();
|
|
129
141
|
refreshEstimate();
|
|
130
142
|
}
|
|
@@ -327,126 +339,6 @@ function uiPrompt(title, value) {
|
|
|
327
339
|
}).then((ok) => ((ok && inp) ? inp.value.trim() : null));
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
/* ------------------------------------------------- 采访式向导(新建任务) */
|
|
331
|
-
/* 不想面对整张表单的新用户:三问一确认——目标 → 类型 → 目录 → 预览填入。
|
|
332
|
-
* 代码类流程多追问一步「验证命令」(怎么算跑通);页序列按所选类型动态生成。
|
|
333
|
-
* 向导只把答案预填进既有表单,提交/附件/评审设置全部留在表单里:
|
|
334
|
-
* 不新增提交路径,质量闸门与既有测试照旧。目录浏览用表单里的「选择…」。 */
|
|
335
|
-
let wiz = null; // { page, pages, goal, type, workdir, verify }
|
|
336
|
-
|
|
337
|
-
function wizardPages(type) {
|
|
338
|
-
const flow = flowById(type);
|
|
339
|
-
return ["goal", "type"].concat(
|
|
340
|
-
flow && flow.engine === "code" ? ["verify"] : [], ["workdir", "summary"]);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
window.openTaskWizard = async function () {
|
|
344
|
-
// 冷启动时 boot 的 loadFlows 可能晚于用户点击:空了就现拉一次(autoForm 同款防御)
|
|
345
|
-
if (!S.flows || !S.flows.length) { try { await loadFlows(); } catch (e) { /* 渲染时兜底 */ } }
|
|
346
|
-
const type = (($("f-type") || {}).value) || ((S.flows || [])[0] || {}).id || "";
|
|
347
|
-
wiz = { page: 0, pages: wizardPages(type), goal: "", type,
|
|
348
|
-
workdir: (($("f-workdir") || {}).value || ""), verify: "" };
|
|
349
|
-
renderWizard();
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
window.wizardBack = function () { if (wiz && wiz.page > 0) { wiz.page--; renderWizard(); } };
|
|
353
|
-
window.wizardPickType = function (id) {
|
|
354
|
-
if (!wiz) return;
|
|
355
|
-
wiz.type = id;
|
|
356
|
-
wiz.pages = wizardPages(id); // 按新类型重排页序列(代码流程插入验证步)
|
|
357
|
-
wiz.page = 2; // 类型页的下一页
|
|
358
|
-
renderWizard();
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
window.wizardNext = function () {
|
|
362
|
-
if (!wiz) return;
|
|
363
|
-
const id = wiz.pages[wiz.page];
|
|
364
|
-
if (id === "goal") {
|
|
365
|
-
const v = (($("wz-goal") || {}).value || "").trim();
|
|
366
|
-
if (!v) { toast(t("目标还不能为空"), true); return; }
|
|
367
|
-
wiz.goal = v;
|
|
368
|
-
wiz.page++;
|
|
369
|
-
} else if (id === "verify") {
|
|
370
|
-
wiz.verify = (($("wz-verify") || {}).value || "").trim();
|
|
371
|
-
wiz.page++;
|
|
372
|
-
} else if (id === "workdir") {
|
|
373
|
-
wiz.workdir = (($("wz-workdir") || {}).value || "").trim();
|
|
374
|
-
wiz.page++;
|
|
375
|
-
}
|
|
376
|
-
renderWizard();
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
function wizardFoot(back) {
|
|
380
|
-
return (back ? '<button class="ghost" onclick="wizardBack()">' + esc(t("上一步")) + "</button>" : "") +
|
|
381
|
-
'<button class="primary" onclick="wizardNext()">' + esc(t("下一步")) + "</button>";
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function renderWizard() {
|
|
385
|
-
if (!wiz) return;
|
|
386
|
-
const f = flowById(wiz.type);
|
|
387
|
-
const id = wiz.pages[wiz.page];
|
|
388
|
-
let body = "", foot = "";
|
|
389
|
-
if (id === "goal") {
|
|
390
|
-
body = '<textarea id="wz-goal" class="wz-goal" rows="4" placeholder="' +
|
|
391
|
-
esc(t("要完成什么,一句话即可")) +
|
|
392
|
-
'" onkeydown="if(event.key===\'Enter\'&&!event.shiftKey){event.preventDefault();wizardNext()}">' +
|
|
393
|
-
esc(wiz.goal) + "</textarea>";
|
|
394
|
-
foot = wizardFoot(false);
|
|
395
|
-
} else if (id === "type") {
|
|
396
|
-
body = '<div class="wz-flows">' + (S.flows || []).map((fl) =>
|
|
397
|
-
'<button type="button" class="wz-flow' + (fl.id === wiz.type ? " on" : "") +
|
|
398
|
-
'" onclick="wizardPickType(\'' + esc(fl.id) + '\')">' +
|
|
399
|
-
flowIconHtml(fl) +
|
|
400
|
-
'<span class="wz-fname">' + esc(fl.name || fl.id) + "</span>" +
|
|
401
|
-
'<span class="wz-fdesc">' + esc(flowDesc(fl)) + "</span></button>").join("") + "</div>" +
|
|
402
|
-
'<p class="hint">' + esc(t("点一个类型即选定并继续;进表单后仍可改")) + "</p>";
|
|
403
|
-
foot = wizardFoot(true);
|
|
404
|
-
} else if (id === "verify") {
|
|
405
|
-
body = '<input id="wz-verify" class="wz-input" autocomplete="off" value="' + esc(wiz.verify) +
|
|
406
|
-
'" placeholder="' + esc(t("例:npm test、pytest -q;留空=只靠 AI 评审")) +
|
|
407
|
-
'" onkeydown="if(event.key===\'Enter\'){event.preventDefault();wizardNext()}">' +
|
|
408
|
-
'<p class="hint">' + esc(t("一条能在工作目录里跑的命令,退出码 0 即视为通过;之后可在表单改")) + "</p>";
|
|
409
|
-
foot = wizardFoot(true);
|
|
410
|
-
} else if (id === "workdir") {
|
|
411
|
-
body = '<input id="wz-workdir" class="wz-input" autocomplete="off" value="' + esc(wiz.workdir) +
|
|
412
|
-
'" placeholder="' + esc(t("留空用默认保存路径;也可稍后在表单里点「选择…」")) +
|
|
413
|
-
'" onkeydown="if(event.key===\'Enter\'){event.preventDefault();wizardNext()}">' +
|
|
414
|
-
'<p class="hint">' + esc(t("建议给每个任务一个独立目录,产出互不覆盖")) + "</p>";
|
|
415
|
-
foot = wizardFoot(true);
|
|
416
|
-
} else {
|
|
417
|
-
body = '<div class="wz-sum">' +
|
|
418
|
-
'<div><span class="wz-k">' + esc(t("类型")) + "</span>" + esc((f && f.name) || wiz.type) + "</div>" +
|
|
419
|
-
'<div><span class="wz-k">' + esc(t("目标")) + "</span>" + esc(wiz.goal.slice(0, 120)) + "</div>" +
|
|
420
|
-
(f && f.engine === "code"
|
|
421
|
-
? '<div><span class="wz-k">' + esc(t("验证命令")) + "</span>" +
|
|
422
|
-
esc(wiz.verify || t("留空靠 AI 评审")) + "</div>"
|
|
423
|
-
: "") +
|
|
424
|
-
'<div><span class="wz-k">' + esc(t("工作目录")) + "</span>" +
|
|
425
|
-
esc(wiz.workdir || t("默认保存路径")) + "</div></div>" +
|
|
426
|
-
'<p class="hint">' + esc(t("填入表单后可继续:加附件、选实现者、改评审设置")) + "</p>";
|
|
427
|
-
foot = '<button class="ghost" onclick="wizardBack()">' + esc(t("上一步")) + "</button>" +
|
|
428
|
-
'<button class="primary" onclick="wizardApply()">' + esc(t("填入表单")) + "</button>";
|
|
429
|
-
}
|
|
430
|
-
openModal(t("新建任务向导") + " · " + (wiz.page + 1) + "/" + wiz.pages.length, body, foot);
|
|
431
|
-
const first = $("wz-goal") || $("wz-verify") || $("wz-workdir");
|
|
432
|
-
if (first) first.focus();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
window.wizardApply = function () {
|
|
436
|
-
if (!wiz) return;
|
|
437
|
-
closeModal();
|
|
438
|
-
const flow = flowById(wiz.type);
|
|
439
|
-
if (flow && $("f-type").value !== wiz.type) { $("f-type").value = wiz.type; onTypeChange(); }
|
|
440
|
-
$("f-goal").value = wiz.goal;
|
|
441
|
-
if (flow && flow.engine === "code") $("f-verify").value = wiz.verify || "";
|
|
442
|
-
if (wiz.workdir) $("f-workdir").value = wiz.workdir; // 只预填本次表单,不再持久化为「下次默认」
|
|
443
|
-
S.atts = []; renderAttachChips(); // 新目标不带旧附件
|
|
444
|
-
queueGitProbe(); // 工作目录可能变了,重新探测代码版本
|
|
445
|
-
$("f-goal").focus();
|
|
446
|
-
toast(t("已按向导预填,检查或补充后点「创建并运行」"));
|
|
447
|
-
wiz = null;
|
|
448
|
-
};
|
|
449
|
-
|
|
450
342
|
/* 访问令牌门(仅远程设备会碰到) */
|
|
451
343
|
function showTokenGate(err) {
|
|
452
344
|
const g = $("token-gate");
|
|
@@ -1073,13 +965,16 @@ function renderProvDetail() {
|
|
|
1073
965
|
html += '<details class="pd-config"><summary>' + t("编辑供应商配置(地址 / 协议 / 难度模型)") + '</summary>' +
|
|
1074
966
|
providerCard(p) + "</details>";
|
|
1075
967
|
if (p.models == null) {
|
|
1076
|
-
html += '<div class="empty">' + t("尚未获取模型列表——点上方「获取模型列表」。") + '</div>'
|
|
968
|
+
html += '<div class="empty">' + t("尚未获取模型列表——点上方「获取模型列表」。") + '</div>' +
|
|
969
|
+
'<div class="hint">' + t("厂商列表接口调不通?直接手工添加模型名也能用。") + '</div>' +
|
|
970
|
+
pmAddHtml(p);
|
|
1077
971
|
} else {
|
|
1078
972
|
const kw = (S.modelFilter || {})[p.id] || "";
|
|
1079
973
|
html += '<div class="pm-tools">' +
|
|
1080
974
|
'<input id="pm-search-' + esc(p.id) + '" type="search" placeholder="' + t("按名称过滤模型…") + '" autocomplete="off"' +
|
|
1081
975
|
' value="' + esc(kw) + '" oninput="filterModels(\'' + esc(p.id) + '\', this.value)">' +
|
|
1082
|
-
'<span class="pm-count" id="pm-count-' + esc(p.id) + '"></span
|
|
976
|
+
'<span class="pm-count" id="pm-count-' + esc(p.id) + '"></span>' +
|
|
977
|
+
pmAddHtml(p) + '</div>';
|
|
1083
978
|
html += '<div id="pm-groups" class="pm-groups' + (selN ? " has-sel" : "") + '">' +
|
|
1084
979
|
provModelGroupsHtml(p) + "</div>";
|
|
1085
980
|
if (hidden.length) {
|
|
@@ -1514,6 +1409,36 @@ async function refreshAllModels() {
|
|
|
1514
1409
|
setTimeout(autoRebindSoon, 8000); // 模型列表落盘后推荐源才完整,补绑一次
|
|
1515
1410
|
}
|
|
1516
1411
|
|
|
1412
|
+
/* 手工添加模型:有的厂商列表接口调不通(或只返回部分),直接填模型名也能进列表。
|
|
1413
|
+
* 后端打 manual 标记,之后刷新即使厂商列表里没有它也不丢。 */
|
|
1414
|
+
function pmAddHtml(p) {
|
|
1415
|
+
return '<div class="pm-add">' +
|
|
1416
|
+
'<input id="pm-add-' + esc(p.id) + '" type="text" autocomplete="off" spellcheck="false"' +
|
|
1417
|
+
' placeholder="' + t("手工添加模型名(列表接口调不通时用),回车添加") + '"' +
|
|
1418
|
+
' onkeydown="pmAddKey(event, \'' + esc(p.id) + '\')">' +
|
|
1419
|
+
'<button class="ghost small" title="' + esc(t("厂商列表接口调不通时,直接把模型名加进列表")) + '"' +
|
|
1420
|
+
' onclick="addModelManual(\'' + esc(p.id) + '\')">' +
|
|
1421
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-plus"/></svg>' + t("添加") + '</button></div>';
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function pmAddKey(ev, pid) {
|
|
1425
|
+
if (ev.key === "Enter") { ev.preventDefault(); addModelManual(pid); }
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function addModelManual(pid) {
|
|
1429
|
+
const inp = $("pm-add-" + pid);
|
|
1430
|
+
const name = ((inp && inp.value) || "").trim();
|
|
1431
|
+
if (!name) { toast(t("先填模型名"), true); return; }
|
|
1432
|
+
try {
|
|
1433
|
+
const r = await api("/api/models/add", { method: "POST", body: JSON.stringify({ id: pid, name }) });
|
|
1434
|
+
if (!r.ok) { toast(t("添加失败:") + (r.message || ""), true); return; }
|
|
1435
|
+
toast(t("已添加模型 ") + name);
|
|
1436
|
+
S.modelsSig = null; // 新模型落盘后签名必变,这里主动置空确保立刻重绘
|
|
1437
|
+
poll();
|
|
1438
|
+
setTimeout(autoRebindSoon, 4000); // 模型可用后推荐源更完整,补绑一次
|
|
1439
|
+
} catch (e) { toast(t("添加失败:") + e.message, true); }
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1517
1442
|
async function refreshProviderModels(id) {
|
|
1518
1443
|
toast(t("正在获取模型列表…"));
|
|
1519
1444
|
try {
|
|
@@ -1803,39 +1728,39 @@ function renderImplSelects() {
|
|
|
1803
1728
|
}
|
|
1804
1729
|
}
|
|
1805
1730
|
|
|
1806
|
-
async function createTask() {
|
|
1807
|
-
if (S.creatingTask) return;
|
|
1808
|
-
S.creatingTask = true;
|
|
1809
|
-
const submit = $("btn-create");
|
|
1810
|
-
if (submit) {
|
|
1811
|
-
submit.disabled = true;
|
|
1812
|
-
submit.setAttribute("aria-busy", "true");
|
|
1813
|
-
}
|
|
1814
|
-
const resetSubmit = () => {
|
|
1815
|
-
S.creatingTask = false;
|
|
1816
|
-
if (submit) {
|
|
1817
|
-
submit.disabled = false;
|
|
1818
|
-
submit.removeAttribute("aria-busy");
|
|
1819
|
-
}
|
|
1820
|
-
};
|
|
1821
|
-
const msg = $("create-msg");
|
|
1731
|
+
async function createTask() {
|
|
1732
|
+
if (S.creatingTask) return;
|
|
1733
|
+
S.creatingTask = true;
|
|
1734
|
+
const submit = $("btn-create");
|
|
1735
|
+
if (submit) {
|
|
1736
|
+
submit.disabled = true;
|
|
1737
|
+
submit.setAttribute("aria-busy", "true");
|
|
1738
|
+
}
|
|
1739
|
+
const resetSubmit = () => {
|
|
1740
|
+
S.creatingTask = false;
|
|
1741
|
+
if (submit) {
|
|
1742
|
+
submit.disabled = false;
|
|
1743
|
+
submit.removeAttribute("aria-busy");
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
const msg = $("create-msg");
|
|
1822
1747
|
msg.className = "msg"; msg.textContent = t("提交中…");
|
|
1823
1748
|
const payload = {
|
|
1824
1749
|
type: $("f-type").value,
|
|
1825
1750
|
mode: $("f-mode").value,
|
|
1826
1751
|
title: $("f-title").value.trim(),
|
|
1827
1752
|
goal: $("f-goal").value.trim(),
|
|
1828
|
-
context: $("f-context").value.trim(),
|
|
1829
|
-
workdir: $("f-workdir").value.trim(),
|
|
1830
|
-
};
|
|
1831
|
-
if (!payload.goal) {
|
|
1832
|
-
msg.className = "msg err";
|
|
1833
|
-
msg.textContent = t("请先填写目标");
|
|
1834
|
-
$("f-goal").focus();
|
|
1835
|
-
resetSubmit();
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
1753
|
+
context: $("f-context").value.trim(),
|
|
1754
|
+
workdir: $("f-workdir").value.trim(),
|
|
1755
|
+
};
|
|
1756
|
+
if (!payload.goal) {
|
|
1757
|
+
msg.className = "msg err";
|
|
1758
|
+
msg.textContent = t("请先填写目标");
|
|
1759
|
+
$("f-goal").focus();
|
|
1760
|
+
resetSubmit();
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
1839
1764
|
else unhideSideDir(payload.workdir); // 在已移除的目录新建任务 → 自动恢复显示
|
|
1840
1765
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
1841
1766
|
// 附件与代码版本:任务创建时服务端把待提交附件移入 _attachments/ 并注入上下文
|
|
@@ -1853,8 +1778,8 @@ async function createTask() {
|
|
|
1853
1778
|
payload.resume = { agent: resumeAgent, session: sid,
|
|
1854
1779
|
preview: opt ? opt.textContent : "", project: (hit && hit.project) || "" };
|
|
1855
1780
|
}
|
|
1856
|
-
const flow = flowById(payload.type) || {};
|
|
1857
|
-
const _eng = flow.engine;
|
|
1781
|
+
const flow = flowById(payload.type) || {};
|
|
1782
|
+
const _eng = flow.engine;
|
|
1858
1783
|
if (_eng === "code") {
|
|
1859
1784
|
payload.verify_command = $("f-verify").value.trim();
|
|
1860
1785
|
} else if (_eng === "direct") {
|
|
@@ -1863,21 +1788,22 @@ async function createTask() {
|
|
|
1863
1788
|
payload.manuscript = $("f-manuscript").value.trim() || "manuscript.md";
|
|
1864
1789
|
payload.rounds = parseInt($("f-rounds").value, 10) || 2;
|
|
1865
1790
|
payload.threshold = parseFloat($("f-threshold").value) || 7.0;
|
|
1791
|
+
payload.best_of = Math.max(1, Math.min(3, parseInt($("f-bestof").value, 10) || 1));
|
|
1866
1792
|
const rubric = $("f-rubric").value.trim();
|
|
1867
1793
|
if (rubric) payload.rubric = rubric.split(/[,,、]/).map((s) => s.trim()).filter(Boolean);
|
|
1868
1794
|
const ch = parseInt($("f-chapters").value, 10);
|
|
1869
|
-
if (ch >= 2) {
|
|
1870
|
-
payload.serial = {
|
|
1871
|
-
chapters: ch,
|
|
1872
|
-
words_per_chapter: parseInt($("f-words-per-ch").value, 10) || 2500,
|
|
1873
|
-
variants: Math.max(1, Math.min(3, parseInt($("f-variants").value, 10) || 1)),
|
|
1874
|
-
};
|
|
1875
|
-
} else if (flow.serial) {
|
|
1876
|
-
// 显式 null 覆盖 serial_novel 的流程默认,空章节就是单稿件。
|
|
1877
|
-
payload.serial = null;
|
|
1878
|
-
}
|
|
1879
|
-
const initialBible = ($("f-bible") || {}).value ? $("f-bible").value.trim() : "";
|
|
1880
|
-
if (initialBible && flow.serial && payload.serial) payload.story_bible = initialBible;
|
|
1795
|
+
if (ch >= 2) {
|
|
1796
|
+
payload.serial = {
|
|
1797
|
+
chapters: ch,
|
|
1798
|
+
words_per_chapter: parseInt($("f-words-per-ch").value, 10) || 2500,
|
|
1799
|
+
variants: Math.max(1, Math.min(3, parseInt($("f-variants").value, 10) || 1)),
|
|
1800
|
+
};
|
|
1801
|
+
} else if (flow.serial) {
|
|
1802
|
+
// 显式 null 覆盖 serial_novel 的流程默认,空章节就是单稿件。
|
|
1803
|
+
payload.serial = null;
|
|
1804
|
+
}
|
|
1805
|
+
const initialBible = ($("f-bible") || {}).value ? $("f-bible").value.trim() : "";
|
|
1806
|
+
if (initialBible && flow.serial && payload.serial) payload.story_bible = initialBible;
|
|
1881
1807
|
const critics = Array.from($("f-critics").querySelectorAll("input:checked")).map((i) => i.value);
|
|
1882
1808
|
if (critics.length) payload.critics = critics;
|
|
1883
1809
|
}
|
|
@@ -1891,12 +1817,12 @@ async function createTask() {
|
|
|
1891
1817
|
$("f-goal").value = "";
|
|
1892
1818
|
if ($("f-bible")) $("f-bible").value = "";
|
|
1893
1819
|
S.atts = []; renderAttachChips(); // 附件已移交任务待提交区,清空本地列表
|
|
1894
|
-
} catch (e) {
|
|
1895
|
-
msg.className = "msg err"; msg.textContent = e.message;
|
|
1896
|
-
} finally {
|
|
1897
|
-
resetSubmit();
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
msg.className = "msg err"; msg.textContent = e.message;
|
|
1822
|
+
} finally {
|
|
1823
|
+
resetSubmit();
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1900
1826
|
|
|
1901
1827
|
/* ---------------------------------------------------------- 附件(截图/文件) */
|
|
1902
1828
|
/* 待提交附件:选中/粘贴即上传到服务端 pending 区,创建任务时随 payload 落盘
|
|
@@ -1910,14 +1836,25 @@ const ATT_CAP_OFFICE = 24 * 1024 * 1024; // Office 文档放宽到 24MB
|
|
|
1910
1836
|
function renderAttachChips() {
|
|
1911
1837
|
const box = $("att-chips");
|
|
1912
1838
|
if (!box) return;
|
|
1913
|
-
box.innerHTML = (S.atts || []).map((a) =>
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
'<button class="ax"
|
|
1919
|
-
|
|
1920
|
-
|
|
1839
|
+
box.innerHTML = (S.atts || []).map((a) => {
|
|
1840
|
+
const name = String(a.name || "attachment");
|
|
1841
|
+
const id = String(a.id || "");
|
|
1842
|
+
const image = /^image\//i.test(String(a.mime || "")) ||
|
|
1843
|
+
/\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(name);
|
|
1844
|
+
const remove = '<button type="button" class="ax" data-att-remove="' + esc(id) +
|
|
1845
|
+
'" title="' + esc(t("移除")) + '" aria-label="' + esc(t("移除")) + '">' +
|
|
1846
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-x"></use></svg></button>';
|
|
1847
|
+
if (image && id) {
|
|
1848
|
+
const src = "/api/attachments/" + encodeURIComponent(id) + qsAuth();
|
|
1849
|
+
return '<span class="att-chip att-image" title="' + esc(name) + '">' +
|
|
1850
|
+
'<img src="' + esc(src) + '" alt="' + esc(name) + '" loading="lazy" draggable="false">' +
|
|
1851
|
+
'<span class="att-image-name">' + esc(name) + "</span>" + remove + "</span>";
|
|
1852
|
+
}
|
|
1853
|
+
return '<span class="att-chip" title="' + esc(name) + '">' +
|
|
1854
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-paperclip"></use></svg>' +
|
|
1855
|
+
"<span>" + esc(name) + "</span>" +
|
|
1856
|
+
" <i>" + fmtSize(a.size) + "</i>" + remove + "</span>";
|
|
1857
|
+
}).join("");
|
|
1921
1858
|
}
|
|
1922
1859
|
|
|
1923
1860
|
function fmtSize(n) {
|
|
@@ -2009,7 +1946,9 @@ async function probeGit() {
|
|
|
2009
1946
|
const prev = sel.value;
|
|
2010
1947
|
const opt = (v, label) => '<option value="' + esc(v) + '">' + esc(label) + "</option>";
|
|
2011
1948
|
const groups = [];
|
|
2012
|
-
|
|
1949
|
+
// 首选项=跟随当前分支:胶囊上直接显示分支名(参考工作区选择器的「main ▾」),
|
|
1950
|
+
// 悬停胶囊看完整说明(renderGitHint 写 row.title);换分支在「分支」组里选
|
|
1951
|
+
groups.push(opt("", info.branch || "HEAD"));
|
|
2013
1952
|
groups.push('<optgroup label="' + esc(t("分支")) + '">'
|
|
2014
1953
|
+ (info.branches || []).map((b) => opt("branch:" + b, b)).join("") + "</optgroup>");
|
|
2015
1954
|
if ((info.tags || []).length) {
|
|
@@ -2037,10 +1976,69 @@ function renderGitHint() {
|
|
|
2037
1976
|
const act = kind
|
|
2038
1977
|
? t(";运行时将从「%1」检出任务分支 codebee/<任务ID>").replace("%1", rev)
|
|
2039
1978
|
: "";
|
|
2040
|
-
|
|
1979
|
+
const text = base + dirty + act;
|
|
1980
|
+
$("git-hint").innerHTML = esc(text);
|
|
1981
|
+
// 极简:提示行不再常驻,分支胶囊的悬停 title 承载同一份详情(剥掉 innerHTML 用的实体)
|
|
1982
|
+
const row = $("row-git");
|
|
1983
|
+
if (row) row.title = text.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
2041
1984
|
}
|
|
2042
1985
|
window.probeGit = probeGit;
|
|
2043
1986
|
|
|
1987
|
+
/* ---- 工作目录「最近文件夹」下拉:默认选中一个目录,▾ 展开最近 5 个 + 浏览/用默认 ---- */
|
|
1988
|
+
function wdRecents() {
|
|
1989
|
+
const st = S.state || {};
|
|
1990
|
+
const seen = new Set(); const out = [];
|
|
1991
|
+
for (const tk of (st.tasks || []).concat(st.archived_tasks || [])) {
|
|
1992
|
+
const wd = (tk.workdir || "").trim();
|
|
1993
|
+
if (!wd || seen.has(wd)) continue;
|
|
1994
|
+
seen.add(wd); out.push(wd);
|
|
1995
|
+
if (out.length >= 5) break;
|
|
1996
|
+
}
|
|
1997
|
+
return out;
|
|
1998
|
+
}
|
|
1999
|
+
window.toggleWdMenu = function (ev) {
|
|
2000
|
+
if (ev) ev.stopPropagation();
|
|
2001
|
+
const menu = $("wd-menu");
|
|
2002
|
+
if (!menu) return;
|
|
2003
|
+
if (!menu.classList.contains("hidden")) { menu.classList.add("hidden"); return; }
|
|
2004
|
+
const cur = (($("f-workdir") || {}).value || "").trim();
|
|
2005
|
+
const eff = ((S.settings || {}).default_workdir_effective || "").trim();
|
|
2006
|
+
const base = (p) => p.split(/[\\/]/).filter(Boolean).pop() || p;
|
|
2007
|
+
const row = (p, label, on) =>
|
|
2008
|
+
'<button type="button" class="wd-item' + (on ? " on" : "") + '" data-wd="' + esc(p) +
|
|
2009
|
+
'" title="' + esc(p) + '">' +
|
|
2010
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-folder"></use></svg>' +
|
|
2011
|
+
"<span>" + esc(label) + "</span>" +
|
|
2012
|
+
(on ? '<svg class="ico wd-check" aria-hidden="true"><use href="#i-check"></use></svg>' : "") +
|
|
2013
|
+
"</button>";
|
|
2014
|
+
let html = wdRecents().map((p) => row(p, base(p), p === cur)).join("");
|
|
2015
|
+
html += '<div class="wd-sep"></div>' +
|
|
2016
|
+
'<button type="button" class="wd-item" data-wd-act="browse">' +
|
|
2017
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-folder"></use></svg>' +
|
|
2018
|
+
"<span>" + esc(t("浏览本机目录…")) + "</span></button>" +
|
|
2019
|
+
'<button type="button" class="wd-item" data-wd-act="default"' + (eff ? "" : " disabled") + ">" +
|
|
2020
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-refresh"></use></svg>' +
|
|
2021
|
+
"<span>" + esc(t("用默认路径")) + "</span></button>";
|
|
2022
|
+
menu.innerHTML = html;
|
|
2023
|
+
menu.classList.remove("hidden");
|
|
2024
|
+
};
|
|
2025
|
+
window.wdMenuPick = function (item) {
|
|
2026
|
+
const inp = $("f-workdir"), menu = $("wd-menu");
|
|
2027
|
+
if (!item || !inp || item.disabled) return;
|
|
2028
|
+
if (item.dataset.wdAct === "browse") {
|
|
2029
|
+
if (menu) menu.classList.add("hidden");
|
|
2030
|
+
window.pickFolder("f-workdir");
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
inp.value = item.dataset.wdAct === "default"
|
|
2034
|
+
? ((S.settings || {}).default_workdir_effective || "")
|
|
2035
|
+
: (item.dataset.wd || "");
|
|
2036
|
+
inp.dispatchEvent(new Event("input", { bubbles: true })); // queueGitProbe 跟上换目录
|
|
2037
|
+
inp.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2038
|
+
if (menu) menu.classList.add("hidden");
|
|
2039
|
+
inp.focus();
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2044
2042
|
/* ---------------------------------------------------------- 会话延续 */
|
|
2045
2043
|
/* 后端支持会话扫描的工具集合(/api/sessions 的键);决定继续会话下拉出现哪些工具 */
|
|
2046
2044
|
async function refreshSessionAgents() {
|
|
@@ -2166,59 +2164,59 @@ function openCtxMenu(x, y, items) {
|
|
|
2166
2164
|
};
|
|
2167
2165
|
}
|
|
2168
2166
|
|
|
2169
|
-
function closeCtxMenu() {
|
|
2170
|
-
const menu = $("ctx-menu");
|
|
2171
|
-
if (menu && !menu.classList.contains("hidden")) menu.classList.add("hidden");
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
/* 任务行的「⋯」与右键共用同一组动作,避免两个入口越改越不一样。
|
|
2175
|
-
* det 是 .stask 元素;菜单位置由调用方提供,缺省时贴在行右下角。 */
|
|
2176
|
-
function taskContextItems(det) {
|
|
2177
|
-
const taskId = det && det.dataset ? (det.dataset.task || "") : "";
|
|
2178
|
-
const runId = det && det.dataset ? (det.dataset.run || "") : "";
|
|
2179
|
-
const items = [];
|
|
2180
|
-
if (runId) items.push({ label: t("打开详情"), fn: () => sideOpenRun(runId) });
|
|
2181
|
-
if (taskId) {
|
|
2182
|
-
items.push("-");
|
|
2183
|
-
items.push({ label: t("打开工作目录"), fn: () => revealPath("tasks", taskId, true) });
|
|
2184
|
-
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2185
|
-
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2186
|
-
const st = det.dataset.status || "";
|
|
2187
|
-
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2188
|
-
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2189
|
-
fn: () => newFromTask(taskId) });
|
|
2190
|
-
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2191
|
-
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
2192
|
-
items.push({ label: t("继续连载(新任务)"), fn: () => continueSerial(taskId) });
|
|
2193
|
-
items.push({ label: t("重命名任务"), fn: () => renameTask(taskId) });
|
|
2194
|
-
const archived = archivedTaskIds().has(taskId);
|
|
2195
|
-
items.push({ label: archived ? t("取消归档") : t("归档"), fn: () => archiveTask(taskId, !archived) });
|
|
2196
|
-
items.push({ label: t("删除任务"), danger: true, fn: () => deleteTask(taskId) });
|
|
2197
|
-
} else if (runId) {
|
|
2198
|
-
items.push("-");
|
|
2199
|
-
items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2200
|
-
items.push({ label: t("删除记录"), danger: true, fn: () => deleteRun(runId) });
|
|
2201
|
-
}
|
|
2202
|
-
return items;
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
window.openTaskActions = function (det, ev) {
|
|
2206
|
-
if (!det) return;
|
|
2207
|
-
const r = det.getBoundingClientRect();
|
|
2208
|
-
const x = ev && Number.isFinite(ev.clientX) ? ev.clientX : r.right - 8;
|
|
2209
|
-
const y = ev && Number.isFinite(ev.clientY) ? ev.clientY : r.bottom;
|
|
2210
|
-
openCtxMenu(x, y, taskContextItems(det));
|
|
2211
|
-
};
|
|
2212
|
-
|
|
2213
|
-
function bindCtxMenus() {
|
|
2167
|
+
function closeCtxMenu() {
|
|
2168
|
+
const menu = $("ctx-menu");
|
|
2169
|
+
if (menu && !menu.classList.contains("hidden")) menu.classList.add("hidden");
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/* 任务行的「⋯」与右键共用同一组动作,避免两个入口越改越不一样。
|
|
2173
|
+
* det 是 .stask 元素;菜单位置由调用方提供,缺省时贴在行右下角。 */
|
|
2174
|
+
function taskContextItems(det) {
|
|
2175
|
+
const taskId = det && det.dataset ? (det.dataset.task || "") : "";
|
|
2176
|
+
const runId = det && det.dataset ? (det.dataset.run || "") : "";
|
|
2177
|
+
const items = [];
|
|
2178
|
+
if (runId) items.push({ label: t("打开详情"), fn: () => sideOpenRun(runId) });
|
|
2179
|
+
if (taskId) {
|
|
2180
|
+
items.push("-");
|
|
2181
|
+
items.push({ label: t("打开工作目录"), fn: () => revealPath("tasks", taskId, true) });
|
|
2182
|
+
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2183
|
+
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2184
|
+
const st = det.dataset.status || "";
|
|
2185
|
+
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2186
|
+
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2187
|
+
fn: () => newFromTask(taskId) });
|
|
2188
|
+
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2189
|
+
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
2190
|
+
items.push({ label: t("继续连载(新任务)"), fn: () => continueSerial(taskId) });
|
|
2191
|
+
items.push({ label: t("重命名任务"), fn: () => renameTask(taskId) });
|
|
2192
|
+
const archived = archivedTaskIds().has(taskId);
|
|
2193
|
+
items.push({ label: archived ? t("取消归档") : t("归档"), fn: () => archiveTask(taskId, !archived) });
|
|
2194
|
+
items.push({ label: t("删除任务"), danger: true, fn: () => deleteTask(taskId) });
|
|
2195
|
+
} else if (runId) {
|
|
2196
|
+
items.push("-");
|
|
2197
|
+
items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2198
|
+
items.push({ label: t("删除记录"), danger: true, fn: () => deleteRun(runId) });
|
|
2199
|
+
}
|
|
2200
|
+
return items;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
window.openTaskActions = function (det, ev) {
|
|
2204
|
+
if (!det) return;
|
|
2205
|
+
const r = det.getBoundingClientRect();
|
|
2206
|
+
const x = ev && Number.isFinite(ev.clientX) ? ev.clientX : r.right - 8;
|
|
2207
|
+
const y = ev && Number.isFinite(ev.clientY) ? ev.clientY : r.bottom;
|
|
2208
|
+
openCtxMenu(x, y, taskContextItems(det));
|
|
2209
|
+
};
|
|
2210
|
+
|
|
2211
|
+
function bindCtxMenus() {
|
|
2214
2212
|
// 侧栏任务树:右键任务 → 详情/目录/重试/重命名/归档/删除;管理类运行 → 详情/目录/删除记录
|
|
2215
2213
|
$("side-tasks").addEventListener("contextmenu", (e) => {
|
|
2216
2214
|
// .stask 在 .sdir 内部:先判任务行,点到才算文件夹自己的菜单
|
|
2217
|
-
const det = e.target.closest(".stask");
|
|
2218
|
-
if (det) {
|
|
2219
|
-
e.preventDefault();
|
|
2220
|
-
openCtxMenu(e.clientX, e.clientY, taskContextItems(det));
|
|
2221
|
-
return;
|
|
2215
|
+
const det = e.target.closest(".stask");
|
|
2216
|
+
if (det) {
|
|
2217
|
+
e.preventDefault();
|
|
2218
|
+
openCtxMenu(e.clientX, e.clientY, taskContextItems(det));
|
|
2219
|
+
return;
|
|
2222
2220
|
}
|
|
2223
2221
|
// 文件夹行(ZCode 桌面端式样):新建任务到该目录 / 打开工作目录 / 复制路径 / 移除。
|
|
2224
2222
|
// 「其他」是兜底展示(无主运行的聚合),不是真实目录,不弹菜单。
|
|
@@ -2254,10 +2252,11 @@ function bindCtxMenus() {
|
|
|
2254
2252
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeCtxMenu(); });
|
|
2255
2253
|
}
|
|
2256
2254
|
|
|
2257
|
-
/* -------------------------------------------------
|
|
2258
|
-
*
|
|
2259
|
-
*
|
|
2260
|
-
*
|
|
2255
|
+
/* ------------------------------------------------- 文件夹选择(工作目录「选择…」/点输入框)
|
|
2256
|
+
* 首选系统原生对话框:服务端 tkinter 子进程弹真窗口(/api/pick_folder,仅本机),
|
|
2257
|
+
* 前端只收回填路径。机器没有 tkinter(fallback=true)时回落网页目录弹框:
|
|
2258
|
+
* /api/browse 浏览(服务端只给目录名、前端拼绝对路径),选定=行内「选这个」或
|
|
2259
|
+
* 底部「使用当前目录」。远端设备两条通道都被 403 拒:点输入框不再弹框,继续手输。 */
|
|
2261
2260
|
const pickerSt = { target: "", cwd: "", parent: "", local: null }; // local: null=未知 true=本机 false=远端
|
|
2262
2261
|
|
|
2263
2262
|
function pickerRender(r) {
|
|
@@ -2305,19 +2304,35 @@ async function pickerBrowse(p) {
|
|
|
2305
2304
|
return true;
|
|
2306
2305
|
}
|
|
2307
2306
|
|
|
2307
|
+
/* 网页目录弹框(原生对话框不可用时的回落通道) */
|
|
2308
|
+
async function webPickFolder(targetId, cur) {
|
|
2309
|
+
openModal(t("选择文件夹"), '<div id="pk-body" class="hint">' + esc(t("正在读取目录…")) + "</div>",
|
|
2310
|
+
'<button class="ghost" onclick="closeModal();pkRefocus()">' + esc(t("取消")) + "</button>" +
|
|
2311
|
+
'<button class="primary" onclick="pickUseP(pickerCwd())">' + esc(t("使用当前目录")) + "</button>");
|
|
2312
|
+
await pickerBrowse(cur || "__drives__");
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2308
2315
|
window.pickFolder = async function (targetId, fromClick) {
|
|
2309
2316
|
if (fromClick && pickerSt.local === false) return; // 远端已确认被拒:点击不打扰
|
|
2310
2317
|
pickerSt.target = targetId;
|
|
2311
2318
|
const cur = (($(targetId) || {}).value || "").trim();
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2319
|
+
let r = null;
|
|
2320
|
+
try {
|
|
2321
|
+
r = await api("/api/pick_folder", { method: "POST",
|
|
2322
|
+
body: JSON.stringify({ initial: cur, title: t("选择文件夹") }) });
|
|
2323
|
+
} catch (e) {
|
|
2324
|
+
if (/仅限本机/.test(e.message || "")) {
|
|
2325
|
+
pickerSt.local = false;
|
|
2326
|
+
if (fromClick) toast(t("目录选择仅限本机使用,请手动输入路径"), true);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
toast(t("操作失败:") + (e.message || e), true);
|
|
2330
|
+
return;
|
|
2320
2331
|
}
|
|
2332
|
+
if (r.busy) { toast(t("已有一个选择窗口正在等待"), true); return; }
|
|
2333
|
+
if (r.fallback) { await webPickFolder(targetId, cur); return; }
|
|
2334
|
+
if (r.path) pickUseP(r.path);
|
|
2335
|
+
else window.pkRefocus(); // 用户取消:焦点还给输入框,方便手输
|
|
2321
2336
|
};
|
|
2322
2337
|
window.pickEnterP = (p) => pickerBrowse(p);
|
|
2323
2338
|
window.pickParentP = () => pickerBrowse(pickerSt.parent || "__drives__");
|
|
@@ -2431,6 +2446,7 @@ function newFromTask(id) {
|
|
|
2431
2446
|
$("f-manuscript").value = tk.manuscript || "manuscript.md";
|
|
2432
2447
|
$("f-rounds").value = tk.rounds || 2;
|
|
2433
2448
|
$("f-threshold").value = (tk.threshold != null ? tk.threshold : 7.0);
|
|
2449
|
+
$("f-bestof").value = tk.best_of || 1;
|
|
2434
2450
|
$("f-rubric").value = (tk.rubric || []).join(", ");
|
|
2435
2451
|
// 连载参数只带批次设置:不带 start_chapter/continues(那是「继续连载」的衔接语义)
|
|
2436
2452
|
$("f-chapters").value = tk.serial ? tk.serial.chapters : "";
|
|
@@ -2883,19 +2899,19 @@ function renderSideTasks() {
|
|
|
2883
2899
|
// 二级任务行:单行即全部——点击主栏直开任务详情。
|
|
2884
2900
|
// 内嵌步骤层已砍:步骤/运行明细在任务详情里更全,侧栏只留状态字形 + 徽章 + 相对时间。
|
|
2885
2901
|
// 右键菜单仍吃 data-task / data-run;主栏开着某任务详情时行保持高亮。
|
|
2886
|
-
const row = (g) => {
|
|
2887
|
-
if (g.active) g.status = "running";
|
|
2888
|
-
const sel = (g.key === S.detailTaskKey || g.runIds.indexOf(S.detailRunId) >= 0) ? " active" : "";
|
|
2889
|
-
return '<div class="stask' + sel + (g.archived ? " archived" : "") + '" tabindex="0" role="button" data-key="' + esc(g.key) +
|
|
2890
|
-
'" data-task="' + esc(g.taskId || "") + '" data-run="' + esc(g.runIds[0] || "") +
|
|
2891
|
-
'" data-status="' + esc(g.status || "") + '">' + staskGlyph(g.status) +
|
|
2892
|
-
'<span class="t">' + esc(g.title) + "</span>" +
|
|
2893
|
-
(g.archived ? '<span class="sbadge archb">' + t("已归档") + "</span>" : "") +
|
|
2894
|
-
(g.verdict ? '<span class="sbadge">' + t("待裁决") + "</span>" : "") +
|
|
2895
|
-
'<span class="tm">' + esc(relTime(g.time)) + "</span>" +
|
|
2896
|
-
'<button class="stask-more" type="button" aria-haspopup="menu" aria-label="' + esc(t("任务操作")) + '" title="' + esc(t("任务操作")) +
|
|
2897
|
-
'" onclick="event.stopPropagation();openTaskActions(this.closest(\'.stask\'), event)">⋯</button></div>';
|
|
2898
|
-
};
|
|
2902
|
+
const row = (g) => {
|
|
2903
|
+
if (g.active) g.status = "running";
|
|
2904
|
+
const sel = (g.key === S.detailTaskKey || g.runIds.indexOf(S.detailRunId) >= 0) ? " active" : "";
|
|
2905
|
+
return '<div class="stask' + sel + (g.archived ? " archived" : "") + '" tabindex="0" role="button" data-key="' + esc(g.key) +
|
|
2906
|
+
'" data-task="' + esc(g.taskId || "") + '" data-run="' + esc(g.runIds[0] || "") +
|
|
2907
|
+
'" data-status="' + esc(g.status || "") + '">' + staskGlyph(g.status) +
|
|
2908
|
+
'<span class="t">' + esc(g.title) + "</span>" +
|
|
2909
|
+
(g.archived ? '<span class="sbadge archb">' + t("已归档") + "</span>" : "") +
|
|
2910
|
+
(g.verdict ? '<span class="sbadge">' + t("待裁决") + "</span>" : "") +
|
|
2911
|
+
'<span class="tm">' + esc(relTime(g.time)) + "</span>" +
|
|
2912
|
+
'<button class="stask-more" type="button" aria-haspopup="menu" aria-label="' + esc(t("任务操作")) + '" title="' + esc(t("任务操作")) +
|
|
2913
|
+
'" onclick="event.stopPropagation();openTaskActions(this.closest(\'.stask\'), event)">⋯</button></div>';
|
|
2914
|
+
};
|
|
2899
2915
|
// 一级文件夹行:折叠箭头 + 目录图标 + 末段名(hover 显全路径)+ 任务计数徽标
|
|
2900
2916
|
box.innerHTML = dirs.map((d) => {
|
|
2901
2917
|
const isOrphan = d.dir === ORPHAN;
|
|
@@ -2949,7 +2965,8 @@ function jumpToRun(id) {
|
|
|
2949
2965
|
* 聚合该任务全部 run 的步骤;不允许任何行点了没反应。
|
|
2950
2966
|
* 无主运行行(任务已删/管理运行,data-task 为空)→ 直开它自己的运行详情;
|
|
2951
2967
|
* 已归档任务 → toast 指路(归档行先取消归档);
|
|
2952
|
-
*
|
|
2968
|
+
* 从未跑过 → toast 说明;跑过但最新 run 排队中(自动续跑退避窗口)→ 照常开
|
|
2969
|
+
* 任务详情,能回看历史轮次的步骤与日志。 */
|
|
2953
2970
|
function sideRowActivate(det) {
|
|
2954
2971
|
const taskId = det.dataset.task || "", runId = det.dataset.run || "";
|
|
2955
2972
|
if (!taskId) {
|
|
@@ -2962,7 +2979,10 @@ function sideRowActivate(det) {
|
|
|
2962
2979
|
toast(arch ? t("已归档任务:先在右键菜单里取消归档,再看任务详情") : t("任务不存在或已删除"), true);
|
|
2963
2980
|
return;
|
|
2964
2981
|
}
|
|
2965
|
-
if (!inspEligible(taskId)) {
|
|
2982
|
+
if (!inspEligible(taskId)) {
|
|
2983
|
+
const lr = ((S.state || {}).task_latest || {})[taskId];
|
|
2984
|
+
if (!lr) { toast(t("该任务还没跑过,还没有任务详情"), true); return; }
|
|
2985
|
+
}
|
|
2966
2986
|
sideOpenTask(taskId);
|
|
2967
2987
|
}
|
|
2968
2988
|
|
|
@@ -3038,30 +3058,42 @@ function drawTaskDetail(key, runs) {
|
|
|
3038
3058
|
// 取消收尾把僵尸步骤落成「已取消」时要立即重画,不等条数变化;
|
|
3039
3059
|
// 作品信息状态入签名:后台一键生成 running→done 要立刻反映到成果区面板
|
|
3040
3060
|
const bmTask = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
3061
|
+
// 自动续跑退避相位入签名:预定入队时刻过了之后 chip 要从「将于 HH:MM」翻回「排队中」
|
|
3062
|
+
const lr0 = runs[0] || {};
|
|
3063
|
+
const resumePending = (lr0.resume_enqueue_at &&
|
|
3064
|
+
Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
|
|
3041
3065
|
const sig = JSON.stringify(runs.map((r) => [r.id, r.status, (r.steps || []).length,
|
|
3042
3066
|
(r.steps || []).map((s) => s.status).join(""),
|
|
3043
3067
|
(r.messages || []).length, (r.messages || []).filter((m) => !m.consumed).length])
|
|
3044
|
-
.concat([JSON.stringify((bmTask || {}).book_meta || null)]));
|
|
3068
|
+
.concat([JSON.stringify((bmTask || {}).book_meta || null), resumePending]));
|
|
3045
3069
|
if (sig === S.taskSig) return;
|
|
3046
3070
|
S.taskSig = sig;
|
|
3047
3071
|
const latest = runs[0]; // runs 新→旧
|
|
3048
3072
|
const ordered = runs.slice(); // 详情按最近运行优先,方便排查
|
|
3049
3073
|
const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
|
|
3050
3074
|
const active = runs.some((r) => r.status === "running" || r.status === "queued");
|
|
3051
|
-
|
|
3075
|
+
// 活跃态细分真实状态:排队里还分「等并发」和「自动续跑退避(预定 HH:MM 入队)」,
|
|
3076
|
+
// 后者在 chip 上写明下一轮何时起跑,别让 5 分钟退避窗口看起来像卡死(Z.ai 误伤案)
|
|
3077
|
+
const activeRun0 = runs.find((r) => r.status === "running" || r.status === "queued");
|
|
3078
|
+
const st = activeRun0 ? activeRun0.status : latest.status;
|
|
3079
|
+
const resumeIn = (st === "queued" && resumePending)
|
|
3080
|
+
? String(latest.resume_enqueue_at).slice(11, 16) : "";
|
|
3052
3081
|
$("rd-title").textContent = latest.title || key;
|
|
3053
3082
|
const chip = $("rd-status");
|
|
3054
3083
|
chip.className = "chip " + st;
|
|
3055
|
-
chip.textContent =
|
|
3084
|
+
chip.textContent = resumeIn
|
|
3085
|
+
? t("将于 ") + resumeIn + t(" 自动续跑(第 ") + (Number(latest.auto_resumes) || 0) + t(" 次)")
|
|
3086
|
+
: ({ queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") }[st] || st);
|
|
3056
3087
|
const bpt = $("btn-pause");
|
|
3057
3088
|
if (bpt) bpt.classList.add("hidden");
|
|
3058
3089
|
$("btn-delete").classList.add("hidden");
|
|
3059
3090
|
$("btn-retry").classList.toggle("hidden", !(latest.task_id && (latest.status === "failed" || latest.status === "cancelled")));
|
|
3091
|
+
$("btn-talk").classList.toggle("hidden", !(latest.task_id && !chatEngineIsDirect(latest)));
|
|
3060
3092
|
setEditRetry(latest.task_id, latest.status,
|
|
3061
3093
|
!!((S.state || {}).tasks || []).some((x) => x.id === latest.task_id));
|
|
3062
|
-
S.lastRun = latest;
|
|
3063
|
-
renderRunOutcome(latest, runs);
|
|
3064
|
-
renderDetailOverview(latest, runs, bmTask);
|
|
3094
|
+
S.lastRun = latest;
|
|
3095
|
+
renderRunOutcome(latest, runs);
|
|
3096
|
+
renderDetailOverview(latest, runs, bmTask);
|
|
3065
3097
|
// 指挥区指向最新的活跃运行(无则隐藏整个指挥区,避免终态任务误导用户以为指令还能生效)
|
|
3066
3098
|
const activeRun = runs.find((r) => r.status === "queued" || r.status === "running");
|
|
3067
3099
|
// 活跃运行(含排队中)显示取消按钮;取消目标钉在活跃 run——任务级详情没有
|
|
@@ -3093,16 +3125,16 @@ function drawTaskDetail(key, runs) {
|
|
|
3093
3125
|
'<span class="chip ' + esc(r.status || "") + '">' + (statusTxt[r.status] || r.status || "") + "</span>" +
|
|
3094
3126
|
(r.error ? '<span class="rs-err" title="' + esc(r.error.slice(0, 200)) + '">' + esc(r.error.slice(0, 60)) + "</span>" : "") +
|
|
3095
3127
|
"</div>";
|
|
3096
|
-
html += (r.steps || []).slice().reverse().map((s) =>
|
|
3097
|
-
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(r.id) + '" data-log="' + esc(s.log || "") + '" title="' + esc(s.note || "") + '">' +
|
|
3128
|
+
html += (r.steps || []).slice().reverse().map((s) =>
|
|
3129
|
+
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(r.id) + '" data-log="' + esc(s.log || "") + '" title="' + esc(s.note || "") + '">' +
|
|
3098
3130
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3099
3131
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3100
3132
|
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3101
3133
|
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3102
3134
|
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3103
|
-
statusChip(s.status) +
|
|
3104
|
-
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(r.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3105
|
-
"</div>"
|
|
3135
|
+
statusChip(s.status) +
|
|
3136
|
+
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(r.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3137
|
+
"</div>"
|
|
3106
3138
|
).join("");
|
|
3107
3139
|
});
|
|
3108
3140
|
$("rd-steps").innerHTML = html || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
@@ -3245,7 +3277,7 @@ function applyStepFocus() {
|
|
|
3245
3277
|
el.classList.add("flash");
|
|
3246
3278
|
try { el.scrollIntoView({ block: "center", behavior: "smooth" }); }
|
|
3247
3279
|
catch (e) { el.scrollIntoView(); }
|
|
3248
|
-
if (el.dataset.log) toggleLog(el.dataset.runId || S.detailRunId, el.dataset.log);
|
|
3280
|
+
if (el.dataset.log) toggleLog(el.dataset.runId || S.detailRunId, el.dataset.log);
|
|
3249
3281
|
setTimeout(() => el.classList.remove("flash"), 1800);
|
|
3250
3282
|
}
|
|
3251
3283
|
|
|
@@ -3366,16 +3398,16 @@ function rdTabsSync(ctx) {
|
|
|
3366
3398
|
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (ctx.status || "") +
|
|
3367
3399
|
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0);
|
|
3368
3400
|
if (S.rdTabSig !== sig) {
|
|
3369
|
-
S.rdTabSig = sig;
|
|
3370
|
-
const avail = rdTabAvail();
|
|
3371
|
-
const direct = chatAvail && chatEngineIsDirect(S.lastRun);
|
|
3372
|
-
// 直连任务的主问题是「继续聊什么、上一轮回答是什么」——默认把对话
|
|
3373
|
-
// 放在第一视线;编排/代码任务才默认落蜂巢,先看阶段与在岗步骤。
|
|
3374
|
-
// 没跑到终态时成果分区是空的,避免打开详情先看到白板。
|
|
3375
|
-
const finishing = !ctx.running && ["done", "failed", "cancelled", "timeout"].includes(ctx.status);
|
|
3376
|
-
S.rdTab = (direct && avail.chat ? "chat" : null)
|
|
3377
|
-
|| (avail.hive ? "hive" : null)
|
|
3378
|
-
|| (avail.chat ? "chat" : null)
|
|
3401
|
+
S.rdTabSig = sig;
|
|
3402
|
+
const avail = rdTabAvail();
|
|
3403
|
+
const direct = chatAvail && chatEngineIsDirect(S.lastRun);
|
|
3404
|
+
// 直连任务的主问题是「继续聊什么、上一轮回答是什么」——默认把对话
|
|
3405
|
+
// 放在第一视线;编排/代码任务才默认落蜂巢,先看阶段与在岗步骤。
|
|
3406
|
+
// 没跑到终态时成果分区是空的,避免打开详情先看到白板。
|
|
3407
|
+
const finishing = !ctx.running && ["done", "failed", "cancelled", "timeout"].includes(ctx.status);
|
|
3408
|
+
S.rdTab = (direct && avail.chat ? "chat" : null)
|
|
3409
|
+
|| (avail.hive ? "hive" : null)
|
|
3410
|
+
|| (avail.chat ? "chat" : null)
|
|
3379
3411
|
|| (ctx.running ? (avail.hive ? "hive" : "steps")
|
|
3380
3412
|
: (ctx.gitState === "isolated" && avail.git ? "git"
|
|
3381
3413
|
: (ctx.hasResult && finishing ? "result" : "steps")));
|
|
@@ -3385,12 +3417,26 @@ function rdTabsSync(ctx) {
|
|
|
3385
3417
|
rdTabBadges();
|
|
3386
3418
|
}
|
|
3387
3419
|
|
|
3420
|
+
/* 详情头部的按需信息面板:统计和任务概览不再常驻挤占蜂巢/对话空间。 */
|
|
3421
|
+
function setRdMetaOpen(open) {
|
|
3422
|
+
const detail = $("run-detail");
|
|
3423
|
+
const pop = $("rd-meta-popover");
|
|
3424
|
+
const toggle = $("rd-more-toggle");
|
|
3425
|
+
if (!pop || !toggle) return;
|
|
3426
|
+
const on = !!open;
|
|
3427
|
+
pop.classList.toggle("hidden", !on);
|
|
3428
|
+
if (detail) detail.classList.toggle("rd-info-open", on);
|
|
3429
|
+
toggle.setAttribute("aria-expanded", on ? "true" : "false");
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3388
3432
|
/* 换一个详情目标时清空选卡状态:下一次渲染按新目标自动落位 */
|
|
3389
3433
|
function rdTabReset(pin) {
|
|
3390
3434
|
S.rdTab = pin || null;
|
|
3391
3435
|
S.rdTabSig = "";
|
|
3392
3436
|
S.rdTabPin = !!pin;
|
|
3393
3437
|
S._rdCtx = {};
|
|
3438
|
+
syncChatLogSpace(false);
|
|
3439
|
+
setRdMetaOpen(false);
|
|
3394
3440
|
}
|
|
3395
3441
|
|
|
3396
3442
|
async function renderRunDetail() {
|
|
@@ -3411,13 +3457,14 @@ async function renderRunDetail() {
|
|
|
3411
3457
|
S.cancelTargetRunId = active ? run.id : null;
|
|
3412
3458
|
$("btn-delete").classList.toggle("hidden", active);
|
|
3413
3459
|
$("btn-retry").classList.toggle("hidden", !(run.task_id && (run.status === "failed" || run.status === "cancelled")));
|
|
3460
|
+
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
3414
3461
|
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
3415
3462
|
$("btn-continue").classList.toggle("hidden",
|
|
3416
3463
|
!(rcTask && rcTask.serial && run.status !== "running" && run.status !== "queued"));
|
|
3417
3464
|
setEditRetry(run.task_id, run.status, !!rcTask);
|
|
3418
|
-
S.lastRun = run;
|
|
3419
|
-
renderRunOutcome(run);
|
|
3420
|
-
renderDetailOverview(run, [run], rcTask);
|
|
3465
|
+
S.lastRun = run;
|
|
3466
|
+
renderRunOutcome(run);
|
|
3467
|
+
renderDetailOverview(run, [run], rcTask);
|
|
3421
3468
|
const bp = $("btn-pause");
|
|
3422
3469
|
if (bp) { bp.classList.toggle("hidden", !active);
|
|
3423
3470
|
bp.textContent = run.paused ? t("继续执行") : t("暂停"); }
|
|
@@ -3433,20 +3480,20 @@ async function renderRunDetail() {
|
|
|
3433
3480
|
'<span class="stat tasksum hidden" id="rd-meta-task"></span>';
|
|
3434
3481
|
if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
|
|
3435
3482
|
renderPlan(run);
|
|
3436
|
-
// 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
|
|
3437
|
-
// 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
|
|
3438
|
-
$("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
|
|
3439
|
-
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(run.id) + '" data-log="' + esc(s.log || "") +
|
|
3440
|
-
'" title="' + esc(s.note || "") + '">' +
|
|
3483
|
+
// 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
|
|
3484
|
+
// 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
|
|
3485
|
+
$("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
|
|
3486
|
+
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(run.id) + '" data-log="' + esc(s.log || "") +
|
|
3487
|
+
'" title="' + esc(s.note || "") + '">' +
|
|
3441
3488
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3442
3489
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3443
|
-
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3444
|
-
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3445
|
-
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3446
|
-
statusChip(s.status) +
|
|
3447
|
-
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(run.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3448
|
-
"</div>"
|
|
3449
|
-
).join("") || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
3490
|
+
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3491
|
+
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3492
|
+
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3493
|
+
statusChip(s.status) +
|
|
3494
|
+
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(run.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3495
|
+
"</div>"
|
|
3496
|
+
).join("") || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
3450
3497
|
applyStepFocus();
|
|
3451
3498
|
// 报告
|
|
3452
3499
|
if (run.status === "done" || run.report) {
|
|
@@ -3924,6 +3971,8 @@ function renderGitWbMain(d, task) {
|
|
|
3924
3971
|
'<button class="ghost"' + (noRemote || ro ? " disabled" : "") + ' onclick="gitWbFetch()">' + t("抓取") + "</button>" +
|
|
3925
3972
|
'<button class="ghost"' + (noRemote || ro ? " disabled" : "") + ' onclick="gitWbPull()">' + t("拉取") + "</button>" +
|
|
3926
3973
|
'<button class="ghost"' + (noRemote || ro ? " disabled" : "") + ' onclick="gitWbPush()">' + t("推送") + "</button>" +
|
|
3974
|
+
'<button class="ghost"' + (noRemote || ro || d.detached ? " disabled" : "") + ' onclick="gitWbPrCreate()" title="' +
|
|
3975
|
+
esc(t("推送当前分支并用 gh 创建 PR(目标分支自动取 main/master)")) + '">' + t("建 PR") + "</button>" +
|
|
3927
3976
|
'<button class="ghost"' + (!hasChg || ro ? " disabled" : "") + ' onclick="gitWbStageAll()">' + t("全部暂存") + "</button>" +
|
|
3928
3977
|
'<button class="ghost"' + (!hasChg || ro ? " disabled" : "") + ' onclick="gitWbStash()" title="' +
|
|
3929
3978
|
esc(t("把未提交改动收进 stash(不含 _attachments)")) + '">' + t("收起改动") + "</button>" +
|
|
@@ -3985,6 +4034,7 @@ function renderGitWbMain(d, task) {
|
|
|
3985
4034
|
window.gitWbDiff = async function (path) {
|
|
3986
4035
|
const key = detailSideTaskKey();
|
|
3987
4036
|
if (!key || !path) return;
|
|
4037
|
+
_fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; _fpCopyText = null; // 复用单例弹窗,别带上一个文件的编辑态
|
|
3988
4038
|
_fpOpen(String(path).split("/").pop(), "",
|
|
3989
4039
|
'<button class="ghost" onclick="copyFPText()">' + esc(t("复制")) + "</button>");
|
|
3990
4040
|
let d;
|
|
@@ -3992,6 +4042,7 @@ window.gitWbDiff = async function (path) {
|
|
|
3992
4042
|
catch (e) { _fpSetBody('<div class="fp-hint">' + esc(t("diff 读取失败:") + e.message) + "</div>"); return; }
|
|
3993
4043
|
if (!filePopIsOpen()) return; // 读取期间被 Esc 关掉:别把内容又糊上去
|
|
3994
4044
|
const lines = String(d.diff || "").split("\n");
|
|
4045
|
+
_fpCopyText = (d.diff && lines.length) ? lines.join("\n") : ""; // 复制给 diff 原文,行号槽不进剪贴板
|
|
3995
4046
|
_fpSetBody((d.diff && lines.length)
|
|
3996
4047
|
? '<div class="fp-diff">' + lines.map((ln, i) => _fpDiffLine(ln, i + 1)).join("") + "</div>"
|
|
3997
4048
|
: '<div class="fp-hint">' + esc(t("该文件当前没有未提交的变更。")) + "</div>");
|
|
@@ -4016,6 +4067,13 @@ window.gitWbStageAll = function () { return _gitWbPost("stage_all", null, t("已
|
|
|
4016
4067
|
window.gitWbFetch = function () { return _gitWbPost("fetch", null, t("已抓取远程更新")); };
|
|
4017
4068
|
window.gitWbPull = function () { return _gitWbPost("pull", null, t("已拉取远程更新")); };
|
|
4018
4069
|
window.gitWbPush = function () { return _gitWbPost("push", null, t("已推送到远程")); };
|
|
4070
|
+
window.gitWbPrCreate = async function () {
|
|
4071
|
+
if (!await uiConfirm(t("把当前分支推送到远程并用 gh 创建 PR?(目标分支自动取 main/master)"), { ok: t("建 PR") })) return null;
|
|
4072
|
+
const res = await _gitWbPost("pr_create", null,
|
|
4073
|
+
(r) => (r && r.url ? t("PR 已创建:") + r.url : t("PR 已创建")));
|
|
4074
|
+
if (res && res.url) window.open(res.url, "_blank", "noopener");
|
|
4075
|
+
return res;
|
|
4076
|
+
};
|
|
4019
4077
|
window.gitWbCheckout = function (br) {
|
|
4020
4078
|
if (!br) return;
|
|
4021
4079
|
return _gitWbPost("checkout", { branch: br }, t("已切换到 ") + br);
|
|
@@ -4321,8 +4379,8 @@ function drawInspector() {
|
|
|
4321
4379
|
? arts.map((f) =>
|
|
4322
4380
|
'<a class="file-chip" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4323
4381
|
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
4324
|
-
esc(f.name + " · " + fmtSize(f.size)) + '"
|
|
4325
|
-
esc(f.name) + "
|
|
4382
|
+
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) +
|
|
4383
|
+
'" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4326
4384
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
4327
4385
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>").join("")
|
|
4328
4386
|
: '<span class="insp-hint">' + esc(t("本次运行没有在工作目录里产出新文件。")) + "</span>";
|
|
@@ -4367,8 +4425,10 @@ window.inspDiffFile = async function (path) {
|
|
|
4367
4425
|
let _fpUrls = []; // 图片预览的 objectURL,关窗统一回收
|
|
4368
4426
|
/* 编辑态:_fpRawText=开窗时取到的原文(json 展示会美化,编辑始终用原文);
|
|
4369
4427
|
* _fpSaveCtx={dir,name,mtime} 非空=该弹窗可编辑(目录文件弹窗专属,成品只读);
|
|
4370
|
-
* _fpDirty
|
|
4371
|
-
|
|
4428
|
+
* _fpDirty=编辑框有改动未保存——关窗/退出编辑先确认,防误丢;
|
|
4429
|
+
* _fpCopyText=复制按钮要吐的原文(正文是行号表格,innerText 会连行号列一起带出);
|
|
4430
|
+
* null=非文本内容,回退 innerText。 */
|
|
4431
|
+
let _fpRawText = "", _fpSaveCtx = null, _fpDirty = false, _fpCopyText = null;
|
|
4372
4432
|
|
|
4373
4433
|
function filePopIsOpen() {
|
|
4374
4434
|
const el = document.getElementById("file-pop");
|
|
@@ -4382,7 +4442,7 @@ window.filePopClose = async function () {
|
|
|
4382
4442
|
const ok = await uiConfirm(t("有未保存的修改,确定关闭?"));
|
|
4383
4443
|
if (!ok) return;
|
|
4384
4444
|
}
|
|
4385
|
-
_fpDirty = false; _fpSaveCtx = null; _fpRawText = "";
|
|
4445
|
+
_fpDirty = false; _fpSaveCtx = null; _fpRawText = ""; _fpCopyText = null;
|
|
4386
4446
|
el.classList.add("hidden");
|
|
4387
4447
|
el.querySelector(".fp-body").innerHTML = "";
|
|
4388
4448
|
el.querySelector(".fp-acts").innerHTML = "";
|
|
@@ -4429,10 +4489,15 @@ function _fpSetBody(html) {
|
|
|
4429
4489
|
el.querySelector(".fp-body").innerHTML = html;
|
|
4430
4490
|
}
|
|
4431
4491
|
|
|
4432
|
-
|
|
4492
|
+
/* 复制源:文本类正文优先给预留原文(不带行号/着色槽),其余回退 innerText */
|
|
4493
|
+
window.fpCopySource = function () {
|
|
4433
4494
|
const body = document.querySelector("#file-pop .fp-body");
|
|
4434
|
-
if (!body) return;
|
|
4435
|
-
|
|
4495
|
+
if (!body) return "";
|
|
4496
|
+
return _fpCopyText !== null ? _fpCopyText : (body.innerText || "");
|
|
4497
|
+
};
|
|
4498
|
+
|
|
4499
|
+
window.copyFPText = function () {
|
|
4500
|
+
try { navigator.clipboard.writeText(window.fpCopySource() || ""); toast(t("已复制")); } catch (e) { /* 剪贴板不可用则忽略 */ }
|
|
4436
4501
|
};
|
|
4437
4502
|
|
|
4438
4503
|
/* diff 行着色:+/− 上色、@@ 小节、头部落灰;"--- "/"+++ " 先判,避免内容行误染。
|
|
@@ -4454,7 +4519,7 @@ function _fpDiffLine(ln, no) {
|
|
|
4454
4519
|
window.fileDiffPopup = async function (path, runId) {
|
|
4455
4520
|
runId = runId || ((S.inspData || {}).run || {}).id || S.detailRunId || (S.lastRun || {}).id || "";
|
|
4456
4521
|
if (!runId) return;
|
|
4457
|
-
_fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; // diff 弹窗只读,别带上一个目录文件的编辑态
|
|
4522
|
+
_fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; _fpCopyText = null; // diff 弹窗只读,别带上一个目录文件的编辑态
|
|
4458
4523
|
let sub = "";
|
|
4459
4524
|
for (const fs of [((S.inspData || {}).changes || {}).files, ((S.lastRun || {}).changes || {}).files]) {
|
|
4460
4525
|
const f = (fs || []).find((x) => x.path === path);
|
|
@@ -4466,6 +4531,7 @@ window.fileDiffPopup = async function (path, runId) {
|
|
|
4466
4531
|
catch (e) { _fpSetBody('<div class="fp-hint">' + esc(t("diff 读取失败:") + e.message) + "</div>"); return; }
|
|
4467
4532
|
if (!filePopIsOpen()) return; // 读取期间被 Esc 关掉:别把内容又糊上去
|
|
4468
4533
|
const lines = _sliceDiff(((d.run || {}).changes || {}).diff || "", path);
|
|
4534
|
+
_fpCopyText = lines.length ? lines.join("\n") : ""; // 复制给切片原文,行号槽(.fp-no)不进剪贴板
|
|
4469
4535
|
_fpSetBody(lines.length
|
|
4470
4536
|
? '<div class="fp-diff">' + lines.map((ln, idx) => _fpDiffLine(ln, idx + 1)).join("") + "</div>"
|
|
4471
4537
|
: '<div class="fp-hint">' + esc(t("暂无该文件的已保存 diff(运行结束后生成变更快照,或该运行没有保存变更内容)。")) + "</div>");
|
|
@@ -4485,7 +4551,7 @@ async function _fpPreviewUrl(url, name, size, opts) {
|
|
|
4485
4551
|
opts = opts || {};
|
|
4486
4552
|
const ext = _fpExt(name);
|
|
4487
4553
|
_fpSaveCtx = opts.save || null;
|
|
4488
|
-
_fpRawText = ""; _fpDirty = false;
|
|
4554
|
+
_fpRawText = ""; _fpDirty = false; _fpCopyText = null;
|
|
4489
4555
|
_fpOpen(String(name).split("/").pop(), size ? "<i>" + esc(fmtSize(size)) + "</i>" : "",
|
|
4490
4556
|
'<a class="ghost" href="' + url + '" download="' + esc(String(name).split("/").pop()) + '">' + esc(t("下载")) + "</a>");
|
|
4491
4557
|
const el = _fpEnsure();
|
|
@@ -4506,9 +4572,10 @@ async function _fpPreviewUrl(url, name, size, opts) {
|
|
|
4506
4572
|
if (!filePopIsOpen()) return;
|
|
4507
4573
|
_fpRawText = text;
|
|
4508
4574
|
if (_fpSaveCtx) _fpSaveCtx.mtime = r.headers.get("X-Tutti-Mtime") || "";
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4575
|
+
let shown = text;
|
|
4576
|
+
if (ext === "json") { try { shown = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 坏 json 原样展示 */ } }
|
|
4577
|
+
_fpCopyText = shown; // 复制=看到的原文;行号列不进剪贴板(编辑/保存仍用 _fpRawText 原文)
|
|
4578
|
+
const codeHtml = codeBlockHTML(shown);
|
|
4512
4579
|
const body = '<div class="fp-code">' + '<div class="fp-vp">' + codeHtml + "</div>" +
|
|
4513
4580
|
(_fpSaveCtx ? '<textarea class="fp-edit hidden" spellcheck="false"></textarea>' : "") + "</div>";
|
|
4514
4581
|
_fpSetBody(body);
|
|
@@ -4574,6 +4641,7 @@ window.fpSaveEdit = async function () {
|
|
|
4574
4641
|
dir: _fpSaveCtx.dir, name: _fpSaveCtx.name, content: ta.value, mtime: _fpSaveCtx.mtime,
|
|
4575
4642
|
}) });
|
|
4576
4643
|
_fpRawText = ta.value;
|
|
4644
|
+
_fpCopyText = ta.value;
|
|
4577
4645
|
_fpSaveCtx.mtime = String(d.mtime || "");
|
|
4578
4646
|
_fpDirty = false;
|
|
4579
4647
|
// 回到预览层并重画为保存后的内容(同一个框,只换里子)
|
|
@@ -4653,10 +4721,10 @@ function bindInspector() {
|
|
|
4653
4721
|
const det = e.target.closest(".stask");
|
|
4654
4722
|
if (det) sideRowActivate(det);
|
|
4655
4723
|
});
|
|
4656
|
-
$("side-tasks").addEventListener("keydown", (e) => {
|
|
4657
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4658
|
-
if (e.target.closest("button, a, input, select, textarea, summary")) return;
|
|
4659
|
-
const det = e.target.closest(".stask");
|
|
4724
|
+
$("side-tasks").addEventListener("keydown", (e) => {
|
|
4725
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4726
|
+
if (e.target.closest("button, a, input, select, textarea, summary")) return;
|
|
4727
|
+
const det = e.target.closest(".stask");
|
|
4660
4728
|
if (det) { e.preventDefault(); sideRowActivate(det); }
|
|
4661
4729
|
});
|
|
4662
4730
|
}
|
|
@@ -4676,13 +4744,13 @@ function artifactsChips(runId, files) {
|
|
|
4676
4744
|
return files.map((f) => {
|
|
4677
4745
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
4678
4746
|
return '<span class="fc-row">' +
|
|
4679
|
-
'<a class="file-chip artifact-file-open" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4680
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
4681
|
-
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4747
|
+
'<a class="file-chip artifact-file-open" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4748
|
+
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
4749
|
+
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4682
4750
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
4683
4751
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>" +
|
|
4684
|
-
(isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
|
|
4685
|
-
'" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4752
|
+
(isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
|
|
4753
|
+
'" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4686
4754
|
'<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
|
|
4687
4755
|
"</span>";
|
|
4688
4756
|
}).join("");
|
|
@@ -4733,14 +4801,14 @@ window.copyText = function (t) {
|
|
|
4733
4801
|
} catch (e) { /* 剪贴板不可用则忽略 */ }
|
|
4734
4802
|
};
|
|
4735
4803
|
|
|
4736
|
-
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
4737
|
-
|
|
4738
|
-
/* 当前打开的日志是否仍属于这批步骤(重画后日志路径消失说明步骤被重建,才收起) */
|
|
4739
|
-
function stepsMatch(runs, current) {
|
|
4740
|
-
if (!current) return false;
|
|
4741
|
-
return (runs || []).some((r) => String(r.id || "") === String(current.runId || "") &&
|
|
4742
|
-
(r.steps || []).some((s) => s.log === current.rel));
|
|
4743
|
-
}
|
|
4804
|
+
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
4805
|
+
|
|
4806
|
+
/* 当前打开的日志是否仍属于这批步骤(重画后日志路径消失说明步骤被重建,才收起) */
|
|
4807
|
+
function stepsMatch(runs, current) {
|
|
4808
|
+
if (!current) return false;
|
|
4809
|
+
return (runs || []).some((r) => String(r.id || "") === String(current.runId || "") &&
|
|
4810
|
+
(r.steps || []).some((s) => s.log === current.rel));
|
|
4811
|
+
}
|
|
4744
4812
|
|
|
4745
4813
|
function renderPlan(run) {
|
|
4746
4814
|
const box = $("rd-plan");
|
|
@@ -4791,8 +4859,17 @@ function rdLogStepLabel(runId, rel) {
|
|
|
4791
4859
|
return rel || "";
|
|
4792
4860
|
}
|
|
4793
4861
|
|
|
4862
|
+
/* 直连任务的输入框只在日志抽屉真正打开时才让出底部空间。
|
|
4863
|
+
* 日志和聊天 pane 位于同一个主区,不能靠普通兄弟选择器反向联动,
|
|
4864
|
+
* 所以用详情根节点的状态类做显式同步。 */
|
|
4865
|
+
function syncChatLogSpace(open) {
|
|
4866
|
+
const detail = $("run-detail");
|
|
4867
|
+
if (detail) detail.classList.toggle("chat-log-open", !!open);
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4794
4870
|
window.rdLogClose = function () {
|
|
4795
4871
|
const box = $("rd-log");
|
|
4872
|
+
syncChatLogSpace(false);
|
|
4796
4873
|
if (!box) return;
|
|
4797
4874
|
box.classList.add("hidden");
|
|
4798
4875
|
currentLog = null;
|
|
@@ -4801,24 +4878,25 @@ window.rdLogClose = function () {
|
|
|
4801
4878
|
if (st) st.textContent = "";
|
|
4802
4879
|
};
|
|
4803
4880
|
|
|
4804
|
-
async function toggleLog(runId, rel) {
|
|
4805
|
-
const box = $("rd-log"), pre = $("rd-log-text");
|
|
4806
|
-
if (currentLog && currentLog.runId === runId && currentLog.rel === rel && !box.classList.contains("hidden")) {
|
|
4881
|
+
async function toggleLog(runId, rel) {
|
|
4882
|
+
const box = $("rd-log"), pre = $("rd-log-text");
|
|
4883
|
+
if (currentLog && currentLog.runId === runId && currentLog.rel === rel && !box.classList.contains("hidden")) {
|
|
4807
4884
|
window.rdLogClose(); return;
|
|
4808
4885
|
}
|
|
4809
4886
|
try {
|
|
4810
4887
|
const r = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4811
4888
|
// 拉取期间可能切详情:过期日志不落盘(A 的步骤输出写进 B 详情的抽屉)。
|
|
4812
4889
|
// 检查器蜂巢/步骤点开日志同款:run 属检查器当前 run 也算在场
|
|
4813
|
-
const owned = S.detailRunId === runId ||
|
|
4814
|
-
// 任务级直连时间线会跨多轮 run 回放;按钮上的 run 可能不是最新一轮,
|
|
4815
|
-
// 只按 latest.id 判断会让旧轮次的「CLI 日志」点击后悄悄失效。
|
|
4816
|
-
(S.detailTaskKey && S.lastRun && S.lastRun.task_id === S.detailTaskKey) ||
|
|
4817
|
-
((S.inspData || {}).run || {}).id === runId;
|
|
4890
|
+
const owned = S.detailRunId === runId ||
|
|
4891
|
+
// 任务级直连时间线会跨多轮 run 回放;按钮上的 run 可能不是最新一轮,
|
|
4892
|
+
// 只按 latest.id 判断会让旧轮次的「CLI 日志」点击后悄悄失效。
|
|
4893
|
+
(S.detailTaskKey && S.lastRun && S.lastRun.task_id === S.detailTaskKey) ||
|
|
4894
|
+
((S.inspData || {}).run || {}).id === runId;
|
|
4818
4895
|
if (!owned) return;
|
|
4819
4896
|
pre.textContent = r.log || t("(等待输出…)");
|
|
4820
4897
|
box.classList.remove("hidden");
|
|
4821
|
-
|
|
4898
|
+
syncChatLogSpace(true);
|
|
4899
|
+
currentLog = { runId, rel };
|
|
4822
4900
|
const st = $("rd-log-step");
|
|
4823
4901
|
if (st) st.textContent = rdLogStepLabel(runId, rel);
|
|
4824
4902
|
pre.scrollTop = pre.scrollHeight; // 打开即看最新输出
|
|
@@ -4828,10 +4906,10 @@ async function toggleLog(runId, rel) {
|
|
|
4828
4906
|
stopLogLive();
|
|
4829
4907
|
if (!live) return;
|
|
4830
4908
|
S.logLive = setInterval(async () => {
|
|
4831
|
-
if (!currentLog || currentLog.runId !== runId || currentLog.rel !== rel) return stopLogLive();
|
|
4909
|
+
if (!currentLog || currentLog.runId !== runId || currentLog.rel !== rel) return stopLogLive();
|
|
4832
4910
|
try {
|
|
4833
4911
|
const rr = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4834
|
-
if (currentLog && currentLog.runId === runId && currentLog.rel === rel) {
|
|
4912
|
+
if (currentLog && currentLog.runId === runId && currentLog.rel === rel) {
|
|
4835
4913
|
// 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰
|
|
4836
4914
|
const stick = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48;
|
|
4837
4915
|
pre.textContent = rr.log || t("(等待输出…)");
|
|
@@ -4845,6 +4923,7 @@ async function toggleLog(runId, rel) {
|
|
|
4845
4923
|
} catch (e) { /* 网络抖动保留上一帧 */ }
|
|
4846
4924
|
}, 2500);
|
|
4847
4925
|
} catch (e) { pre.textContent = t("日志读取失败: ") + e.message; box.classList.remove("hidden");
|
|
4926
|
+
syncChatLogSpace(true);
|
|
4848
4927
|
const st = $("rd-log-step"); if (st) st.textContent = rdLogStepLabel(runId, rel);
|
|
4849
4928
|
logLiveBadge(false); }
|
|
4850
4929
|
}
|
|
@@ -4920,10 +4999,10 @@ window.renderHive = function (run) {
|
|
|
4920
4999
|
const issues = steps.filter((s) => ["failed", "timeout", "cancelled"].includes(String(s.status || "").toLowerCase())).length;
|
|
4921
5000
|
sub.innerHTML = '<span class="hive-count hive-count-live"><i class="live-dot"></i>' +
|
|
4922
5001
|
esc(t("在岗")) + " " + running.length + '</span>' +
|
|
4923
|
-
'<span class="hive-count hive-count-done">✓ ' + done + '</span>' +
|
|
4924
|
-
(issues ? '<span class="hive-count hive-count-issue">! ' + issues + '</span>' : "") +
|
|
4925
|
-
'<span class="hive-count hive-count-total">' + steps.length + ' ' + esc(t("步骤")) + '</span>' +
|
|
4926
|
-
'<span class="hive-hint">' + esc(t("点击步骤卡片查看 CLI 日志")) + '</span>';
|
|
5002
|
+
'<span class="hive-count hive-count-done">✓ ' + done + '</span>' +
|
|
5003
|
+
(issues ? '<span class="hive-count hive-count-issue">! ' + issues + '</span>' : "") +
|
|
5004
|
+
'<span class="hive-count hive-count-total">' + steps.length + ' ' + esc(t("步骤")) + '</span>' +
|
|
5005
|
+
'<span class="hive-hint">' + esc(t("点击步骤卡片查看 CLI 日志")) + '</span>';
|
|
4927
5006
|
}
|
|
4928
5007
|
const activeIdx = lanes.map((l) => byStage[l].some((s) => s.status === "running")).lastIndexOf(true);
|
|
4929
5008
|
const cell = (s, laneIdx, cellIdx) => {
|
|
@@ -4945,11 +5024,11 @@ window.renderHive = function (run) {
|
|
|
4945
5024
|
(s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
|
|
4946
5025
|
+ (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
|
|
4947
5026
|
// --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
|
|
4948
|
-
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '" role="button" tabindex="0"' +
|
|
4949
|
-
' data-hive-run="' + esc(run.id) + '" data-hive-log="' + esc(s.log || "") + '"' +
|
|
4950
|
-
' style="--d:' + (laneIdx * 110 + cellIdx * 45) + 'ms"' +
|
|
4951
|
-
' title="' + esc(title) + '" ' +
|
|
4952
|
-
'>' +
|
|
5027
|
+
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '" role="button" tabindex="0"' +
|
|
5028
|
+
' data-hive-run="' + esc(run.id) + '" data-hive-log="' + esc(s.log || "") + '"' +
|
|
5029
|
+
' style="--d:' + (laneIdx * 110 + cellIdx * 45) + 'ms"' +
|
|
5030
|
+
' title="' + esc(title) + '" ' +
|
|
5031
|
+
'>' +
|
|
4953
5032
|
'<div class="hc-head">' +
|
|
4954
5033
|
'<img class="hc-bee" src="icons/bee.svg" alt="" aria-hidden="true">' +
|
|
4955
5034
|
'<span class="hc-role">' + esc(s.role || "") + "</span>" +
|
|
@@ -4959,10 +5038,10 @@ window.renderHive = function (run) {
|
|
|
4959
5038
|
'<div class="hc-meta"><span class="hc-elapsed" data-started="' + esc(s.started_at || "") + '">' +
|
|
4960
5039
|
(s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at)) + "</span>" +
|
|
4961
5040
|
(st === "running" ? '<span class="hc-live"><i class="live-dot"></i>' + t("工作中") + "</span>" : "") +
|
|
4962
|
-
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4963
|
-
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4964
|
-
(s.log ? '<span class="hc-log-link">' + esc(t("CLI 日志")) + "</span>" : "") +
|
|
4965
|
-
"</div></div>";
|
|
5041
|
+
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
5042
|
+
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
5043
|
+
(s.log ? '<span class="hc-log-link">' + esc(t("CLI 日志")) + "</span>" : "") +
|
|
5044
|
+
"</div></div>";
|
|
4966
5045
|
};
|
|
4967
5046
|
const dots = (list, laneIdx) => list.slice(-12).map((s, di) => {
|
|
4968
5047
|
const dc = s.status === "running" ? "run" : s.status === "failed" ? "fail"
|
|
@@ -5206,6 +5285,74 @@ window.dirRetract = async function (msgId) {
|
|
|
5206
5285
|
} catch (e) { /* 下一轮轮询兜底 */ }
|
|
5207
5286
|
};
|
|
5208
5287
|
|
|
5288
|
+
/* 人工干预(非直连任务):运行中=递话入箱,下一个步骤开始前送达主智能体
|
|
5289
|
+
* (它判断后引导执行方向);已结束=发送后自动断点续跑,主智能体带着
|
|
5290
|
+
* 反馈重新规划。后端链路:信箱 → retry 继承未消费消息 → 规划 peek/执行 drain。 */
|
|
5291
|
+
window.rdTalkToggle = function () {
|
|
5292
|
+
const box = $("rd-talk");
|
|
5293
|
+
if (!box) return;
|
|
5294
|
+
const show = box.classList.contains("hidden");
|
|
5295
|
+
box.classList.toggle("hidden", !show);
|
|
5296
|
+
const hint = $("rd-talk-hint");
|
|
5297
|
+
if (hint) {
|
|
5298
|
+
const run = S.lastRun;
|
|
5299
|
+
const active = !!run && (run.status === "running" || run.status === "queued");
|
|
5300
|
+
hint.textContent = active ? t("运行中:下一个步骤开始前送达主智能体")
|
|
5301
|
+
: t("已结束:发送后自动断点续跑,主智能体带着反馈重新规划");
|
|
5302
|
+
}
|
|
5303
|
+
if (show) { const ta = $("rd-talk-input"); if (ta) setTimeout(() => ta.focus(), 0); }
|
|
5304
|
+
};
|
|
5305
|
+
|
|
5306
|
+
window.rdTalkSend = async function () {
|
|
5307
|
+
const run = S.lastRun;
|
|
5308
|
+
const ta = $("rd-talk-input");
|
|
5309
|
+
const text = ((ta && ta.value) || "").trim();
|
|
5310
|
+
if (!run || !run.id) return;
|
|
5311
|
+
if (!text && !talkAtts.length) { if (ta) ta.focus(); return; }
|
|
5312
|
+
const btn = $("rd-talk-send");
|
|
5313
|
+
btn.disabled = true;
|
|
5314
|
+
try {
|
|
5315
|
+
await api("/api/runs/" + encodeURIComponent(run.id) + "/messages",
|
|
5316
|
+
{ method: "POST", body: JSON.stringify({ text, attachments: talkAtts.map((a) => a.id) }) });
|
|
5317
|
+
if (ta) ta.value = "";
|
|
5318
|
+
talkAtts = []; drawTalkAtts();
|
|
5319
|
+
const box = $("rd-talk");
|
|
5320
|
+
if (box) box.classList.add("hidden");
|
|
5321
|
+
const active = run.status === "running" || run.status === "queued";
|
|
5322
|
+
if (!active && run.task_id) {
|
|
5323
|
+
await retryTask(run.task_id);
|
|
5324
|
+
toast(t("已转交:带着你的反馈断点续跑,主智能体会重新规划"));
|
|
5325
|
+
} else {
|
|
5326
|
+
toast(t("已递话:主智能体在下一个步骤开始前会看到"));
|
|
5327
|
+
}
|
|
5328
|
+
} catch (e) { toast(t("发送失败:") + e.message, true); }
|
|
5329
|
+
finally { btn.disabled = false; }
|
|
5330
|
+
};
|
|
5331
|
+
|
|
5332
|
+
/* 递话附件:与运行中指挥同一落盘通道(/api/attachments 待提交区 → 随消息入箱) */
|
|
5333
|
+
let talkAtts = [];
|
|
5334
|
+
async function talkUploadFiles(files) {
|
|
5335
|
+
for (const f of files) {
|
|
5336
|
+
if (talkAtts.length >= DIR_ATT_MAX) { toast(t("附件最多 ") + DIR_ATT_MAX + t(" 个"), true); break; }
|
|
5337
|
+
if (f.size > DIR_ATT_BYTES) { toast(f.name + t(":超过 8MB,已跳过"), true); continue; }
|
|
5338
|
+
try {
|
|
5339
|
+
const b64 = await fileToB64(f);
|
|
5340
|
+
const d = await api("/api/attachments", { method: "POST",
|
|
5341
|
+
body: JSON.stringify({ name: f.name, data: b64 }) });
|
|
5342
|
+
if (d.attachment) talkAtts.push(d.attachment);
|
|
5343
|
+
} catch (e) { toast(t("附件上传失败:") + e.message, true); }
|
|
5344
|
+
}
|
|
5345
|
+
drawTalkAtts();
|
|
5346
|
+
}
|
|
5347
|
+
function drawTalkAtts() {
|
|
5348
|
+
const box = $("rd-talk-atts");
|
|
5349
|
+
if (!box) return;
|
|
5350
|
+
box.innerHTML = talkAtts.map((a, i) =>
|
|
5351
|
+
'<span class="att-chip2">' + esc(a.name) + "<b onclick=\"talkRemoveAtt(" + i + ')" title="' +
|
|
5352
|
+
t("移除") + "\">×</b></span>").join("");
|
|
5353
|
+
}
|
|
5354
|
+
window.talkRemoveAtt = function (i) { talkAtts.splice(i, 1); drawTalkAtts(); };
|
|
5355
|
+
|
|
5209
5356
|
function bindDirector() {
|
|
5210
5357
|
$("rd-attach-btn").addEventListener("click", () => $("rd-attach-file").click());
|
|
5211
5358
|
$("btn-pause").addEventListener("click", window.dirTogglePause);
|
|
@@ -5242,8 +5389,11 @@ let chatAtts = [];
|
|
|
5242
5389
|
let chatSig = "";
|
|
5243
5390
|
|
|
5244
5391
|
function chatEngineIsDirect(run) {
|
|
5245
|
-
const
|
|
5246
|
-
|
|
5392
|
+
const st = S.state || {};
|
|
5393
|
+
const all = (st.tasks || []).concat(st.archived_tasks || []);
|
|
5394
|
+
const task = all.find((x) => x.id === (run && run.task_id));
|
|
5395
|
+
return !!((task && task.engine === "direct") ||
|
|
5396
|
+
(!task && run && (run.engine === "direct" || run.type === "direct")));
|
|
5247
5397
|
}
|
|
5248
5398
|
|
|
5249
5399
|
/* 对话正文:``` 围栏渲染成真代码块(复用 codeBlockHTML:行号+高亮+代码主题),
|
|
@@ -5284,6 +5434,8 @@ async function renderChat(run, active) {
|
|
|
5284
5434
|
// 只对直连任务启用(其它流程有自己的蜂巢/步骤视图,不抢默认选卡)
|
|
5285
5435
|
if (!run || !chatEngineIsDirect(run)) { box.classList.add("hidden"); return; }
|
|
5286
5436
|
box.classList.remove("hidden");
|
|
5437
|
+
const continueNote = $("rd-chat-continue-note");
|
|
5438
|
+
if (continueNote) continueNote.classList.toggle("hidden", !!active);
|
|
5287
5439
|
if (chatRunId !== run.id) { chatRunId = run.id; chatAtts = []; drawChatAtts(); chatSig = ""; }
|
|
5288
5440
|
const sig = run.id + "|" + run.status + "|" + (run.steps || []).length +
|
|
5289
5441
|
"|" + ((run.messages || []).length);
|
|
@@ -5332,8 +5484,15 @@ async function renderChat(run, active) {
|
|
|
5332
5484
|
: "") +
|
|
5333
5485
|
"</div></div>";
|
|
5334
5486
|
}
|
|
5335
|
-
//
|
|
5336
|
-
|
|
5487
|
+
// 协议尾行是给编排器判收的机器标记,不该出现在聊天正文里(真源在提示词约定);
|
|
5488
|
+
// <followups> 块后端已剥并转成结构化字段,这里再兜一次底防老数据/漏剥;
|
|
5489
|
+
// 开场「本轮做了什么:…」+「回复:」是模型复读协议措辞的元描述(提示词已禁,
|
|
5490
|
+
// 这里兜历史数据),只剥消息开头的第一行
|
|
5491
|
+
const body = String(it.text || "")
|
|
5492
|
+
.replace(/^\s*本轮做了什么\s*[::][^\n]*\n+(回复\s*[::]\s*\n?)?/, "")
|
|
5493
|
+
.replace(/\n*DIRECT_DONE:.*\s*$/s, "")
|
|
5494
|
+
.replace(/\n*<followups>[\s\S]*?<\/followups>\s*$/s, "")
|
|
5495
|
+
.trim();
|
|
5337
5496
|
// 运行中还没有正文:三点打字动画(终态无正文才落「无文本输出」占位)
|
|
5338
5497
|
const bodyHtml = body ? chatBodyHTML(body)
|
|
5339
5498
|
: (it.status === "running"
|
|
@@ -5342,15 +5501,20 @@ async function renderChat(run, active) {
|
|
|
5342
5501
|
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
5343
5502
|
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
5344
5503
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
5345
|
-
return '<div class="chat-row">' +
|
|
5346
|
-
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5347
|
-
'<div class="chat-bubble agent">' +
|
|
5348
|
-
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5349
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5350
|
-
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div>" +
|
|
5351
|
-
(it.log && it.run ? '<div class="chat-actions"><button class="chat-log-btn" type="button" data-chat-log-run="' +
|
|
5352
|
-
esc(it.run) + '" data-chat-log-rel="' + esc(it.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button></div>" : "") +
|
|
5353
|
-
|
|
5504
|
+
return '<div class="chat-row">' +
|
|
5505
|
+
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5506
|
+
'<div class="chat-bubble agent">' +
|
|
5507
|
+
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5508
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5509
|
+
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div>" +
|
|
5510
|
+
(it.log && it.run ? '<div class="chat-actions"><button class="chat-log-btn" type="button" data-chat-log-run="' +
|
|
5511
|
+
esc(it.run) + '" data-chat-log-rel="' + esc(it.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button></div>" : "") +
|
|
5512
|
+
(Array.isArray(it.followups) && it.followups.length
|
|
5513
|
+
? '<div class="chat-fups">' + it.followups.map((f) =>
|
|
5514
|
+
'<button type="button" class="chat-fup-btn" data-fup="' + esc(f.prompt || "") +
|
|
5515
|
+
'" title="' + esc(f.prompt || "") + '">' + esc(f.label || "") + "</button>").join("") + "</div>"
|
|
5516
|
+
: "") +
|
|
5517
|
+
"</div></div>";
|
|
5354
5518
|
}).join("");
|
|
5355
5519
|
// 时间线收尾的「执行结果」卡:run 终态后的确定性摘要(成没成/耗时/执行者/
|
|
5356
5520
|
// 产出文件)——不依赖模型最后一句话自觉交代(用户反馈:输"1"跑完 55 秒
|
|
@@ -5379,9 +5543,9 @@ function chatResultHTML(run, res) {
|
|
|
5379
5543
|
if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
|
|
5380
5544
|
const files = res.files || [];
|
|
5381
5545
|
const chips = files.map((f) =>
|
|
5382
|
-
'<a class="file-chip chat-file-open" href="/api/runs/' + encodeURIComponent(run.id) + "/file?name=" +
|
|
5383
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
5384
|
-
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(run.id) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
5546
|
+
'<a class="file-chip chat-file-open" href="/api/runs/' + encodeURIComponent(run.id) + "/file?name=" +
|
|
5547
|
+
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
5548
|
+
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(run.id) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
5385
5549
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
5386
5550
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>").join("");
|
|
5387
5551
|
return '<div class="chat-row">' +
|
|
@@ -5400,125 +5564,122 @@ function chatResultHTML(run, res) {
|
|
|
5400
5564
|
"</div></div>";
|
|
5401
5565
|
}
|
|
5402
5566
|
|
|
5403
|
-
function chatDurTxt(s) {
|
|
5404
|
-
s = Number(s) || 0;
|
|
5405
|
-
if (s < 60) return s + " " + t("秒");
|
|
5406
|
-
const m = Math.floor(s / 60), r = s % 60;
|
|
5407
|
-
return m + " " + t("分") + (r ? " " + r + " " + t("秒") : "");
|
|
5408
|
-
}
|
|
5409
|
-
|
|
5410
|
-
/* 代码/编排任务的确定性结果摘要:报告是长文,步骤是过程,用户还需要一眼
|
|
5411
|
-
* 知道这次到底成没成、做了多少步、用了多久,以及失败原因。这个卡片不依赖
|
|
5412
|
-
* 智能体是否在最后一句话里主动总结;直连任务也能在「成果」页签看到同一口径。 */
|
|
5413
|
-
function runDurationSeconds(run) {
|
|
5414
|
-
if (!run) return null;
|
|
5415
|
-
if (Number.isFinite(Number(run.duration_s))) return Number(run.duration_s);
|
|
5416
|
-
const steps = (run.steps || []).filter((s) => Number.isFinite(Number(s.duration_s)));
|
|
5417
|
-
if (steps.length) return steps.reduce((a, s) => a + Number(s.duration_s || 0), 0);
|
|
5418
|
-
const a = Date.parse(String(run.started_at || "").replace(" ", "T"));
|
|
5419
|
-
const b = Date.parse(String(run.ended_at || "").replace(" ", "T"));
|
|
5420
|
-
return Number.isFinite(a) && Number.isFinite(b) && b >= a ? (b - a) / 1000 : null;
|
|
5421
|
-
}
|
|
5422
|
-
|
|
5423
|
-
function runOutcomeSummary(run) {
|
|
5424
|
-
if (!run) return "";
|
|
5425
|
-
const last = (run.steps || []).slice().reverse().find((s) => s.output || s.summary);
|
|
5426
|
-
const raw = run.summary || (last && (last.output || last.summary)) || run.error || "";
|
|
5427
|
-
return String(raw).replace(/\s+/g, " ").trim().slice(0, 520);
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
|
-
function renderRunOutcome(run, runs) {
|
|
5431
|
-
const box = $("rd-outcome");
|
|
5432
|
-
if (!box) return;
|
|
5433
|
-
if (!run) { box.classList.add("hidden"); box.innerHTML = ""; return; }
|
|
5434
|
-
const all = runs && runs.length ? runs : [run];
|
|
5435
|
-
const steps = (run.steps || []);
|
|
5436
|
-
const total = all.reduce((n, r) => n + ((r.steps || []).length), 0);
|
|
5437
|
-
const settled = all.reduce((n, r) => n + (r.steps || []).filter((s) =>
|
|
5438
|
-
!["running", "queued"].includes(String(s.status || ""))).length, 0);
|
|
5439
|
-
const status = String(run.status || "");
|
|
5440
|
-
const active = status === "running" || status === "queued";
|
|
5441
|
-
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5442
|
-
const label = active ? (status === "queued" ? t("排队中") : t("运行中"))
|
|
5443
|
-
: (status === "done" ? t("完成") : status === "failed" ? t("失败") : status === "cancelled" ? t("已取消") : status || t("未知状态"));
|
|
5444
|
-
const icon = status === "done" ? "#i-check" : bad ? "#i-x" : "#i-gauge";
|
|
5445
|
-
const duration = runDurationSeconds(run);
|
|
5446
|
-
const latest = steps.slice().reverse().find((s) => s.agent_label || s.agent || s.role);
|
|
5447
|
-
const route = run.route && typeof run.route === "object" ? run.route.implementer : "";
|
|
5448
|
-
// route.implementer 是路由评分说明,不是用户真正关心的 CLI 名称;优先
|
|
5449
|
-
// 展示步骤上的 agent_label,只有旧数据没有步骤执行者时才回退到路由说明。
|
|
5450
|
-
const executor = run.executor || run.implementer || (latest && (latest.agent_label || latest.agent)) || route || "";
|
|
5451
|
-
const summary = runOutcomeSummary(run);
|
|
5452
|
-
const meta = [];
|
|
5453
|
-
if (executor) meta.push('<span><b>' + esc(t("执行者")) + '</b> ' + esc(executor) + "</span>");
|
|
5454
|
-
if (total) meta.push('<span><b>' + esc(t("步骤")) + '</b> ' + settled + "/" + total + "</span>");
|
|
5455
|
-
if (duration != null) meta.push('<span><b>' + esc(t("耗时")) + '</b> ' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + "</span>");
|
|
5456
|
-
return (box.classList.remove("hidden"), box.innerHTML =
|
|
5457
|
-
'<div class="outcome-top">' +
|
|
5458
|
-
'<div class="outcome-title ' + (bad ? "bad" : active ? "live" : "ok") + '">' +
|
|
5459
|
-
'<svg class="ico" aria-hidden="true"><use href="' + icon + '"></use></svg>' + esc(t("执行结果")) +
|
|
5460
|
-
'<span class="outcome-status">' + esc(label) + "</span></div>" +
|
|
5461
|
-
(meta.length ? '<div class="outcome-meta">' + meta.join("") + "</div>" : "") +
|
|
5462
|
-
"</div>" +
|
|
5463
|
-
(summary ? '<div class="outcome-summary">' + esc(summary) + "</div>" : "") +
|
|
5464
|
-
(run.error ? '<div class="outcome-error">' + esc(run.error) + "</div>" : "") +
|
|
5465
|
-
(total ? '<div class="outcome-actions"><button class="ghost" type="button" onclick="rdChatNavGo(\'steps\')">' + esc(t("查看执行步骤")) + "</button></div>" : "")
|
|
5466
|
-
);
|
|
5467
|
-
}
|
|
5468
|
-
|
|
5469
|
-
/* 任务详情首屏概览:把“这次任务做了什么、做到哪、最近发生了什么”放在
|
|
5470
|
-
* 蜂巢上方。直连任务仍以对话为主,由 chat-mode 隐藏概览,避免它抢走输入空间。 */
|
|
5471
|
-
function renderDetailOverview(run, runs, task) {
|
|
5472
|
-
const box = $("rd-overview");
|
|
5473
|
-
if (!box || !run) return;
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
const
|
|
5480
|
-
|
|
5481
|
-
const
|
|
5482
|
-
const
|
|
5483
|
-
|
|
5484
|
-
const
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
const
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
const
|
|
5492
|
-
const
|
|
5493
|
-
const
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
'
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
function drawChatAtts() {
|
|
5567
|
+
function chatDurTxt(s) {
|
|
5568
|
+
s = Number(s) || 0;
|
|
5569
|
+
if (s < 60) return s + " " + t("秒");
|
|
5570
|
+
const m = Math.floor(s / 60), r = s % 60;
|
|
5571
|
+
return m + " " + t("分") + (r ? " " + r + " " + t("秒") : "");
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
/* 代码/编排任务的确定性结果摘要:报告是长文,步骤是过程,用户还需要一眼
|
|
5575
|
+
* 知道这次到底成没成、做了多少步、用了多久,以及失败原因。这个卡片不依赖
|
|
5576
|
+
* 智能体是否在最后一句话里主动总结;直连任务也能在「成果」页签看到同一口径。 */
|
|
5577
|
+
function runDurationSeconds(run) {
|
|
5578
|
+
if (!run) return null;
|
|
5579
|
+
if (Number.isFinite(Number(run.duration_s))) return Number(run.duration_s);
|
|
5580
|
+
const steps = (run.steps || []).filter((s) => Number.isFinite(Number(s.duration_s)));
|
|
5581
|
+
if (steps.length) return steps.reduce((a, s) => a + Number(s.duration_s || 0), 0);
|
|
5582
|
+
const a = Date.parse(String(run.started_at || "").replace(" ", "T"));
|
|
5583
|
+
const b = Date.parse(String(run.ended_at || "").replace(" ", "T"));
|
|
5584
|
+
return Number.isFinite(a) && Number.isFinite(b) && b >= a ? (b - a) / 1000 : null;
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
function runOutcomeSummary(run) {
|
|
5588
|
+
if (!run) return "";
|
|
5589
|
+
const last = (run.steps || []).slice().reverse().find((s) => s.output || s.summary);
|
|
5590
|
+
const raw = run.summary || (last && (last.output || last.summary)) || run.error || "";
|
|
5591
|
+
return String(raw).replace(/\s+/g, " ").trim().slice(0, 520);
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
function renderRunOutcome(run, runs) {
|
|
5595
|
+
const box = $("rd-outcome");
|
|
5596
|
+
if (!box) return;
|
|
5597
|
+
if (!run) { box.classList.add("hidden"); box.innerHTML = ""; return; }
|
|
5598
|
+
const all = runs && runs.length ? runs : [run];
|
|
5599
|
+
const steps = (run.steps || []);
|
|
5600
|
+
const total = all.reduce((n, r) => n + ((r.steps || []).length), 0);
|
|
5601
|
+
const settled = all.reduce((n, r) => n + (r.steps || []).filter((s) =>
|
|
5602
|
+
!["running", "queued"].includes(String(s.status || ""))).length, 0);
|
|
5603
|
+
const status = String(run.status || "");
|
|
5604
|
+
const active = status === "running" || status === "queued";
|
|
5605
|
+
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5606
|
+
const label = active ? (status === "queued" ? t("排队中") : t("运行中"))
|
|
5607
|
+
: (status === "done" ? t("完成") : status === "failed" ? t("失败") : status === "cancelled" ? t("已取消") : status || t("未知状态"));
|
|
5608
|
+
const icon = status === "done" ? "#i-check" : bad ? "#i-x" : "#i-gauge";
|
|
5609
|
+
const duration = runDurationSeconds(run);
|
|
5610
|
+
const latest = steps.slice().reverse().find((s) => s.agent_label || s.agent || s.role);
|
|
5611
|
+
const route = run.route && typeof run.route === "object" ? run.route.implementer : "";
|
|
5612
|
+
// route.implementer 是路由评分说明,不是用户真正关心的 CLI 名称;优先
|
|
5613
|
+
// 展示步骤上的 agent_label,只有旧数据没有步骤执行者时才回退到路由说明。
|
|
5614
|
+
const executor = run.executor || run.implementer || (latest && (latest.agent_label || latest.agent)) || route || "";
|
|
5615
|
+
const summary = runOutcomeSummary(run);
|
|
5616
|
+
const meta = [];
|
|
5617
|
+
if (executor) meta.push('<span><b>' + esc(t("执行者")) + '</b> ' + esc(executor) + "</span>");
|
|
5618
|
+
if (total) meta.push('<span><b>' + esc(t("步骤")) + '</b> ' + settled + "/" + total + "</span>");
|
|
5619
|
+
if (duration != null) meta.push('<span><b>' + esc(t("耗时")) + '</b> ' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + "</span>");
|
|
5620
|
+
return (box.classList.remove("hidden"), box.innerHTML =
|
|
5621
|
+
'<div class="outcome-top">' +
|
|
5622
|
+
'<div class="outcome-title ' + (bad ? "bad" : active ? "live" : "ok") + '">' +
|
|
5623
|
+
'<svg class="ico" aria-hidden="true"><use href="' + icon + '"></use></svg>' + esc(t("执行结果")) +
|
|
5624
|
+
'<span class="outcome-status">' + esc(label) + "</span></div>" +
|
|
5625
|
+
(meta.length ? '<div class="outcome-meta">' + meta.join("") + "</div>" : "") +
|
|
5626
|
+
"</div>" +
|
|
5627
|
+
(summary ? '<div class="outcome-summary">' + esc(summary) + "</div>" : "") +
|
|
5628
|
+
(run.error ? '<div class="outcome-error">' + esc(run.error) + "</div>" : "") +
|
|
5629
|
+
(total ? '<div class="outcome-actions"><button class="ghost" type="button" onclick="rdChatNavGo(\'steps\')">' + esc(t("查看执行步骤")) + "</button></div>" : "")
|
|
5630
|
+
);
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
/* 任务详情首屏概览:把“这次任务做了什么、做到哪、最近发生了什么”放在
|
|
5634
|
+
* 蜂巢上方。直连任务仍以对话为主,由 chat-mode 隐藏概览,避免它抢走输入空间。 */
|
|
5635
|
+
function renderDetailOverview(run, runs, task) {
|
|
5636
|
+
const box = $("rd-overview");
|
|
5637
|
+
if (!box || !run) return;
|
|
5638
|
+
const direct = chatEngineIsDirect(run);
|
|
5639
|
+
box.classList.toggle("direct-overview", direct);
|
|
5640
|
+
const all = runs && runs.length ? runs : [run];
|
|
5641
|
+
const steps = (run.steps || []);
|
|
5642
|
+
const total = all.reduce((n, r) => n + ((r.steps || []).length), 0);
|
|
5643
|
+
const settled = all.reduce((n, r) => n + (r.steps || []).filter((s) =>
|
|
5644
|
+
!["running", "queued"].includes(String(s.status || ""))).length, 0);
|
|
5645
|
+
const pct = total ? Math.max(0, Math.min(100, Math.round((settled / total) * 100))) : 0;
|
|
5646
|
+
const status = String(run.status || "");
|
|
5647
|
+
const active = status === "running" || status === "queued";
|
|
5648
|
+
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5649
|
+
const statusText = active ? (status === "queued" ? t("排队中") : t("运行中"))
|
|
5650
|
+
: status === "done" ? t("完成") : status === "failed" ? t("失败")
|
|
5651
|
+
: status === "cancelled" ? t("已取消") : status || t("未知状态");
|
|
5652
|
+
const last = steps.slice().reverse().find((s) => s.summary || s.output || s.note) || steps[steps.length - 1];
|
|
5653
|
+
const lastText = last ? String(last.summary || last.output || last.note || "").replace(/\s+/g, " ").trim().slice(0, 240) : "";
|
|
5654
|
+
const executor = run.executor || run.implementer || (last && (last.agent_label || last.agent)) || "";
|
|
5655
|
+
const duration = runDurationSeconds(run);
|
|
5656
|
+
const goal = (task && task.goal) || run.goal || run.title || "";
|
|
5657
|
+
const actions = [];
|
|
5658
|
+
if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
|
|
5659
|
+
if (last && last.log) actions.push('<button type="button" class="ghost" data-overview-run="' + esc(run.id) + '" data-overview-log="' + esc(last.log) + '">' + esc(t("打开 CLI 日志")) + "</button>");
|
|
5660
|
+
if (status === "done" || run.report) actions.push('<button type="button" class="ghost" data-overview-tab="result">' + esc(t("查看成果")) + "</button>");
|
|
5661
|
+
box.innerHTML =
|
|
5662
|
+
'<div class="rd-overview-main">' +
|
|
5663
|
+
'<div class="rd-overview-kicker">' + esc(t("任务概览")) + '</div>' +
|
|
5664
|
+
'<div class="rd-overview-title" title="' + esc(run.title || "") + '">' + esc(run.title || "") + '</div>' +
|
|
5665
|
+
(goal ? '<div class="rd-overview-goal" title="' + esc(goal) + '">' + esc(goal) + '</div>' : "") +
|
|
5666
|
+
'</div>' +
|
|
5667
|
+
'<div class="rd-overview-state">' +
|
|
5668
|
+
'<span class="state-label">' + esc(t("当前状态")) + '</span>' +
|
|
5669
|
+
'<strong class="state-value ' + (bad ? "bad" : status === "done" ? "ok" : "") + '">' + esc(statusText) + '</strong>' +
|
|
5670
|
+
'</div>' +
|
|
5671
|
+
'<div class="rd-overview-progress" aria-label="' + esc(pct + "%") + '"><i style="width:' + pct + '%"></i></div>' +
|
|
5672
|
+
'<div class="rd-overview-facts">' +
|
|
5673
|
+
'<span><b>' + settled + "/" + total + '</b> ' + esc(t("步骤已完成")) + '</span>' +
|
|
5674
|
+
(executor ? '<span>' + esc(t("执行者")) + ' <b>' + esc(executor) + '</b></span>' : "") +
|
|
5675
|
+
(duration != null ? '<span>' + esc(t("耗时")) + ' <b>' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + '</b></span>' : "") +
|
|
5676
|
+
'</div>' +
|
|
5677
|
+
(lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
|
|
5678
|
+
(actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
|
|
5679
|
+
box.classList.remove("hidden");
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
function drawChatAtts() {
|
|
5522
5683
|
const box = $("rd-chat-att-list");
|
|
5523
5684
|
if (!box) return;
|
|
5524
5685
|
// 点名字开预览(待提交区原文走 /api/attachments/<id>),× 才是移除
|
|
@@ -5605,6 +5766,13 @@ function bindChat() {
|
|
|
5605
5766
|
const btn = $("rd-chat-attach"), file = $("rd-chat-file"), send = $("rd-chat-send"),
|
|
5606
5767
|
ta = $("rd-chat-input");
|
|
5607
5768
|
if (!btn || !file || !send || !ta) return;
|
|
5769
|
+
const focusBtn = $("rd-chat-focus");
|
|
5770
|
+
if (focusBtn) focusBtn.addEventListener("click", () => {
|
|
5771
|
+
S.rdTab = "chat";
|
|
5772
|
+
S.rdTabPin = true;
|
|
5773
|
+
applyRdTabs();
|
|
5774
|
+
setTimeout(() => ta.focus(), 0);
|
|
5775
|
+
});
|
|
5608
5776
|
btn.addEventListener("click", () => file.click());
|
|
5609
5777
|
file.addEventListener("change", (e) => {
|
|
5610
5778
|
chatUploadFiles(Array.from(e.target.files || []));
|
|
@@ -5633,16 +5801,27 @@ function bindChat() {
|
|
|
5633
5801
|
// 附件胶囊点击预览:委托绑在静态容器上——时间线轮询整块重建 innerHTML、
|
|
5634
5802
|
// 输入条胶囊增删重画,都不丢监听。时间线胶囊走已落盘文件,输入条胶囊
|
|
5635
5803
|
// (× 移除按钮除外)走待提交区
|
|
5636
|
-
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5637
|
-
const logBtn = e.target.closest("[data-chat-log-run][data-chat-log-rel]");
|
|
5638
|
-
if (logBtn) { e.stopPropagation(); toggleLog(logBtn.dataset.chatLogRun, logBtn.dataset.chatLogRel); return; }
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5804
|
+
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5805
|
+
const logBtn = e.target.closest("[data-chat-log-run][data-chat-log-rel]");
|
|
5806
|
+
if (logBtn) { e.stopPropagation(); toggleLog(logBtn.dataset.chatLogRun, logBtn.dataset.chatLogRel); return; }
|
|
5807
|
+
// 建议追问芯片:点击把整句回填输入框(不直接发送——用户可改后再发),焦点落输入框
|
|
5808
|
+
const fup = e.target.closest(".chat-fup-btn[data-fup]");
|
|
5809
|
+
if (fup) {
|
|
5810
|
+
const ta = $("rd-chat-input");
|
|
5811
|
+
if (ta) {
|
|
5812
|
+
ta.value = fup.dataset.fup;
|
|
5813
|
+
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5814
|
+
ta.focus();
|
|
5815
|
+
}
|
|
5816
|
+
return;
|
|
5817
|
+
}
|
|
5818
|
+
const file = e.target.closest(".chat-file-open[data-file-run]");
|
|
5819
|
+
if (file) {
|
|
5820
|
+
e.preventDefault();
|
|
5821
|
+
artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
|
|
5822
|
+
return;
|
|
5823
|
+
}
|
|
5824
|
+
const chip = e.target.closest(".att-open");
|
|
5646
5825
|
if (chip) window.chatAttPopup(chip.dataset.att);
|
|
5647
5826
|
});
|
|
5648
5827
|
const attBox = $("rd-chat-att-list");
|
|
@@ -5985,11 +6164,20 @@ function protoLabel(p) {
|
|
|
5985
6164
|
* 通过的 wire_caps(auto 供应商全靠它)。返回命中的协议名,不适配返回空串。
|
|
5986
6165
|
* 与 modelhub._entry_endpoint 同规则——它同时就是「注入本 CLI 实际走的 wire」,
|
|
5987
6166
|
* 勾选面板据此标死解析时会被跳过的条目。 */
|
|
5988
|
-
function provAdaptedProto(p, allow) {
|
|
6167
|
+
function provAdaptedProto(p, allow, kind) {
|
|
5989
6168
|
if (!p) return "";
|
|
5990
|
-
|
|
6169
|
+
// codex 0.154+ 只讲 responses wire:chat-only 端点(显式 wire_api=chat 或
|
|
6170
|
+
// wire_caps 实测为 chat)对 codex 等于没有可用协议——与后端 resolve_binding
|
|
6171
|
+
// 的剔除规则同源(2026-09-18 重写任务案:链显示健康、运行时必剔死的假象)
|
|
6172
|
+
const codexOnly = /codex/.test(String(kind || "").toLowerCase());
|
|
6173
|
+
const chatBlocked = (w) => codexOnly && (w || "responses") === "chat";
|
|
6174
|
+
if (allow.indexOf(p.protocol) >= 0) {
|
|
6175
|
+
if (chatBlocked(p.wire_api)) return "";
|
|
6176
|
+
return p.protocol;
|
|
6177
|
+
}
|
|
5991
6178
|
const caps = p.wire_caps || {};
|
|
5992
|
-
return allow.find((pr) => (caps[pr] || {}).base
|
|
6179
|
+
return allow.find((pr) => (caps[pr] || {}).base
|
|
6180
|
+
&& !chatBlocked((caps[pr] || {}).wire_api)) || "";
|
|
5993
6181
|
}
|
|
5994
6182
|
|
|
5995
6183
|
/* CLI 允许注入的供应商 wire 协议——与 modelhub.resolve_binding 的 allowed 规则
|
|
@@ -6010,7 +6198,7 @@ function modelSelectHtml(id, current, kind) {
|
|
|
6010
6198
|
const allow = bindAllowedProtocols(kind);
|
|
6011
6199
|
const groups = modelGroups().filter((g) => {
|
|
6012
6200
|
const p = (S.providers || []).find((x) => x.id === g.id);
|
|
6013
|
-
return provAdaptedProto(p, allow);
|
|
6201
|
+
return provAdaptedProto(p, allow, kind);
|
|
6014
6202
|
});
|
|
6015
6203
|
let opts = '<option value="">' + t("(未设置)") + '</option>';
|
|
6016
6204
|
if (cur && !groups.some((g) => g.models.includes(cur))) {
|
|
@@ -6089,7 +6277,7 @@ function recommendFor(c) {
|
|
|
6089
6277
|
const allow = bindAllowedProtocols(c.orch_kind || c.id);
|
|
6090
6278
|
const badDefault = (p) => (p.models || []).some((m) => m.name === p.model &&
|
|
6091
6279
|
(m.hidden || m.enabled === false));
|
|
6092
|
-
const provs = (S.providers || []).filter((p) => !badDefault(p) && chainProvUsable(p, allow));
|
|
6280
|
+
const provs = (S.providers || []).filter((p) => !badDefault(p) && chainProvUsable(p, allow, c.orch_kind || c.id));
|
|
6093
6281
|
if (!provs.length) return null;
|
|
6094
6282
|
const score = (p) => (allow.indexOf(p.protocol) >= 0 ? 0 : 1) * 1000 + (p.priority || 0);
|
|
6095
6283
|
provs.sort((a, b) => score(a) - score(b));
|
|
@@ -6101,9 +6289,9 @@ function recommendFor(c) {
|
|
|
6101
6289
|
|
|
6102
6290
|
/* 供应商对某 CLI 是否「推荐可用」:启用、协议适配,且至少有一把不在冷却期的
|
|
6103
6291
|
* KEY(全冷却=欠费失效,推荐了也白推荐——解析层照样失败)。 */
|
|
6104
|
-
function chainProvUsable(p, allow) {
|
|
6292
|
+
function chainProvUsable(p, allow, kind) {
|
|
6105
6293
|
if (!p || p.enabled === false) return false;
|
|
6106
|
-
if (!provAdaptedProto(p, allow)) return false;
|
|
6294
|
+
if (!provAdaptedProto(p, allow, kind)) return false;
|
|
6107
6295
|
const ks = p.keys || [];
|
|
6108
6296
|
if (!ks.length) return true; // 老数据单 KEY(api_key 镜像):无 keys 结构视为可用
|
|
6109
6297
|
return ks.some((k) => k.enabled !== false && !k.cooling);
|
|
@@ -6118,7 +6306,7 @@ function chainDeadReasons(c, chain) {
|
|
|
6118
6306
|
const pv = (S.providers || []).find((p) => p.id === x.p);
|
|
6119
6307
|
if (!pv) return t("供应商已删除");
|
|
6120
6308
|
if (pv.enabled === false) return t("供应商已停用");
|
|
6121
|
-
if (!provAdaptedProto(pv, allow)) return t("协议不匹配");
|
|
6309
|
+
if (!provAdaptedProto(pv, allow, c.orch_kind)) return t("协议不匹配");
|
|
6122
6310
|
const ks = pv.keys || [];
|
|
6123
6311
|
if (ks.length && !ks.some((k) => k.enabled !== false && !k.cooling))
|
|
6124
6312
|
return t("密钥全部冷却中");
|
|
@@ -6250,7 +6438,7 @@ function bindModelBox(c, provId) {
|
|
|
6250
6438
|
? st.chain.map((c2, i) => {
|
|
6251
6439
|
const pname = c2.p ? provName(c2.p) : t("CLI 默认凭据");
|
|
6252
6440
|
const pv = c2.p ? (S.providers || []).find((p) => p.id === c2.p) : null;
|
|
6253
|
-
const adapted = provAdaptedProto(pv, allow);
|
|
6441
|
+
const adapted = provAdaptedProto(pv, allow, c.orch_kind);
|
|
6254
6442
|
const dead = c2.p && !adapted;
|
|
6255
6443
|
// auto 供应商按实测 wire 注入是常态,说「自动」;显式协议靠适配才通的
|
|
6256
6444
|
// 才叫「已适配」——两者含义不同,别混成一句。
|
|
@@ -6290,7 +6478,7 @@ function bindPanel(c) {
|
|
|
6290
6478
|
const allow = bindAllowedProtocols(c.orch_kind);
|
|
6291
6479
|
const all = modelGroups();
|
|
6292
6480
|
// 原生协议匹配,或适配测试过 allow 里某条 wire 的供应商都可勾选
|
|
6293
|
-
const groups = all.filter((g) => provAdaptedProto(g, allow));
|
|
6481
|
+
const groups = all.filter((g) => provAdaptedProto(g, allow, c.orch_kind));
|
|
6294
6482
|
const hiddenN = all.length - groups.length;
|
|
6295
6483
|
if (!groups.length) {
|
|
6296
6484
|
return '<div class="ohint">' + esc(all.length
|
|
@@ -6305,12 +6493,12 @@ function bindPanel(c) {
|
|
|
6305
6493
|
'<div class="ogroup"><div class="ogname">' + esc(g.name) +
|
|
6306
6494
|
' <span class="tag">' + (g.protocol === "auto"
|
|
6307
6495
|
? t("自动 · 注入该厂商凭据")
|
|
6308
|
-
: (provAdaptedProto(g, allow) === g.protocol
|
|
6496
|
+
: (provAdaptedProto(g, allow, c.orch_kind) === g.protocol
|
|
6309
6497
|
? t("注入该厂商凭据") : t("已适配 · 注入该厂商凭据"))) + '</span></div>' +
|
|
6310
6498
|
g.models.map((m) => {
|
|
6311
6499
|
// 本供应商注入本 CLI 实际走的 wire(与后端 _entry_endpoint 同规则);
|
|
6312
6500
|
// 无命中 = 勾了也会被解析层整条跳过,标死防止白配
|
|
6313
|
-
const wire = provAdaptedProto(g, allow);
|
|
6501
|
+
const wire = provAdaptedProto(g, allow, c.orch_kind);
|
|
6314
6502
|
const dead = !wire;
|
|
6315
6503
|
const has = st.chain.some((x) => x.p === g.id && x.m === m);
|
|
6316
6504
|
return '<label class="oitem' + (dead ? " dim" : "") + '"><input type="checkbox" value="' + esc(m) + '" data-p="' + esc(g.id) + '"' +
|
|
@@ -6380,11 +6568,19 @@ function renderCatalog() {
|
|
|
6380
6568
|
});
|
|
6381
6569
|
}
|
|
6382
6570
|
|
|
6571
|
+
/* 该条目有管理操作在跑(queued/running)时,写操作按钮置灰防重复触发;
|
|
6572
|
+
页面刷新后按钮恢复,后端同条目去重闸仍然兜底 */
|
|
6573
|
+
function mgmtBusy(id) {
|
|
6574
|
+
const st = (S.mgmt || {})[id];
|
|
6575
|
+
return !!st && (st.status === "queued" || st.status === "running");
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6383
6578
|
function card(c) {
|
|
6384
6579
|
const orchBox = c.orch_kind
|
|
6385
6580
|
? '<label class="toggle"><input type="checkbox" ' + (c.orch_enabled ? "checked" : "") +
|
|
6386
6581
|
' onchange="toggleOrch(\'' + esc(c.id) + '\', this.checked)">' + t(" 参与编排") + '</label>'
|
|
6387
6582
|
: '<span class="tag">' + t("仅管理") + '</span>';
|
|
6583
|
+
const dis = mgmtBusy(c.id) ? " disabled" : "";
|
|
6388
6584
|
const hasModels = modelGroups().length > 0;
|
|
6389
6585
|
const modelBox = c.config_writable
|
|
6390
6586
|
? (hasModels
|
|
@@ -6398,10 +6594,10 @@ function card(c) {
|
|
|
6398
6594
|
// 一键打开(web 类开浏览器 / console 类新终端跑 TUI):已安装且配了 launch 才出
|
|
6399
6595
|
c.installed && c.launch ? '<button class="primary small" onclick="openAgent(\'' + esc(c.id) + '\')">' +
|
|
6400
6596
|
(c.launch.kind === "web" ? t("打开网页") : t("打开")) + "</button>" : "",
|
|
6401
|
-
c.installed && c.orch_kind ? '<button class="ghost small" onclick="mgmt(\'' + esc(c.id) + '\', \'smoke\')">' + t("冒烟测试") + '</button>' : "",
|
|
6402
|
-
c.installed && c.has_upgrade ? '<button class="ghost small" onclick="mgmt(\'' + esc(c.id) + '\', \'upgrade\')">' + t("升级") + '</button>' : "",
|
|
6597
|
+
c.installed && c.orch_kind ? '<button class="ghost small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'smoke\')">' + t("冒烟测试") + '</button>' : "",
|
|
6598
|
+
c.installed && c.has_upgrade ? '<button class="ghost small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'upgrade\')">' + t("升级") + '</button>' : "",
|
|
6403
6599
|
c.installed && c.has_upgrade ? '<button class="ghost small" onclick="checkUpdate(\'' + esc(c.id) + '\')">' + t("检查更新") + '</button>' : "",
|
|
6404
|
-
!c.installed && c.has_install ? '<button class="ghost small" onclick="mgmt(\'' + esc(c.id) + '\', \'install\')">' + t("安装") + '</button>' : "",
|
|
6600
|
+
!c.installed && c.has_install ? '<button class="ghost small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'install\')">' + t("安装") + '</button>' : "",
|
|
6405
6601
|
c.installed ? '<button class="ghost small" onclick="showMgmtLog(\'' + esc(c.id) + '\')">' + t("日志") + '</button>' : "",
|
|
6406
6602
|
!c.installed && !c.has_install ? '<span class="hint">' + t("安装命令待配置(编辑 data/catalog.json)") + '</span>' : "",
|
|
6407
6603
|
].join("");
|
|
@@ -6409,7 +6605,7 @@ function card(c) {
|
|
|
6409
6605
|
'<div class="head"><span class="name">' + esc(c.name) + "</span>" +
|
|
6410
6606
|
(c.installed ? statusChip("done") : '<span class="tag">' + t("未安装") + '</span>') + updateChip(c) +
|
|
6411
6607
|
// 卸载放卡片右上角(与状态徽标同行),不再吊在操作行尾部
|
|
6412
|
-
(c.installed && c.uninstall_cmd ? '<button class="danger small" onclick="mgmt(\'' + esc(c.id) + '\', \'uninstall\')">' + t("卸载") + '</button>' : "") +
|
|
6608
|
+
(c.installed && c.uninstall_cmd ? '<button class="danger small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'uninstall\')">' + t("卸载") + '</button>' : "") +
|
|
6413
6609
|
"</div>" +
|
|
6414
6610
|
'<div class="note">' + esc(t(c.note || "")) + "</div>" +
|
|
6415
6611
|
'<div class="facts">' + t("版本 ") + '<b>' + esc(c.version || "-") + "</b>" + t(" 模型 ") + "<b>" + esc(fmtModel(c.model) || "-") + "</b>" +
|
|
@@ -6538,6 +6734,7 @@ async function mgmt(id, op) {
|
|
|
6538
6734
|
return;
|
|
6539
6735
|
}
|
|
6540
6736
|
S.mgmt[id].runId = r.run_id;
|
|
6737
|
+
if (r.deduped) toast(t("该条目已有进行中的任务,已转为跟踪该任务"));
|
|
6541
6738
|
S.catSig = null; renderCatalog();
|
|
6542
6739
|
pollMgmt(id, r.run_id); // 就地跟踪,不再跳转页面
|
|
6543
6740
|
}
|
|
@@ -7427,6 +7624,8 @@ async function loadSettings() {
|
|
|
7427
7624
|
S.settings = await api("/api/settings");
|
|
7428
7625
|
const inp = $("set-workers");
|
|
7429
7626
|
if (inp && S.settings) inp.value = S.settings.max_concurrent_jobs;
|
|
7627
|
+
const tel = $("set-telemetry");
|
|
7628
|
+
if (tel && S.settings) tel.checked = S.settings.telemetry_errors !== false;
|
|
7430
7629
|
const wd = $("set-workdir"), hint = $("set-workdir-hint");
|
|
7431
7630
|
if (wd && S.settings) wd.value = S.settings.default_workdir_effective || "";
|
|
7432
7631
|
if (hint && S.settings) {
|
|
@@ -7434,9 +7633,13 @@ async function loadSettings() {
|
|
|
7434
7633
|
hint.textContent = custom ? t("当前生效:") + S.settings.default_workdir_effective + t("(自定义)")
|
|
7435
7634
|
: t("当前生效:") + S.settings.default_workdir_effective + t("(内置默认,尚未自定义)");
|
|
7436
7635
|
}
|
|
7437
|
-
//
|
|
7438
|
-
|
|
7439
|
-
|
|
7636
|
+
// 新任务表单:默认就「选中」默认路径(参考工作区选择器的默认选中态);
|
|
7637
|
+
// 手动清空输入仍表示跟随默认。f-workdir-hint 保留给「继续会话/克隆任务」的覆盖警告
|
|
7638
|
+
const wdInput = $("f-workdir");
|
|
7639
|
+
if (wdInput && S.settings && !wdInput.value.trim()) {
|
|
7640
|
+
wdInput.value = S.settings.default_workdir_effective || "";
|
|
7641
|
+
queueGitProbe(); // 目录在场即探测代码版本,点亮分支胶囊
|
|
7642
|
+
}
|
|
7440
7643
|
} catch (e) { /* 忽略 */ }
|
|
7441
7644
|
}
|
|
7442
7645
|
|
|
@@ -7582,6 +7785,36 @@ function maybeWhatsnew() {
|
|
|
7582
7785
|
}
|
|
7583
7786
|
}
|
|
7584
7787
|
|
|
7788
|
+
/* ===== 首次启动欢迎引导:产品是什么 + 装完三步开工 =====
|
|
7789
|
+
* localStorage orch.welcomed 记账,只自动弹一次;「关于与更新」页可手动重开。
|
|
7790
|
+
* 令牌门在场时让位(远端新设备先输令牌,下次启动引导照常弹)。 */
|
|
7791
|
+
function welcomeOpen() {
|
|
7792
|
+
const w = $("welcome");
|
|
7793
|
+
if (!w) return;
|
|
7794
|
+
if (!$("token-gate").classList.contains("hidden")) return;
|
|
7795
|
+
w.classList.remove("hidden");
|
|
7796
|
+
document.body.classList.add("welcome-open");
|
|
7797
|
+
}
|
|
7798
|
+
function welcomeClose() {
|
|
7799
|
+
const w = $("welcome");
|
|
7800
|
+
if (!w) return;
|
|
7801
|
+
w.classList.add("hidden");
|
|
7802
|
+
document.body.classList.remove("welcome-open");
|
|
7803
|
+
try { localStorage.setItem("orch.welcomed", "1"); } catch (e) { /* 存储不可用则下次再弹 */ }
|
|
7804
|
+
}
|
|
7805
|
+
function welcomeGo(tab) {
|
|
7806
|
+
welcomeClose();
|
|
7807
|
+
switchTab(tab); // 直达「模型接入」/「CLI 绑定」
|
|
7808
|
+
}
|
|
7809
|
+
function maybeWelcome() {
|
|
7810
|
+
let seen = "";
|
|
7811
|
+
try { seen = localStorage.getItem("orch.welcomed"); } catch (e) { /* ignore */ }
|
|
7812
|
+
if (!seen) welcomeOpen();
|
|
7813
|
+
}
|
|
7814
|
+
window.welcomeOpen = welcomeOpen;
|
|
7815
|
+
window.welcomeClose = welcomeClose;
|
|
7816
|
+
window.welcomeGo = welcomeGo;
|
|
7817
|
+
|
|
7585
7818
|
function renderSu() {
|
|
7586
7819
|
const info = $("su-info");
|
|
7587
7820
|
if (!info) return;
|
|
@@ -7626,6 +7859,57 @@ function updDismiss() {
|
|
|
7626
7859
|
toast(t("已忽略该版本提醒,可随时在「关于与更新」里升级"));
|
|
7627
7860
|
}
|
|
7628
7861
|
|
|
7862
|
+
/* 匿名错误报告开关:改完立即生效(后端每轮上报前都会读一次设置) */
|
|
7863
|
+
async function saveTelemetry(on) {
|
|
7864
|
+
try {
|
|
7865
|
+
const r = await api("/api/settings", { method: "POST", body: JSON.stringify({ telemetry_errors: !!on }) });
|
|
7866
|
+
S.settings = r.settings;
|
|
7867
|
+
toast(on ? t("已开启匿名错误报告,感谢帮助改进 CodeBee")
|
|
7868
|
+
: t("已关闭匿名错误报告,数据不再离开本机"));
|
|
7869
|
+
} catch (e) {
|
|
7870
|
+
const tel = $("set-telemetry");
|
|
7871
|
+
if (tel) tel.checked = !on; // 存失败弹回旧值,不让 UI 与后端各说各话
|
|
7872
|
+
toast(e.message || t("保存失败"), true);
|
|
7873
|
+
}
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
/* 导出诊断包:脱敏错误台账 + 用量统计 + 版本环境信息(zip),贴 Issue 用 */
|
|
7877
|
+
async function exportDiagBundle() {
|
|
7878
|
+
const b = $("diag-export");
|
|
7879
|
+
if (b) b.disabled = true;
|
|
7880
|
+
try {
|
|
7881
|
+
const r = await fetch("/api/diagnostics/bundle", { headers: authHeaders() });
|
|
7882
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
7883
|
+
const u = URL.createObjectURL(await r.blob());
|
|
7884
|
+
const a = document.createElement("a");
|
|
7885
|
+
a.href = u;
|
|
7886
|
+
a.download = "codebee-diag-" + new Date().toISOString().slice(0, 10) + ".zip";
|
|
7887
|
+
document.body.appendChild(a);
|
|
7888
|
+
a.click();
|
|
7889
|
+
a.remove();
|
|
7890
|
+
setTimeout(() => URL.revokeObjectURL(u), 5000);
|
|
7891
|
+
toast(t("诊断包已开始下载,反馈问题时可附在 Issue 里"));
|
|
7892
|
+
} catch (e) {
|
|
7893
|
+
toast(t("诊断包导出失败:") + e.message, true);
|
|
7894
|
+
} finally {
|
|
7895
|
+
if (b) b.disabled = false;
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
|
|
7899
|
+
/* 一键反馈 Issue:拉脱敏摘要 → 预填 GitHub Issue 新建页(用户亲手提交,不自动回传) */
|
|
7900
|
+
async function reportIssue() {
|
|
7901
|
+
try {
|
|
7902
|
+
const s = await api("/api/diagnostics/issue-summary");
|
|
7903
|
+
const url = "https://github.com/Vercel-By-WXP/CodeBee/issues/new" +
|
|
7904
|
+
"?title=" + encodeURIComponent(s.title || "") +
|
|
7905
|
+
"&body=" + encodeURIComponent(s.body || "");
|
|
7906
|
+
window.open(url, "_blank");
|
|
7907
|
+
toast(t("已打开 GitHub 反馈页,内容已预填,可直接提交(也可附上诊断包 zip)"));
|
|
7908
|
+
} catch (e) {
|
|
7909
|
+
toast(t("反馈摘要生成失败:") + e.message, true);
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
|
|
7629
7913
|
async function suCheck() {
|
|
7630
7914
|
const b = $("su-check");
|
|
7631
7915
|
if (b) b.disabled = true;
|
|
@@ -7984,31 +8268,31 @@ function usageMultilineSvg(byDay, byDayModel) {
|
|
|
7984
8268
|
"<title>" + esc(days[i] + "\n" + s.name + " " + fmtTok(s.vals[i]) + t(" tokens")) + "</title></circle>";
|
|
7985
8269
|
});
|
|
7986
8270
|
});
|
|
7987
|
-
const grid = [0.25, 0.5, 0.75].map((f) =>
|
|
7988
|
-
'<line class="uc-grid" x1="' + padL + '" y1="' + (padT + ih * f).toFixed(1) +
|
|
7989
|
-
'" x2="' + (W - padR) + '" y2="' + (padT + ih * f).toFixed(1) + '"/>').join("") +
|
|
7990
|
-
'<line class="uc-base" x1="' + padL + '" y1="' + (padT + ih) + '" x2="' + (W - padR) + '" y2="' + (padT + ih) + '"/>';
|
|
7991
|
-
// 底层活动量柱保留每日总量轮廓;折线仍表达各模型趋势,双层信息比单一曲线
|
|
7992
|
-
// 更容易看出“哪天整体有调用、哪天只是某个模型占比变化”。
|
|
7993
|
-
const dailyTotals = days.map((_, i) => series.reduce((sum, s) => sum + (s.vals[i] || 0), 0));
|
|
7994
|
-
const dayMax = Math.max(1, ...dailyTotals);
|
|
7995
|
-
const barW = Math.max(3, (iw / n) * 0.52);
|
|
7996
|
-
const bars = dailyTotals.map((v, i) => {
|
|
7997
|
-
const h = ih * v / dayMax;
|
|
7998
|
-
const x = xOf(i) - barW / 2;
|
|
7999
|
-
const y = padT + ih - h;
|
|
8000
|
-
return '<rect class="uc-bar" x="' + x.toFixed(1) + '" y="' + y.toFixed(1) +
|
|
8001
|
-
'" width="' + barW.toFixed(1) + '" height="' + Math.max(0.5, h).toFixed(1) +
|
|
8002
|
-
'" rx="2"><title>' + esc(days[i] + " · " + fmtTok(v) + t(" tokens")) + "</title></rect>";
|
|
8003
|
-
}).join("");
|
|
8271
|
+
const grid = [0.25, 0.5, 0.75].map((f) =>
|
|
8272
|
+
'<line class="uc-grid" x1="' + padL + '" y1="' + (padT + ih * f).toFixed(1) +
|
|
8273
|
+
'" x2="' + (W - padR) + '" y2="' + (padT + ih * f).toFixed(1) + '"/>').join("") +
|
|
8274
|
+
'<line class="uc-base" x1="' + padL + '" y1="' + (padT + ih) + '" x2="' + (W - padR) + '" y2="' + (padT + ih) + '"/>';
|
|
8275
|
+
// 底层活动量柱保留每日总量轮廓;折线仍表达各模型趋势,双层信息比单一曲线
|
|
8276
|
+
// 更容易看出“哪天整体有调用、哪天只是某个模型占比变化”。
|
|
8277
|
+
const dailyTotals = days.map((_, i) => series.reduce((sum, s) => sum + (s.vals[i] || 0), 0));
|
|
8278
|
+
const dayMax = Math.max(1, ...dailyTotals);
|
|
8279
|
+
const barW = Math.max(3, (iw / n) * 0.52);
|
|
8280
|
+
const bars = dailyTotals.map((v, i) => {
|
|
8281
|
+
const h = ih * v / dayMax;
|
|
8282
|
+
const x = xOf(i) - barW / 2;
|
|
8283
|
+
const y = padT + ih - h;
|
|
8284
|
+
return '<rect class="uc-bar" x="' + x.toFixed(1) + '" y="' + y.toFixed(1) +
|
|
8285
|
+
'" width="' + barW.toFixed(1) + '" height="' + Math.max(0.5, h).toFixed(1) +
|
|
8286
|
+
'" rx="2"><title>' + esc(days[i] + " · " + fmtTok(v) + t(" tokens")) + "</title></rect>";
|
|
8287
|
+
}).join("");
|
|
8004
8288
|
const legend = '<div class="um-legend">' +
|
|
8005
8289
|
series.map((s, si) =>
|
|
8006
8290
|
'<span><i style="background:' + usageColor(si) + '"></i>' +
|
|
8007
8291
|
esc(s.name.length > 22 ? s.name.slice(0, 21) + "…" : s.name) + "</span>").join("") +
|
|
8008
8292
|
'<span class="um-peak">' + esc(t("峰值 ") + fmtTok(max)) + "</span></div>";
|
|
8009
|
-
return legend +
|
|
8010
|
-
'<svg class="usage-svg" viewBox="0 0 ' + W + " " + H + '" role="img" preserveAspectRatio="xMidYMid meet">' +
|
|
8011
|
-
grid + bars + body + labels + "</svg>";
|
|
8293
|
+
return legend +
|
|
8294
|
+
'<svg class="usage-svg" viewBox="0 0 ' + W + " " + H + '" role="img" preserveAspectRatio="xMidYMid meet">' +
|
|
8295
|
+
grid + bars + body + labels + "</svg>";
|
|
8012
8296
|
}
|
|
8013
8297
|
|
|
8014
8298
|
/* Token 活动热力图:GitHub 贡献图风格。窗口自适应——覆盖全部活跃历史(封顶
|
|
@@ -8239,10 +8523,10 @@ function renderUsage() {
|
|
|
8239
8523
|
// 不提示的话「输入 0 · 输出 0」会被误读成统计坏了
|
|
8240
8524
|
const noBreakdown = tot.tokens > 0 && !tot.input && !tot.output && !tot.cached;
|
|
8241
8525
|
$("usage-kpis").innerHTML = [
|
|
8242
|
-
kpiCard(t("累计 Token 数"), fmtTok(tot.tokens),
|
|
8243
|
-
t("调用 ") + fmtTok(tot.calls) + t(" 次 · 成功率 ") +
|
|
8244
|
-
(tot.calls ? Math.round(tot.ok * 100 / tot.calls) : 0) + "% · " +
|
|
8245
|
-
t("失败 ") + fmtTok(tot.failed || 0) + t(" · ") + fmtUsd(tot.cost_usd),
|
|
8526
|
+
kpiCard(t("累计 Token 数"), fmtTok(tot.tokens),
|
|
8527
|
+
t("调用 ") + fmtTok(tot.calls) + t(" 次 · 成功率 ") +
|
|
8528
|
+
(tot.calls ? Math.round(tot.ok * 100 / tot.calls) : 0) + "% · " +
|
|
8529
|
+
t("失败 ") + fmtTok(tot.failed || 0) + t(" · ") + fmtUsd(tot.cost_usd),
|
|
8246
8530
|
true, "i-sigma"),
|
|
8247
8531
|
kpiCard(t("峰值 Token 数"), fmtTok(tot.peak_tokens),
|
|
8248
8532
|
t("单日最高 · 日均 ") + fmtTok(activeDays ? tot.tokens / activeDays : 0), false, "i-gauge"),
|
|
@@ -8345,6 +8629,7 @@ function collapseDrawerIfMobile() {
|
|
|
8345
8629
|
|
|
8346
8630
|
function switchTab(name) {
|
|
8347
8631
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
8632
|
+
if (name === "__guide") { welcomeOpen(); return; } // 使用引导是弹层,不切页(设置导航「软件」组)
|
|
8348
8633
|
// 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
|
|
8349
8634
|
S.tab = name;
|
|
8350
8635
|
if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
|
|
@@ -8516,47 +8801,61 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8516
8801
|
b.addEventListener("click", () => setUsageHeatMode(b.dataset.heat)));
|
|
8517
8802
|
$("btn-back").addEventListener("click", closeRun);
|
|
8518
8803
|
$("btn-cancel").addEventListener("click", cancelRun);
|
|
8519
|
-
bindDirector();
|
|
8520
|
-
bindChat();
|
|
8521
|
-
const bindArtifactClicks = (root) => {
|
|
8522
|
-
if (!root) return;
|
|
8523
|
-
root.addEventListener("click", (e) => {
|
|
8524
|
-
const file = e.target.closest("[data-file-run][data-file-name]");
|
|
8525
|
-
if (!file) return;
|
|
8526
|
-
e.preventDefault();
|
|
8527
|
-
artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
|
|
8528
|
-
});
|
|
8529
|
-
};
|
|
8530
|
-
bindArtifactClicks($("rd-arts"));
|
|
8531
|
-
bindArtifactClicks($("insp-artifacts"));
|
|
8532
|
-
// 动态详情区统一用 data 属性委托,日志路径/文件名不再拼进 inline JS。
|
|
8533
|
-
const detailOverview = $("rd-overview");
|
|
8534
|
-
if (detailOverview) detailOverview.addEventListener("click", (e) => {
|
|
8535
|
-
const log = e.target.closest("[data-overview-log]");
|
|
8536
|
-
if (log) { toggleLog(log.dataset.overviewRun, log.dataset.overviewLog); return; }
|
|
8537
|
-
const tab = e.target.closest("[data-overview-tab]");
|
|
8538
|
-
if (tab) { S.rdTab = tab.dataset.overviewTab; S.rdTabPin = true; applyRdTabs(); }
|
|
8539
|
-
});
|
|
8540
|
-
const
|
|
8541
|
-
if (
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8804
|
+
bindDirector();
|
|
8805
|
+
bindChat();
|
|
8806
|
+
const bindArtifactClicks = (root) => {
|
|
8807
|
+
if (!root) return;
|
|
8808
|
+
root.addEventListener("click", (e) => {
|
|
8809
|
+
const file = e.target.closest("[data-file-run][data-file-name]");
|
|
8810
|
+
if (!file) return;
|
|
8811
|
+
e.preventDefault();
|
|
8812
|
+
artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
|
|
8813
|
+
});
|
|
8814
|
+
};
|
|
8815
|
+
bindArtifactClicks($("rd-arts"));
|
|
8816
|
+
bindArtifactClicks($("insp-artifacts"));
|
|
8817
|
+
// 动态详情区统一用 data 属性委托,日志路径/文件名不再拼进 inline JS。
|
|
8818
|
+
const detailOverview = $("rd-overview");
|
|
8819
|
+
if (detailOverview) detailOverview.addEventListener("click", (e) => {
|
|
8820
|
+
const log = e.target.closest("[data-overview-log]");
|
|
8821
|
+
if (log) { toggleLog(log.dataset.overviewRun, log.dataset.overviewLog); return; }
|
|
8822
|
+
const tab = e.target.closest("[data-overview-tab]");
|
|
8823
|
+
if (tab) { S.rdTab = tab.dataset.overviewTab; S.rdTabPin = true; applyRdTabs(); }
|
|
8824
|
+
});
|
|
8825
|
+
const rdMore = $("rd-more-toggle"), rdMetaPop = $("rd-meta-popover"), rdMoreClose = $("rd-more-close");
|
|
8826
|
+
if (rdMore) rdMore.addEventListener("click", (e) => {
|
|
8827
|
+
e.stopPropagation();
|
|
8828
|
+
setRdMetaOpen(rdMetaPop && rdMetaPop.classList.contains("hidden"));
|
|
8829
|
+
});
|
|
8830
|
+
if (rdMoreClose) rdMoreClose.addEventListener("click", () => setRdMetaOpen(false));
|
|
8831
|
+
document.addEventListener("click", (e) => {
|
|
8832
|
+
if (!rdMetaPop || rdMetaPop.classList.contains("hidden")) return;
|
|
8833
|
+
if (rdMetaPop.contains(e.target) || (rdMore && rdMore.contains(e.target))) return;
|
|
8834
|
+
setRdMetaOpen(false);
|
|
8835
|
+
});
|
|
8836
|
+
document.addEventListener("keydown", (e) => {
|
|
8837
|
+
if (e.key === "Escape" && rdMetaPop && !rdMetaPop.classList.contains("hidden")) setRdMetaOpen(false);
|
|
8838
|
+
});
|
|
8839
|
+
const stepBox = $("rd-steps");
|
|
8840
|
+
if (stepBox) stepBox.addEventListener("click", (e) => {
|
|
8841
|
+
const btn = e.target.closest("[data-log-run][data-log-rel]");
|
|
8842
|
+
const row = e.target.closest(".step[data-log]");
|
|
8843
|
+
if (btn) { e.stopPropagation(); toggleLog(btn.dataset.logRun, btn.dataset.logRel); return; }
|
|
8844
|
+
if (row && row.dataset.log) toggleLog(row.dataset.runId || S.detailRunId, row.dataset.log);
|
|
8845
|
+
});
|
|
8846
|
+
const hiveBox = $("rd-hive-cells");
|
|
8847
|
+
if (hiveBox) {
|
|
8848
|
+
hiveBox.addEventListener("click", (e) => {
|
|
8849
|
+
const cell = e.target.closest("[data-hive-run][data-hive-log]");
|
|
8850
|
+
if (cell) hiveOpenLog(cell.dataset.hiveRun, cell.dataset.hiveLog);
|
|
8851
|
+
});
|
|
8852
|
+
hiveBox.addEventListener("keydown", (e) => {
|
|
8853
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
8854
|
+
const cell = e.target.closest("[data-hive-run][data-hive-log]");
|
|
8855
|
+
if (cell) { e.preventDefault(); hiveOpenLog(cell.dataset.hiveRun, cell.dataset.hiveLog); }
|
|
8856
|
+
});
|
|
8857
|
+
}
|
|
8858
|
+
// 详情标签页:手点即切并钉住——状态变化触发的自动选卡不再抢用户的手选
|
|
8560
8859
|
$("rd-tabs").addEventListener("click", (e) => {
|
|
8561
8860
|
const b = e.target.closest(".rd-tab");
|
|
8562
8861
|
if (!b || b.classList.contains("hidden")) return;
|
|
@@ -8589,6 +8888,60 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8589
8888
|
sync();
|
|
8590
8889
|
})();
|
|
8591
8890
|
$("btn-retry").addEventListener("click", () => { const r = S.lastRun; if (r && r.task_id) retryTask(r.task_id); });
|
|
8891
|
+
/* 日志控制台高度:顶边手柄拖拽调整(localStorage 记忆),双击复位为默认弹性高度 */
|
|
8892
|
+
(() => {
|
|
8893
|
+
const grip = $("rd-log-grip"), box = $("rd-log");
|
|
8894
|
+
if (!grip || !box) return;
|
|
8895
|
+
const KEY = "orch.rdLogH";
|
|
8896
|
+
const saved = parseInt(localStorage.getItem(KEY), 10);
|
|
8897
|
+
/* 记忆值是「期望高度」:flex 允许收缩(shrink 1),窗口变矮时自动缩回视口内,
|
|
8898
|
+
永不把抽屉顶出窗口底(用户反馈「还是超长了」的根因就是固定 px 不收缩) */
|
|
8899
|
+
if (saved >= 140) box.style.flex = "0 1 " + saved + "px";
|
|
8900
|
+
grip.addEventListener("dblclick", () => {
|
|
8901
|
+
localStorage.removeItem(KEY);
|
|
8902
|
+
box.style.flex = "";
|
|
8903
|
+
});
|
|
8904
|
+
grip.addEventListener("pointerdown", (e) => {
|
|
8905
|
+
e.preventDefault();
|
|
8906
|
+
try { grip.setPointerCapture(e.pointerId); } catch (err) { /* 老浏览器降级 */ }
|
|
8907
|
+
const startY = e.clientY;
|
|
8908
|
+
const startH = box.getBoundingClientRect().height;
|
|
8909
|
+
const pre = $("rd-log-text");
|
|
8910
|
+
const stick = pre ? pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48 : false;
|
|
8911
|
+
let lastH = Math.round(Math.max(startH, 140));
|
|
8912
|
+
document.body.classList.add("rd-log-resizing");
|
|
8913
|
+
const onMove = (ev) => {
|
|
8914
|
+
lastH = Math.round(Math.min(Math.max(startH + (startY - ev.clientY), 140), window.innerHeight * 0.72));
|
|
8915
|
+
box.style.flex = "0 1 " + lastH + "px";
|
|
8916
|
+
};
|
|
8917
|
+
const onUp = (ev) => {
|
|
8918
|
+
try { grip.releasePointerCapture(ev.pointerId); } catch (err) { /* 已释放 */ }
|
|
8919
|
+
document.body.classList.remove("rd-log-resizing");
|
|
8920
|
+
grip.removeEventListener("pointermove", onMove);
|
|
8921
|
+
grip.removeEventListener("pointerup", onUp);
|
|
8922
|
+
grip.removeEventListener("pointercancel", onUp);
|
|
8923
|
+
localStorage.setItem(KEY, String(lastH)); // 存钳制后的目标值,隐藏态 rect 是 0 不能用
|
|
8924
|
+
if (stick && pre) pre.scrollTop = pre.scrollHeight; // 拖完仍在贴底状态则继续跟最新
|
|
8925
|
+
};
|
|
8926
|
+
grip.addEventListener("pointermove", onMove);
|
|
8927
|
+
grip.addEventListener("pointerup", onUp);
|
|
8928
|
+
grip.addEventListener("pointercancel", onUp);
|
|
8929
|
+
});
|
|
8930
|
+
})();
|
|
8931
|
+
if ($("btn-talk")) $("btn-talk").addEventListener("click", window.rdTalkToggle);
|
|
8932
|
+
if ($("rd-talk-send")) $("rd-talk-send").addEventListener("click", window.rdTalkSend);
|
|
8933
|
+
if ($("rd-talk-input")) $("rd-talk-input").addEventListener("keydown", (e) => {
|
|
8934
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); window.rdTalkSend(); }
|
|
8935
|
+
});
|
|
8936
|
+
if ($("rd-talk-attach")) $("rd-talk-attach").addEventListener("click", () => $("rd-talk-file").click());
|
|
8937
|
+
if ($("rd-talk-file")) $("rd-talk-file").addEventListener("change", (e) => {
|
|
8938
|
+
talkUploadFiles(Array.from(e.target.files || []));
|
|
8939
|
+
e.target.value = "";
|
|
8940
|
+
});
|
|
8941
|
+
if ($("rd-talk-input")) $("rd-talk-input").addEventListener("paste", (e) => {
|
|
8942
|
+
const files = Array.from((e.clipboardData || {}).files || []);
|
|
8943
|
+
if (files.length) { e.preventDefault(); talkUploadFiles(files); }
|
|
8944
|
+
});
|
|
8592
8945
|
$("btn-continue").addEventListener("click", () => {
|
|
8593
8946
|
const tid = (S.lastRun && S.lastRun.task_id) || S.detailTaskKey || "";
|
|
8594
8947
|
if (tid) continueSerial(tid);
|
|
@@ -8674,8 +9027,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8674
9027
|
$("f-resume-agent").addEventListener("change", loadSessions);
|
|
8675
9028
|
$("f-resume-session").addEventListener("change", showResumeHint);
|
|
8676
9029
|
/* 工作目录不再用「上次用过」的旧记忆预填:那样改默认保存路径后,新建任务仍会被
|
|
8677
|
-
*
|
|
8678
|
-
*
|
|
9030
|
+
* 旧目录盖过。首屏默认选中设置里的默认路径(loadSettings 落值),克隆/右键新建
|
|
9031
|
+
* 等显式入口各自直接写字段值,不跨刷新持久化。清掉旧版残留键,避免误解为默认选中。 */
|
|
8679
9032
|
localStorage.removeItem("orch.workdir");
|
|
8680
9033
|
// 附件:按钮选文件 + 目标框粘贴截图;工作目录变化时探测 git 仓库(代码版本下拉)
|
|
8681
9034
|
$("btn-attach").addEventListener("click", () => $("f-attach-file").click());
|
|
@@ -8683,6 +9036,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8683
9036
|
addAttachFiles(Array.from(e.target.files || []));
|
|
8684
9037
|
e.target.value = ""; // 允许重复选同一个文件
|
|
8685
9038
|
});
|
|
9039
|
+
const attChipBox = $("att-chips");
|
|
9040
|
+
if (attChipBox) attChipBox.addEventListener("click", (e) => {
|
|
9041
|
+
const remove = e.target.closest("[data-att-remove]");
|
|
9042
|
+
if (remove) {
|
|
9043
|
+
e.preventDefault();
|
|
9044
|
+
e.stopPropagation();
|
|
9045
|
+
removeAtt(remove.dataset.attRemove || "");
|
|
9046
|
+
}
|
|
9047
|
+
});
|
|
8686
9048
|
$("f-goal").addEventListener("paste", onGoalPaste);
|
|
8687
9049
|
// Composer 手感:Enter 直接发送,Shift+Enter 换行(输入法组词中不劫持)
|
|
8688
9050
|
$("f-goal").addEventListener("keydown", (e) => {
|
|
@@ -8692,6 +9054,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8692
9054
|
$("f-workdir").addEventListener("change", queueGitProbe);
|
|
8693
9055
|
// 点工作目录输入框直接进文件夹选择(远端设备被 403 守卫拦下,静默继续手输)
|
|
8694
9056
|
$("f-workdir").addEventListener("click", () => window.pickFolder("f-workdir", true));
|
|
9057
|
+
// 最近文件夹菜单:条目点击统一走 wdMenuPick;点外面收起
|
|
9058
|
+
const wdMenu = $("wd-menu");
|
|
9059
|
+
if (wdMenu) wdMenu.addEventListener("click", (e) => {
|
|
9060
|
+
const item = e.target.closest(".wd-item");
|
|
9061
|
+
if (!item) return;
|
|
9062
|
+
e.stopPropagation();
|
|
9063
|
+
window.wdMenuPick(item);
|
|
9064
|
+
});
|
|
9065
|
+
document.addEventListener("click", (e) => {
|
|
9066
|
+
if (wdMenu && !wdMenu.classList.contains("hidden") &&
|
|
9067
|
+
!e.target.closest("#wd-menu, #f-workdir-menu-btn")) wdMenu.classList.add("hidden");
|
|
9068
|
+
});
|
|
8695
9069
|
$("set-workdir").addEventListener("click", () => window.pickFolder("set-workdir", true));
|
|
8696
9070
|
$("f-git-rev").addEventListener("change", renderGitHint);
|
|
8697
9071
|
if ($("f-workdir").value) queueGitProbe(); // 回填的目录:进页面即探测代码版本
|
|
@@ -8736,6 +9110,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8736
9110
|
&& e.target.tagName !== "TEXTAREA") { _askClose(true); return; }
|
|
8737
9111
|
// 文件内容弹窗(240) 夹在 ask(250) 与 modal(200) 之间,Esc 同样逐层关
|
|
8738
9112
|
if (e.key === "Escape" && filePopIsOpen()) { window.filePopClose(); return; }
|
|
9113
|
+
if (e.key === "Escape" && !$("welcome").classList.contains("hidden")) { welcomeClose(); return; }
|
|
8739
9114
|
if (e.key === "Escape" && !$("modal").classList.contains("hidden")) closeModal();
|
|
8740
9115
|
// 日志抽屉:Esc 收起(最底层,放在弹窗之后)
|
|
8741
9116
|
if (e.key === "Escape" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
|
|
@@ -8749,7 +9124,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8749
9124
|
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable;
|
|
8750
9125
|
if (!typing && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === "n" || e.key === "N")
|
|
8751
9126
|
&& $("modal").classList.contains("hidden") && $("ask").classList.contains("hidden")
|
|
8752
|
-
&& filePopIsOpen() === false && $("cmdk-mask").classList.contains("hidden")
|
|
9127
|
+
&& $("welcome").classList.contains("hidden") && filePopIsOpen() === false && $("cmdk-mask").classList.contains("hidden")
|
|
8753
9128
|
&& !document.body.classList.contains("settings-mode")
|
|
8754
9129
|
&& !document.body.classList.contains("files-mode")) {
|
|
8755
9130
|
$("btn-new-task").click();
|
|
@@ -8811,8 +9186,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8811
9186
|
loadFlows(); // 任务类型下拉(内置 + 自定义流程)
|
|
8812
9187
|
refreshSessionAgents(); // 继续会话下拉的工具集合(服务端 60s 缓存,开销小)
|
|
8813
9188
|
loadOrchestrator(); // 侧栏左下角的编排者供应商指示(进入编排设置页时会再拉一次)
|
|
8814
|
-
loadSettings(); //
|
|
9189
|
+
loadSettings(); // 启动即拉设置:新建表单「默认选中默认路径」依赖它
|
|
8815
9190
|
suStartupCheck(); // 静默查一次新版本(有新版 toast 提醒,同版本只提一次)
|
|
9191
|
+
maybeWelcome(); // 首次启动弹欢迎引导(orch.welcomed 记账,只弹一次)
|
|
8816
9192
|
});
|
|
8817
9193
|
|
|
8818
9194
|
/* ===== 命令面板(Ctrl+K / 侧栏搜索行):任务直达 + 快捷命令 ===== */
|
|
@@ -8827,6 +9203,7 @@ function cmdkOps() {
|
|
|
8827
9203
|
{ icon: "i-cpu", label: t("智能体管理"), run: () => switchTab("agents") },
|
|
8828
9204
|
{ icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
|
|
8829
9205
|
{ icon: "i-gear", label: t("设置"), run: () => enterSettings() },
|
|
9206
|
+
{ icon: "i-bee", label: t("使用引导"), run: () => welcomeOpen() },
|
|
8830
9207
|
{ icon: "i-phone", label: t("手机连接"), run: () => switchTab("__phone") },
|
|
8831
9208
|
];
|
|
8832
9209
|
}
|