codebee 0.1.73 → 0.1.74
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 +6 -0
- package/README.md +3 -3
- package/app/core/analytics.py +38 -0
- package/app/core/bookmeta.py +0 -14
- package/app/core/contracts.py +255 -0
- package/app/core/error_codes.py +9 -0
- package/app/core/health.py +14 -1
- package/app/core/jobs.py +50 -0
- package/app/core/manager.py +38 -14
- package/app/core/modelhub.py +104 -9
- package/app/core/openapi.py +154 -0
- package/app/core/pipeline.py +41 -0
- package/app/core/presence.py +77 -0
- package/app/core/publish/auto.py +11 -0
- package/app/core/retrieval.py +137 -0
- package/app/core/runner.py +85 -2
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +241 -0
- package/app/main.py +181 -20
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +129 -31
- package/app/ui/i18n.js +19 -5
- package/app/ui/index.html +5 -0
- package/app/ui/style.css +10 -0
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -10,7 +10,7 @@ function codebeeDocumentTitle(lang) {
|
|
|
10
10
|
document.title = codebeeDocumentTitle((localStorage.getItem("orch.lang") || "zh").toLowerCase());
|
|
11
11
|
|
|
12
12
|
const $ = (id) => document.getElementById(id);
|
|
13
|
-
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", mainPage: "", /* 主栏正显示的全局页("" = 新建任务表单);左栏是任务树还是设置导航看 body.settings-mode */ detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, provStatus: "enabled", mlistStatus: "enabled", /* 供应商/模型列表状态过滤:默认只看启用,停用的点对应 chip 才现身 */ 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, rdHiveFirst: false, _rdCtx: {}, lastPrefs: null, prefsApplied: false, sideUsage: null, sideUsageAt: 0, ovUsage: null, ovDays: undefined };
|
|
13
|
+
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", mainPage: "", /* 主栏正显示的全局页("" = 新建任务表单);左栏是任务树还是设置导航看 body.settings-mode */ detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, provStatus: "enabled", mlistStatus: "enabled", /* 供应商/模型列表状态过滤:默认只看启用,停用的点对应 chip 才现身 */ 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, rdHiveFirst: false, _rdCtx: {}, taskContracts: {}, lastPrefs: null, prefsApplied: false, sideUsage: null, sideUsageAt: 0, ovUsage: null, ovDays: undefined };
|
|
14
14
|
|
|
15
15
|
/* ---------------------------------------------------------- 内置浏览器工作区 */
|
|
16
16
|
const BROWSER_STORAGE_KEY = "orch.browser.workspace.v1";
|
|
@@ -1271,20 +1271,34 @@ async function api(path, opts) {
|
|
|
1271
1271
|
|
|
1272
1272
|
/* 质量门禁三选一弹框(替代原生 confirm 的单一「强制放行」出口):
|
|
1273
1273
|
* 取消 / ↻ 重新评审 / 强制放行。「重新评审」= 重写未达标章再评(语义同详情页
|
|
1274
|
-
* 「↻ 重写未达标章」按钮,走 retry 的未达标分支),仅 serial
|
|
1275
|
-
*
|
|
1274
|
+
* 「↻ 重写未达标章」按钮,走 retry 的未达标分支),仅 serial 任务才给;任务
|
|
1275
|
+
* 运行/排队中按钮置灰(后端 retry 会拒),但必须显出来并写明出路——正在跑的
|
|
1276
|
+
* 这轮结束后再来发布,仍不达标时按钮即可点;无声藏按钮用户只会看到死路。
|
|
1277
|
+
* 返回 "review" | "force" | null(取消)。 */
|
|
1276
1278
|
async function pbGateChoice(err, promptText, taskId) {
|
|
1277
1279
|
const tk = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
+
const busy = !!(tk && (tk.status === "running" || tk.status === "queued"));
|
|
1281
|
+
const canReview = !!(tk && tk.serial && !busy);
|
|
1282
|
+
const resumeAt = busy
|
|
1283
|
+
? ((((S.state || {}).task_latest || {})[taskId] || {}).resume_enqueue_at || "") : "";
|
|
1284
|
+
const busyHint = (tk && tk.serial && busy)
|
|
1285
|
+
? (resumeAt
|
|
1286
|
+
? t("任务正在运行/排队中,预计 {0} 自动续跑,暂不能重复发起评审;等这轮跑完再来点发布——若质量仍不达标,这里就能点「重新评审」(只重写未达标章)。",
|
|
1287
|
+
String(resumeAt).slice(11, 16))
|
|
1288
|
+
: t("任务正在运行/排队中,暂不能重复发起评审;等这轮跑完再来点发布——若质量仍不达标,这里就能点「重新评审」(只重写未达标章)。"))
|
|
1289
|
+
: "";
|
|
1280
1290
|
const msg = (promptText || t("质量门禁未通过")) + "\n\n" + ((err && err.message) || "");
|
|
1281
1291
|
const v = await _askOpen({
|
|
1282
1292
|
title: t("质量门禁未通过"),
|
|
1283
1293
|
bodyHtml: '<div class="ask-msg">' + esc(msg) + "</div>" +
|
|
1284
1294
|
(canReview ? '<div class="ask-msg">' +
|
|
1285
|
-
esc(t("「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。")) + "</div>" : "")
|
|
1295
|
+
esc(t("「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。")) + "</div>" : "") +
|
|
1296
|
+
(busyHint ? '<div class="ask-msg">' + esc(busyHint) + "</div>" : ""),
|
|
1286
1297
|
okText: t("强制放行…"),
|
|
1287
|
-
extra:
|
|
1298
|
+
extra: (tk && tk.serial)
|
|
1299
|
+
? [{ text: "↻ " + t("重新评审"), value: "review", disabled: busy,
|
|
1300
|
+
title: busy ? t("任务运行/排队中,暂不能发起;等这轮跑完再试") : "" }]
|
|
1301
|
+
: [],
|
|
1288
1302
|
onOpen: () => $("ask-yes").focus(),
|
|
1289
1303
|
});
|
|
1290
1304
|
return v === "review" ? "review" : (v === true ? "force" : null);
|
|
@@ -1363,6 +1377,8 @@ function _askOpen(opts) {
|
|
|
1363
1377
|
const el = document.createElement("button");
|
|
1364
1378
|
el.className = (b.cls || "ghost") + " ask-extra";
|
|
1365
1379
|
el.textContent = b.text;
|
|
1380
|
+
el.disabled = !!b.disabled; // 置灰态:看得见点不了(配合 title 说明原因)
|
|
1381
|
+
if (b.title) el.title = b.title;
|
|
1366
1382
|
el.addEventListener("click", () => _askClose(b.value));
|
|
1367
1383
|
foot.insertBefore(el, $("ask-no"));
|
|
1368
1384
|
});
|
|
@@ -3129,6 +3145,9 @@ async function createTask() {
|
|
|
3129
3145
|
S.clarifyDone = false;
|
|
3130
3146
|
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
3131
3147
|
else unhideSideDir(payload.workdir); // 在已移除的目录新建任务 → 自动恢复显示
|
|
3148
|
+
const acceptance = (($('f-acceptance') || {}).value || "").split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
3149
|
+
if (acceptance.length) payload.acceptance_criteria = acceptance.slice(0, 40);
|
|
3150
|
+
payload.approval_required = !!(($('f-approval-required') || {}).checked);
|
|
3132
3151
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
3133
3152
|
// 附件与代码版本:任务创建时服务端把待提交附件移入 _attachments/ 并注入上下文
|
|
3134
3153
|
if ((S.atts || []).length) payload.attachments = S.atts.filter((a) => a.id).map((a) => a.id);
|
|
@@ -4607,6 +4626,20 @@ function setRetryBtn(run, task) {
|
|
|
4607
4626
|
: t("再跑一次这个任务;连载任务会断点续跑,已完成章节不重写");
|
|
4608
4627
|
}
|
|
4609
4628
|
|
|
4629
|
+
/* 「立即重试」:退避窗口内的续跑副本(将于 HH:MM 自动续跑)可跳过等待直接
|
|
4630
|
+
* 入队。只认 queued + 未来时刻的 resume_enqueue_at;普通并发排队每 15s 有
|
|
4631
|
+
* 补跑心跳,用不上这个按钮。退避相位在两处渲染的签名里都有(resumePending /
|
|
4632
|
+
* etaBucket 分钟桶),到点后下一帧自然隐藏。 */
|
|
4633
|
+
function setResumeNowBtn(run) {
|
|
4634
|
+
const b = $("btn-resume-now");
|
|
4635
|
+
if (!b) return;
|
|
4636
|
+
const at = run && run.status === "queued"
|
|
4637
|
+
? Date.parse(String(run.resume_enqueue_at || "").replace(" ", "T")) : NaN;
|
|
4638
|
+
const inBackoff = !isNaN(at) && Date.now() < at;
|
|
4639
|
+
b.classList.toggle("hidden", !inBackoff);
|
|
4640
|
+
S.resumeNowRunId = inBackoff ? run.id : null;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4610
4643
|
/* 任务级详情:聚合该任务所有 run 的步骤。
|
|
4611
4644
|
* 有任务档案的从 /api/tasks/<id>/runs 拉全量——前端 run 窗口只有最近 40 条,
|
|
4612
4645
|
* 窗口外的历史会被算丢;无主运行组(管理操作/任务已删)仍用窗口数据。
|
|
@@ -4624,6 +4657,7 @@ function renderTaskDetail() {
|
|
|
4624
4657
|
setRetryBtn(retrySnapshotForTask(key, tk, latestSummary), tk);
|
|
4625
4658
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
4626
4659
|
syncArchBtn(tk, tk && (tk.status === "running" || tk.status === "queued"));
|
|
4660
|
+
setResumeNowBtn(null); // 退避与否要看 run 数据,先收起防上一个任务残留
|
|
4627
4661
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
4628
4662
|
if (isTask) {
|
|
4629
4663
|
loadTaskRuns(key, latestSummary);
|
|
@@ -4687,6 +4721,7 @@ function loadTaskRuns(key, latestSummary) {
|
|
|
4687
4721
|
offset: Number(data.offset) + runs.length, total: Number(data.total),
|
|
4688
4722
|
hasMore: !!data.has_more, totals: data.totals || null, loading: false,
|
|
4689
4723
|
});
|
|
4724
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4690
4725
|
_taskRunsCache.delete(key);
|
|
4691
4726
|
_taskRunsCache.set(key, runs);
|
|
4692
4727
|
while (_taskRunsCache.size > 3) {
|
|
@@ -4736,6 +4771,7 @@ async function loadEarlierTaskRuns(key) {
|
|
|
4736
4771
|
page.offset = Number(data.offset) + older.length;
|
|
4737
4772
|
page.total = Number(data.total) || page.total;
|
|
4738
4773
|
page.hasMore = !!data.has_more;
|
|
4774
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4739
4775
|
drawTaskDetail(key, _taskRunsCache.get(key), appendFrom);
|
|
4740
4776
|
} catch (e) {
|
|
4741
4777
|
toast(t("加载历史运行失败:") + e.message, true);
|
|
@@ -4830,6 +4866,7 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4830
4866
|
// S.detailRunId,cancelRun 靠 S.cancelTargetRunId 知道取消谁
|
|
4831
4867
|
$("btn-cancel").classList.toggle("hidden", !activeRun);
|
|
4832
4868
|
S.cancelTargetRunId = activeRun ? activeRun.id : null;
|
|
4869
|
+
setResumeNowBtn(activeRun && activeRun.status === "queued" ? activeRun : null);
|
|
4833
4870
|
const bp2 = $("btn-pause");
|
|
4834
4871
|
if (bp2 && activeRun) {
|
|
4835
4872
|
bp2.classList.toggle("hidden", false);
|
|
@@ -5387,6 +5424,7 @@ async function renderRunDetail() {
|
|
|
5387
5424
|
const active = run.status === "queued" || run.status === "running";
|
|
5388
5425
|
$("btn-cancel").classList.toggle("hidden", !active);
|
|
5389
5426
|
S.cancelTargetRunId = active ? run.id : null;
|
|
5427
|
+
setResumeNowBtn(run.status === "queued" ? run : null); // 在 sig 守卫前:退避相位由 etaBucket 分钟桶兜底刷新
|
|
5390
5428
|
$("btn-delete").classList.toggle("hidden", active);
|
|
5391
5429
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5392
5430
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
@@ -5611,12 +5649,6 @@ function bmRootTask(task) {
|
|
|
5611
5649
|
return cur || task;
|
|
5612
5650
|
}
|
|
5613
5651
|
|
|
5614
|
-
window.bmGoRoot = function (rootId) {
|
|
5615
|
-
const t = (((S.state || {}).tasks || []).find((x) => x.id === rootId)) || null;
|
|
5616
|
-
if (!t) { toast(t("找不到首批任务(可能已删除)"), true); return; }
|
|
5617
|
-
browserOpenTaskRun(t, "bookmeta");
|
|
5618
|
-
};
|
|
5619
|
-
|
|
5620
5652
|
function renderBookMetaPanel(task) {
|
|
5621
5653
|
const box = $("rd-bookmeta");
|
|
5622
5654
|
if (!box || !(task || {}).id || !bmNeedsPanel(task)) {
|
|
@@ -5627,10 +5659,14 @@ function renderBookMetaPanel(task) {
|
|
|
5627
5659
|
//(正在填登记表单)或章节清单正展开时跳过本帧整块重绘——innerHTML
|
|
5628
5660
|
// 重建会把没提交的字清掉、把展开列表折回去。轮询链继续转,blur/收起
|
|
5629
5661
|
// 后下一轮数据变化自然跟上;登记草稿另有 S.pbRegDraft 兜底回填。
|
|
5662
|
+
// 守卫只保护同一任务(box.dataset.taskId):切到别的任务的详情时,
|
|
5663
|
+
// 上一任务的展开态没有保留价值,放行重绘——否则新任务详情的作品信息
|
|
5664
|
+
// 页签一直挂着上一本书的建书/发布数据(2026-09-30 用户实案)。
|
|
5630
5665
|
const ae = document.activeElement;
|
|
5631
|
-
if (
|
|
5666
|
+
if (box.dataset.taskId === task.id &&
|
|
5667
|
+
((ae && box.contains(ae) &&
|
|
5632
5668
|
(ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) ||
|
|
5633
|
-
|
|
5669
|
+
box.querySelector(".pb-chapters:not(.hidden)"))) {
|
|
5634
5670
|
pbSyncState(task);
|
|
5635
5671
|
return;
|
|
5636
5672
|
}
|
|
@@ -5646,21 +5682,12 @@ function renderBookMetaPanel(task) {
|
|
|
5646
5682
|
'<button class="hhelp" data-help-topic="publish" title="' + esc(t("这是什么?点开看帮助")) + '" aria-label="' + esc(t("帮助")) + '"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button>' +
|
|
5647
5683
|
'<span class="flex1"></span>';
|
|
5648
5684
|
if (taskBusy) html += '<span class="bm-warn">' + esc(t("任务运行中,生成将在本轮结束后可用")) + "</span>";
|
|
5649
|
-
// 续写批次只读沿用:生成/重生成回首批任务做,这里给指路入口(一本书一份资料)
|
|
5650
|
-
if (inherited) html += '<button class="ghost" onclick="bmGoRoot(\'' + esc(src.id) + '\')" title="' +
|
|
5651
|
-
esc(t("开书资料在首批任务上生成与维护")) + '">' +
|
|
5652
|
-
'<svg class="ico" aria-hidden="true"><use href="#i-arrow-right"/></svg>' + t("回首批任务") + "</button>";
|
|
5653
5685
|
html += "</div>";
|
|
5654
5686
|
if (!bm.fanqie && !bm.qimao && !taskBusy) {
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
} else {
|
|
5660
|
-
html += '<div class="bm-guide"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5661
|
-
'<div><b>' + esc(t("章节已就绪,创建作品还差开书资料")) + "</b>" +
|
|
5662
|
-
"<p>" + esc(t("点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。")) + "</p></div></div>";
|
|
5663
|
-
}
|
|
5687
|
+
// 生成入口不再区分首批/续写(2026-09-30 放开;后端落点统一在链上根任务),指引文案只留一份
|
|
5688
|
+
html += '<div class="bm-guide"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5689
|
+
'<div><b>' + esc(t("章节已就绪,创建作品还差开书资料")) + "</b>" +
|
|
5690
|
+
"<p>" + esc(t("点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。")) + "</p></div></div>";
|
|
5664
5691
|
}
|
|
5665
5692
|
html += '<div class="bm-cards">' + BOOKMETA_PLATFORMS.map((p) => {
|
|
5666
5693
|
const entry = bm[p.id] || {};
|
|
@@ -5690,9 +5717,7 @@ function renderBookMetaPanel(task) {
|
|
|
5690
5717
|
body = '<div class="bm-empty">' + esc(t("尚未生成")) + "</div>";
|
|
5691
5718
|
}
|
|
5692
5719
|
let action;
|
|
5693
|
-
if (
|
|
5694
|
-
action = ""; // 续写批次只读:生成/重生成集中在头部「回首批任务」入口
|
|
5695
|
-
} else if (st === "running") {
|
|
5720
|
+
if (st === "running") {
|
|
5696
5721
|
action = '<button class="ghost" disabled><svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + t("生成中…") + "</button>";
|
|
5697
5722
|
} else if (st === "done") {
|
|
5698
5723
|
action = '<button class="ghost" onclick="bmGen(\'' + esc(task.id) + "', '" + p.id + '\')" title="' + esc(t("重新生成会覆盖现有内容")) + '">' +
|
|
@@ -5748,6 +5773,7 @@ function renderBookMetaPanel(task) {
|
|
|
5748
5773
|
bmStatusChip(cgSt) + '<span class="flex1"></span>' + cgAction + "</div>" + cgBody + "</div></div>";
|
|
5749
5774
|
box.classList.remove("hidden");
|
|
5750
5775
|
box.innerHTML = html;
|
|
5776
|
+
box.dataset.taskId = task.id; // 跨任务守卫标记:展开态/输入保护只对同一任务生效
|
|
5751
5777
|
// TAB 徽章:已生成平台数(生成中显示 ●,随下次轮询刷新)
|
|
5752
5778
|
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="bookmeta"] .rd-badge');
|
|
5753
5779
|
if (badge) {
|
|
@@ -8261,6 +8287,23 @@ async function cancelRun() {
|
|
|
8261
8287
|
toast(r && r.ok ? t("已强制终止运行") : t("该运行已结束,无需取消"));
|
|
8262
8288
|
}
|
|
8263
8289
|
|
|
8290
|
+
/* 立即重试:跳过自动续跑的退避等待,马上把续跑副本入队。任务级详情没有
|
|
8291
|
+
* S.detailRunId,目标用渲染时钉好的 resumeNowRunId(与 cancel 同款约定)。 */
|
|
8292
|
+
async function resumeNow() {
|
|
8293
|
+
const rid = S.detailRunId || S.resumeNowRunId;
|
|
8294
|
+
if (!rid) return;
|
|
8295
|
+
let r;
|
|
8296
|
+
try {
|
|
8297
|
+
r = await api("/api/runs/" + encodeURIComponent(rid) + "/resume_now", { method: "POST" });
|
|
8298
|
+
} catch (e) { toast(t("操作失败:") + (e && e.message ? e.message : t("网络异常")), true); return; }
|
|
8299
|
+
if (r && r.ok) {
|
|
8300
|
+
toast(t("已跳过等待,马上继续"));
|
|
8301
|
+
refreshState(); // 后端 bump 也会推 SSE,这里主动刷一轮让按钮立刻消失
|
|
8302
|
+
} else {
|
|
8303
|
+
toast((r && r.error) || t("操作失败"), true);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
|
|
8264
8307
|
/* ---------------------------------------------------- 运行中指挥(消息信箱) */
|
|
8265
8308
|
/* 用户在详情页往运行中的任务「递话」:文字 + 附件(截图/文件)。
|
|
8266
8309
|
* 无头 CLI 插不进正在跑的进程,指令在下一次步骤下达前由后端 drain 注入。
|
|
@@ -8965,6 +9008,7 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8965
9008
|
const duration = runDurationSeconds(run);
|
|
8966
9009
|
const etaText = runEtaText(run);
|
|
8967
9010
|
const goal = (task && task.goal) || run.goal || run.title || "";
|
|
9011
|
+
const contract = (task && task.contract) || S.taskContracts[(task && task.id) || run.task_id] || null;
|
|
8968
9012
|
const actions = [];
|
|
8969
9013
|
if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
|
|
8970
9014
|
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>");
|
|
@@ -8987,10 +9031,63 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8987
9031
|
(etaText ? '<span class="rd-eta">' + esc(etaText) + '</span>' : "") +
|
|
8988
9032
|
'</div>' +
|
|
8989
9033
|
(lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
|
|
9034
|
+
(contract ? contractOverviewHtml(contract) : "") +
|
|
8990
9035
|
(actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
|
|
8991
9036
|
box.classList.remove("hidden");
|
|
8992
9037
|
}
|
|
8993
9038
|
|
|
9039
|
+
function contractOverviewHtml(contract) {
|
|
9040
|
+
const criteria = Array.isArray(contract.acceptance_criteria) ? contract.acceptance_criteria : [];
|
|
9041
|
+
const evidence = Array.isArray(contract.evidence) ? contract.evidence : [];
|
|
9042
|
+
const latest = {};
|
|
9043
|
+
evidence.forEach((e) => { if (e && e.criterion) latest[e.criterion] = e; });
|
|
9044
|
+
const passed = criteria.filter((c) => latest[c] && latest[c].status === "passed").length;
|
|
9045
|
+
const approval = contract.approval_required
|
|
9046
|
+
? (contract.approval && contract.approval.status === "approved" ? t("已审批") : t("待审批")) : t("无需审批");
|
|
9047
|
+
const taskId = String(contract.task_id || "");
|
|
9048
|
+
const approvalAction = contract.approval_required && (!contract.approval || contract.approval.status !== "approved")
|
|
9049
|
+
? '<button type="button" class="ghost small" onclick="contractAction(\'' + jsq(taskId) + '\',\'approve\')">' + esc(t("批准发布")) + '</button>' : "";
|
|
9050
|
+
const evidenceText = criteria.map((c) => {
|
|
9051
|
+
const e = latest[c];
|
|
9052
|
+
const controls = '<button type="button" class="rd-contract-mark pass" title="' + esc(t("标记通过")) + '" onclick="contractEvidenceAction(\'' + jsq(taskId) + '\',\'' + jsq(c) + '\',\'passed\')">✓</button>' +
|
|
9053
|
+
'<button type="button" class="rd-contract-mark fail" title="' + esc(t("标记失败")) + '" onclick="contractEvidenceAction(\'' + jsq(taskId) + '\',\'' + jsq(c) + '\',\'failed\')">×</button>';
|
|
9054
|
+
return '<span class="rd-contract-criterion ' + (e ? esc(e.status) : "pending") + '">' + esc(c) + ': ' + esc(e ? e.status : t("待验收")) + controls + '</span>';
|
|
9055
|
+
}).join("");
|
|
9056
|
+
return '<div class="rd-contract"><b>' + esc(t("任务契约")) + '</b>' +
|
|
9057
|
+
'<span>' + esc(t("验收")) + ' ' + passed + '/' + criteria.length + '</span>' +
|
|
9058
|
+
'<span>' + esc(t("审批")) + ' ' + esc(approval) + '</span>' +
|
|
9059
|
+
(evidenceText ? '<div class="rd-contract-evidence">' + evidenceText + '</div>' : "") + approvalAction +
|
|
9060
|
+
(contract.blocked_reason ? '<span class="err">' + esc(t("Blocked:")) + esc(contract.blocked_reason) + '</span>' : "") +
|
|
9061
|
+
'</div>';
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
async function contractAction(taskId, op) {
|
|
9065
|
+
try {
|
|
9066
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/contract", {
|
|
9067
|
+
method: "POST", body: JSON.stringify({ op: op, actor: "ui" })
|
|
9068
|
+
});
|
|
9069
|
+
if (data && data.contract) S.taskContracts[taskId] = data.contract;
|
|
9070
|
+
toast(op === "approve" ? t("已批准发布") : t("契约已更新"));
|
|
9071
|
+
refreshState();
|
|
9072
|
+
} catch (e) { toast((e && e.message) || t("操作失败"), true); }
|
|
9073
|
+
}
|
|
9074
|
+
window.contractAction = contractAction;
|
|
9075
|
+
|
|
9076
|
+
async function contractEvidenceAction(taskId, criterion, status) {
|
|
9077
|
+
const summary = await uiPrompt(t("证据说明(可空)"), "");
|
|
9078
|
+
if (summary === null) return;
|
|
9079
|
+
try {
|
|
9080
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/contract", {
|
|
9081
|
+
method: "POST", body: JSON.stringify({ op: "evidence", actor: "ui",
|
|
9082
|
+
evidence: { criterion: criterion, status: status, summary: summary, source: "manual" } })
|
|
9083
|
+
});
|
|
9084
|
+
if (data && data.contract) S.taskContracts[taskId] = data.contract;
|
|
9085
|
+
toast(t("证据已记录"));
|
|
9086
|
+
refreshState();
|
|
9087
|
+
} catch (e) { toast((e && e.message) || t("操作失败"), true); }
|
|
9088
|
+
}
|
|
9089
|
+
window.contractEvidenceAction = contractEvidenceAction;
|
|
9090
|
+
|
|
8994
9091
|
/* 对话条三件套:与任务参数双向同步(2026-09-21 用户反馈「这里面也需要同步改」)。
|
|
8995
9092
|
* 回填自当前任务;改动即写任务(空闲态),发送 /chat 前再兜底同步一次。 */
|
|
8996
9093
|
let rdPrefsTaskId = "";
|
|
@@ -15270,6 +15367,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15270
15367
|
? S.detailTaskKey : "");
|
|
15271
15368
|
if (taskId) retryTask(taskId);
|
|
15272
15369
|
});
|
|
15370
|
+
$("btn-resume-now").addEventListener("click", resumeNow);
|
|
15273
15371
|
/* 日志控制台高度:顶边手柄拖拽调整(localStorage 记忆),双击复位为默认弹性高度 */
|
|
15274
15372
|
(() => {
|
|
15275
15373
|
const grip = $("rd-log-grip"), box = $("rd-log");
|
package/app/ui/i18n.js
CHANGED
|
@@ -749,6 +749,9 @@
|
|
|
749
749
|
"「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。": "“Re-review” rewrites only the unqualified chapters and reviews them again (qualified chapters keep their drafts and scores). Publish again once it finishes.",
|
|
750
750
|
"已发起重新评审(重写未达标章)——评审完成后再来发布": "Re-review started (rewriting unqualified chapters) — publish again after it finishes",
|
|
751
751
|
"发起重新评审失败:": "Failed to start re-review: ",
|
|
752
|
+
"任务正在运行/排队中,预计 {0} 自动续跑,暂不能重复发起评审;等这轮跑完再来点发布——若质量仍不达标,这里就能点「重新评审」(只重写未达标章)。": "The task is running or queued (auto-resumes around {0}), so a re-review can't be started right now; come back to publish after this run finishes — if quality still falls short, “Re-review” will be clickable here (it rewrites only the unqualified chapters).",
|
|
753
|
+
"任务正在运行/排队中,暂不能重复发起评审;等这轮跑完再来点发布——若质量仍不达标,这里就能点「重新评审」(只重写未达标章)。": "The task is running or queued, so a re-review can't be started right now; come back to publish after this run finishes — if quality still falls short, “Re-review” will be clickable here (it rewrites only the unqualified chapters).",
|
|
754
|
+
"任务运行/排队中,暂不能发起;等这轮跑完再试": "Task is running or queued — wait for this run to finish, then try again",
|
|
752
755
|
"强制放行:请输入人工复核原因(必填)": "Force publish: enter the manual review reason (required)",
|
|
753
756
|
"未填写人工复核原因,已取消强制放行": "No manual review reason entered — force publish cancelled",
|
|
754
757
|
"开始建书": "Start book creation",
|
|
@@ -2082,11 +2085,6 @@
|
|
|
2082
2085
|
"章节已就绪,创建作品还差开书资料。点平台按钮一键生成(书名/简介/标签/主角名),生成后逐字段复制进建书表单。": "Chapters are ready — what's missing is the signup data. Click a platform button to generate (title/synopsis/tags/protagonists) in one go, then copy each field into the signup form.",
|
|
2083
2086
|
"章节已就绪,创建作品还差开书资料": "Chapters are ready — what's missing is the signup data",
|
|
2084
2087
|
"继承自首批": "Inherited from batch 1",
|
|
2085
|
-
"回首批任务": "Go to first-batch task",
|
|
2086
|
-
"开书资料在首批任务上生成与维护": "Book-signup data is generated and maintained on the first-batch task",
|
|
2087
|
-
"首批任务还没生成开书资料": "The first-batch task hasn't generated book-signup data yet",
|
|
2088
|
-
"开书资料属于这本书,在首批任务上生成后整条连载链都能看到。": "Book-signup data belongs to the book itself — generate it once on the first batch and the whole serial chain can see it.",
|
|
2089
|
-
"找不到首批任务(可能已删除)": "First-batch task not found (it may have been deleted)",
|
|
2090
2088
|
"点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。": "Click “Generate” on a platform card to produce the signup data (title/synopsis/tags/protagonists) in one go, then copy each field into the signup form.",
|
|
2091
2089
|
"任务运行中,生成将在本轮结束后可用": "Task is running — generation unlocks after this round finishes",
|
|
2092
2090
|
"已生成": "Generated",
|
|
@@ -2838,6 +2836,22 @@
|
|
|
2838
2836
|
"推荐": "Recommended",
|
|
2839
2837
|
"摘要列表": "Digest list",
|
|
2840
2838
|
"操作失败": "Operation failed",
|
|
2839
|
+
"立即重试": "Retry now",
|
|
2840
|
+
"不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写": "Skip the auto-resume backoff wait and continue right away; serial tasks resume from the breakpoint — finished chapters are never rewritten",
|
|
2841
|
+
"已跳过等待,马上继续": "Wait skipped — resuming now",
|
|
2842
|
+
"任务契约": "Task contract",
|
|
2843
|
+
"验收": "Acceptance",
|
|
2844
|
+
"审批": "Approval",
|
|
2845
|
+
"待审批": "Approval required",
|
|
2846
|
+
"已审批": "Approved",
|
|
2847
|
+
"无需审批": "No approval",
|
|
2848
|
+
"待验收": "Pending",
|
|
2849
|
+
"批准发布": "Approve release",
|
|
2850
|
+
"已批准发布": "Release approved",
|
|
2851
|
+
"标记通过": "Mark passed",
|
|
2852
|
+
"标记失败": "Mark failed",
|
|
2853
|
+
"证据说明(可空)": "Evidence note (optional)",
|
|
2854
|
+
"证据已记录": "Evidence recorded",
|
|
2841
2855
|
"新建任务": "New task",
|
|
2842
2856
|
"新建钩子": "New hook",
|
|
2843
2857
|
"正在删除任务…": "Deleting task…",
|
package/app/ui/index.html
CHANGED
|
@@ -481,6 +481,10 @@
|
|
|
481
481
|
<div class="field"><label data-i18n="任务标题">任务标题</label><input id="f-title" placeholder="可空,自动取目标首行" data-i18n-ph="可空,自动取目标首行"></div>
|
|
482
482
|
</div>
|
|
483
483
|
<div class="field"><label data-i18n="上下文">上下文</label><textarea id="f-context" rows="2" placeholder="背景信息、约束、相关文件(可空)" data-i18n-ph="背景信息、约束、相关文件(可空)"></textarea></div>
|
|
484
|
+
<div class="grid-2">
|
|
485
|
+
<div class="field"><label>验收标准(每行一条)</label><textarea id="f-acceptance" rows="3" placeholder="例如:测试全部通过 例如:产出文件存在"></textarea></div>
|
|
486
|
+
<div class="field"><label class="checkline"><input id="f-approval-required" type="checkbox"> 完成前需要人工审批</label><p class="hint">启用后,Git 合并和发布会等待审批记录。</p></div>
|
|
487
|
+
</div>
|
|
484
488
|
|
|
485
489
|
<div id="f-code-only">
|
|
486
490
|
<div class="field"><label data-i18n="验证命令">验证命令</label><input id="f-verify" placeholder="例:python -m pytest -q(可空=只评审)" data-i18n-ph="例:python -m pytest -q(可空=只评审)"></div>
|
|
@@ -544,6 +548,7 @@
|
|
|
544
548
|
<div class="rd-actions">
|
|
545
549
|
<button id="btn-pause" class="ghost hidden" data-i18n="暂停">暂停</button>
|
|
546
550
|
<button data-i18n="取消运行" id="btn-cancel" class="danger hidden">取消运行</button>
|
|
551
|
+
<button data-i18n="立即重试" id="btn-resume-now" class="primary hidden" data-i18n-title="不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写" title="不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写">立即重试</button>
|
|
547
552
|
<button data-i18n="↻ 继续任务" id="btn-retry" class="primary hidden" data-i18n-title="再跑一次这个任务;连载任务会断点续跑,已完成章节不重写" title="再跑一次这个任务;连载任务会断点续跑,已完成章节不重写">↻ 继续任务</button>
|
|
548
553
|
<button data-i18n="✎ 编辑重试" id="btn-editretry" class="ghost hidden" data-i18n-title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑" title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑">✎ 编辑重试</button>
|
|
549
554
|
<button data-i18n="继续连载" id="btn-continue" class="ghost hidden">继续连载</button>
|
package/app/ui/style.css
CHANGED
|
@@ -4574,6 +4574,16 @@ main:has(> #page-settings > #sub-runs:not(.hidden)) { padding-bottom: 28px; }
|
|
|
4574
4574
|
.rd-overview-last span { color: var(--text); }
|
|
4575
4575
|
.rd-overview-actions { display: flex; justify-content: flex-end; gap: 6px; grid-column: 1 / -1; }
|
|
4576
4576
|
.rd-overview-actions button { padding: 5px 9px; font-size: 11.5px; }
|
|
4577
|
+
.rd-contract { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 14px; grid-column: 1 / -1; padding-top: 8px; border-top: 1px solid var(--border); color: var(--muted); font-size: 11.5px; }
|
|
4578
|
+
.rd-contract b { color: var(--text); }
|
|
4579
|
+
.rd-contract .err { color: var(--bad); }
|
|
4580
|
+
.rd-contract-evidence { display: flex; flex-wrap: wrap; gap: 4px 8px; width: 100%; }
|
|
4581
|
+
.rd-contract-criterion { padding: 2px 6px; border-radius: 5px; background: color-mix(in srgb, var(--text) 7%, transparent); }
|
|
4582
|
+
.rd-contract-criterion.passed { color: var(--ok); }
|
|
4583
|
+
.rd-contract-criterion.failed { color: var(--bad); }
|
|
4584
|
+
.rd-contract-mark { margin-left: 4px; padding: 0 4px; border: 0; border-radius: 4px; color: var(--muted); background: transparent; cursor: pointer; }
|
|
4585
|
+
.rd-contract-mark.pass:hover { color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); }
|
|
4586
|
+
.rd-contract-mark.fail:hover { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
|
|
4577
4587
|
|
|
4578
4588
|
/* 直连详情把概览放进右上角信息面板;主区仍然只留对话。 */
|
|
4579
4589
|
#run-detail.chat-mode .rd-meta-popover #rd-overview:not(.hidden) { display: grid !important; }
|
package/package.json
CHANGED