codebee 0.1.73 → 0.1.76
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 +23 -0
- package/README.md +5 -4
- package/app/core/analytics.py +38 -0
- package/app/core/bookmeta.py +115 -14
- package/app/core/contracts.py +255 -0
- package/app/core/error_codes.py +9 -0
- package/app/core/health.py +16 -1
- package/app/core/jobs.py +72 -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 +210 -18
- 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 +101 -5
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +275 -0
- package/app/main.py +240 -20
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +237 -43
- package/app/ui/i18n.js +34 -5
- package/app/ui/index.html +9 -0
- package/app/ui/style.css +32 -6
- 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);
|
|
@@ -4439,6 +4458,14 @@ function renderSideTasks() {
|
|
|
4439
4458
|
vb.classList.toggle("hidden", !vcount);
|
|
4440
4459
|
vb.title = vcount ? vcount + t(" 个任务待裁决") : "";
|
|
4441
4460
|
}
|
|
4461
|
+
// 急停钮现身条件同源侧栏真相(全量组,不受搜索过滤):有排队/运行中任务
|
|
4462
|
+
// 才显示。点击走后端全量扫描,窗口外/侧栏外的活跃运行也一并取消。
|
|
4463
|
+
const acount = groups.filter((g) => g.status === "running" || g.status === "queued").length;
|
|
4464
|
+
const kb = $("btn-kill-all");
|
|
4465
|
+
if (kb) {
|
|
4466
|
+
kb.classList.toggle("hidden", !acount);
|
|
4467
|
+
kb.title = acount ? t("急停:取消全部进行中(排队/运行)任务") + "(" + acount + ")" : "";
|
|
4468
|
+
}
|
|
4442
4469
|
}
|
|
4443
4470
|
|
|
4444
4471
|
/* 主视图打开详情面板的公共部分:留在任务树,主栏切到运行/任务详情 */
|
|
@@ -4607,6 +4634,20 @@ function setRetryBtn(run, task) {
|
|
|
4607
4634
|
: t("再跑一次这个任务;连载任务会断点续跑,已完成章节不重写");
|
|
4608
4635
|
}
|
|
4609
4636
|
|
|
4637
|
+
/* 「立即重试」:退避窗口内的续跑副本(将于 HH:MM 自动续跑)可跳过等待直接
|
|
4638
|
+
* 入队。只认 queued + 未来时刻的 resume_enqueue_at;普通并发排队每 15s 有
|
|
4639
|
+
* 补跑心跳,用不上这个按钮。退避相位在两处渲染的签名里都有(resumePending /
|
|
4640
|
+
* etaBucket 分钟桶),到点后下一帧自然隐藏。 */
|
|
4641
|
+
function setResumeNowBtn(run) {
|
|
4642
|
+
const b = $("btn-resume-now");
|
|
4643
|
+
if (!b) return;
|
|
4644
|
+
const at = run && run.status === "queued"
|
|
4645
|
+
? Date.parse(String(run.resume_enqueue_at || "").replace(" ", "T")) : NaN;
|
|
4646
|
+
const inBackoff = !isNaN(at) && Date.now() < at;
|
|
4647
|
+
b.classList.toggle("hidden", !inBackoff);
|
|
4648
|
+
S.resumeNowRunId = inBackoff ? run.id : null;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4610
4651
|
/* 任务级详情:聚合该任务所有 run 的步骤。
|
|
4611
4652
|
* 有任务档案的从 /api/tasks/<id>/runs 拉全量——前端 run 窗口只有最近 40 条,
|
|
4612
4653
|
* 窗口外的历史会被算丢;无主运行组(管理操作/任务已删)仍用窗口数据。
|
|
@@ -4624,6 +4665,7 @@ function renderTaskDetail() {
|
|
|
4624
4665
|
setRetryBtn(retrySnapshotForTask(key, tk, latestSummary), tk);
|
|
4625
4666
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
4626
4667
|
syncArchBtn(tk, tk && (tk.status === "running" || tk.status === "queued"));
|
|
4668
|
+
setResumeNowBtn(null); // 退避与否要看 run 数据,先收起防上一个任务残留
|
|
4627
4669
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
4628
4670
|
if (isTask) {
|
|
4629
4671
|
loadTaskRuns(key, latestSummary);
|
|
@@ -4687,6 +4729,7 @@ function loadTaskRuns(key, latestSummary) {
|
|
|
4687
4729
|
offset: Number(data.offset) + runs.length, total: Number(data.total),
|
|
4688
4730
|
hasMore: !!data.has_more, totals: data.totals || null, loading: false,
|
|
4689
4731
|
});
|
|
4732
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4690
4733
|
_taskRunsCache.delete(key);
|
|
4691
4734
|
_taskRunsCache.set(key, runs);
|
|
4692
4735
|
while (_taskRunsCache.size > 3) {
|
|
@@ -4736,6 +4779,7 @@ async function loadEarlierTaskRuns(key) {
|
|
|
4736
4779
|
page.offset = Number(data.offset) + older.length;
|
|
4737
4780
|
page.total = Number(data.total) || page.total;
|
|
4738
4781
|
page.hasMore = !!data.has_more;
|
|
4782
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4739
4783
|
drawTaskDetail(key, _taskRunsCache.get(key), appendFrom);
|
|
4740
4784
|
} catch (e) {
|
|
4741
4785
|
toast(t("加载历史运行失败:") + e.message, true);
|
|
@@ -4830,6 +4874,7 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4830
4874
|
// S.detailRunId,cancelRun 靠 S.cancelTargetRunId 知道取消谁
|
|
4831
4875
|
$("btn-cancel").classList.toggle("hidden", !activeRun);
|
|
4832
4876
|
S.cancelTargetRunId = activeRun ? activeRun.id : null;
|
|
4877
|
+
setResumeNowBtn(activeRun && activeRun.status === "queued" ? activeRun : null);
|
|
4833
4878
|
const bp2 = $("btn-pause");
|
|
4834
4879
|
if (bp2 && activeRun) {
|
|
4835
4880
|
bp2.classList.toggle("hidden", false);
|
|
@@ -5024,6 +5069,26 @@ async function clearRuns() {
|
|
|
5024
5069
|
poll();
|
|
5025
5070
|
}
|
|
5026
5071
|
|
|
5072
|
+
/* 急停(kill switch):取消全部排队/运行中任务。危险操作——确认框点名
|
|
5073
|
+
* 数量;后端全量扫,侧栏窗口外的活跃运行也一并取消。防双击:busy 期间
|
|
5074
|
+
* 直接吞掉重复点击。 */
|
|
5075
|
+
async function cancelAllRuns() {
|
|
5076
|
+
const btn = $("btn-kill-all");
|
|
5077
|
+
if (!btn || btn.classList.contains("busy")) return;
|
|
5078
|
+
if (!await uiConfirm(t("确认急停:取消全部进行中(排队/运行)任务?"), { ok: t("急停"), danger: true })) return;
|
|
5079
|
+
btn.classList.add("busy");
|
|
5080
|
+
try {
|
|
5081
|
+
const d = await api("/api/runs/cancel_all", { method: "POST", timeout: 30000 });
|
|
5082
|
+
toast(t("已取消 ") + ((d && d.count) || 0) + t(" 个进行中任务"));
|
|
5083
|
+
} catch (e) {
|
|
5084
|
+
toast(t("急停失败:") + e.message, true);
|
|
5085
|
+
} finally {
|
|
5086
|
+
btn.classList.remove("busy");
|
|
5087
|
+
}
|
|
5088
|
+
poll();
|
|
5089
|
+
}
|
|
5090
|
+
window.cancelAllRuns = cancelAllRuns;
|
|
5091
|
+
|
|
5027
5092
|
async function openRun(id, pinTab) {
|
|
5028
5093
|
_taskStepRenderToken++;
|
|
5029
5094
|
S.detailRunId = id;
|
|
@@ -5387,6 +5452,7 @@ async function renderRunDetail() {
|
|
|
5387
5452
|
const active = run.status === "queued" || run.status === "running";
|
|
5388
5453
|
$("btn-cancel").classList.toggle("hidden", !active);
|
|
5389
5454
|
S.cancelTargetRunId = active ? run.id : null;
|
|
5455
|
+
setResumeNowBtn(run.status === "queued" ? run : null); // 在 sig 守卫前:退避相位由 etaBucket 分钟桶兜底刷新
|
|
5390
5456
|
$("btn-delete").classList.toggle("hidden", active);
|
|
5391
5457
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5392
5458
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
@@ -5611,12 +5677,6 @@ function bmRootTask(task) {
|
|
|
5611
5677
|
return cur || task;
|
|
5612
5678
|
}
|
|
5613
5679
|
|
|
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
5680
|
function renderBookMetaPanel(task) {
|
|
5621
5681
|
const box = $("rd-bookmeta");
|
|
5622
5682
|
if (!box || !(task || {}).id || !bmNeedsPanel(task)) {
|
|
@@ -5627,10 +5687,14 @@ function renderBookMetaPanel(task) {
|
|
|
5627
5687
|
//(正在填登记表单)或章节清单正展开时跳过本帧整块重绘——innerHTML
|
|
5628
5688
|
// 重建会把没提交的字清掉、把展开列表折回去。轮询链继续转,blur/收起
|
|
5629
5689
|
// 后下一轮数据变化自然跟上;登记草稿另有 S.pbRegDraft 兜底回填。
|
|
5690
|
+
// 守卫只保护同一任务(box.dataset.taskId):切到别的任务的详情时,
|
|
5691
|
+
// 上一任务的展开态没有保留价值,放行重绘——否则新任务详情的作品信息
|
|
5692
|
+
// 页签一直挂着上一本书的建书/发布数据(2026-09-30 用户实案)。
|
|
5630
5693
|
const ae = document.activeElement;
|
|
5631
|
-
if (
|
|
5694
|
+
if (box.dataset.taskId === task.id &&
|
|
5695
|
+
((ae && box.contains(ae) &&
|
|
5632
5696
|
(ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) ||
|
|
5633
|
-
|
|
5697
|
+
box.querySelector(".pb-chapters:not(.hidden)"))) {
|
|
5634
5698
|
pbSyncState(task);
|
|
5635
5699
|
return;
|
|
5636
5700
|
}
|
|
@@ -5646,21 +5710,12 @@ function renderBookMetaPanel(task) {
|
|
|
5646
5710
|
'<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
5711
|
'<span class="flex1"></span>';
|
|
5648
5712
|
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
5713
|
html += "</div>";
|
|
5654
5714
|
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
|
-
}
|
|
5715
|
+
// 生成入口不再区分首批/续写(2026-09-30 放开;后端落点统一在链上根任务),指引文案只留一份
|
|
5716
|
+
html += '<div class="bm-guide"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5717
|
+
'<div><b>' + esc(t("章节已就绪,创建作品还差开书资料")) + "</b>" +
|
|
5718
|
+
"<p>" + esc(t("点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。")) + "</p></div></div>";
|
|
5664
5719
|
}
|
|
5665
5720
|
html += '<div class="bm-cards">' + BOOKMETA_PLATFORMS.map((p) => {
|
|
5666
5721
|
const entry = bm[p.id] || {};
|
|
@@ -5679,10 +5734,26 @@ function renderBookMetaPanel(task) {
|
|
|
5679
5734
|
const v = tags
|
|
5680
5735
|
? '<span class="bm-chips">' + raw.map((x) => '<span class="bm-chip">' + esc(String(x)) + "</span>").join("") + "</span>"
|
|
5681
5736
|
: '<span class="bm-f-v' + (long ? " bm-f--scroll" : "") + '" title="' + esc(val) + '">' + esc(val) + "</span>";
|
|
5737
|
+
// 书名行加「换名」(2026-10-02 撞名实案):只换书名一个字段,其余
|
|
5738
|
+
// 已核对过的分类/标签/简介不被重摇覆盖;换名中禁用态由 S.bmRenaming
|
|
5739
|
+
// 跨重绘保持(轮询整块 innerHTML 重建会把按钮态复位)
|
|
5740
|
+
let rename = "";
|
|
5741
|
+
if (key === "book_name") {
|
|
5742
|
+
const renaming = S.bmRenaming === task.id + "|" + p.id;
|
|
5743
|
+
rename = '<button class="bm-rename" data-task="' + esc(task.id) +
|
|
5744
|
+
'" data-plat="' + p.id + '" data-old="' + esc(val) + '"' + (renaming ? " disabled" : "") +
|
|
5745
|
+
' title="' + esc(t("书名与本站已有作品重名时,点这里换一个新书名(其余字段不动)")) + '"' +
|
|
5746
|
+
' onclick="bmRename(this)">' +
|
|
5747
|
+
(renaming
|
|
5748
|
+
? '<svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名中…"))
|
|
5749
|
+
: '<svg class="ico" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名"))) +
|
|
5750
|
+
"</button>";
|
|
5751
|
+
}
|
|
5682
5752
|
return '<div class="bm-f' + (long || tags ? " bm-f--full" : "") + '">' +
|
|
5683
5753
|
'<span class="bm-f-k">' + esc(t(label)) + "</span>" + v +
|
|
5684
5754
|
'<button class="bm-copy" data-text="' + esc(val) + '" data-label="' + esc(t(label)) +
|
|
5685
|
-
'" title="' + esc(t("点击复制")) + '" onclick="bmCopyBtn(this)"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg>' + t("复制") + "</button
|
|
5755
|
+
'" title="' + esc(t("点击复制")) + '" onclick="bmCopyBtn(this)"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg>' + t("复制") + "</button>" +
|
|
5756
|
+
rename + "</div>";
|
|
5686
5757
|
}).join("") + "</div>";
|
|
5687
5758
|
} else if (st === "failed") {
|
|
5688
5759
|
body = '<div class="bm-errhint">' + esc(entry.error || t("生成失败")) + "</div>";
|
|
@@ -5690,9 +5761,7 @@ function renderBookMetaPanel(task) {
|
|
|
5690
5761
|
body = '<div class="bm-empty">' + esc(t("尚未生成")) + "</div>";
|
|
5691
5762
|
}
|
|
5692
5763
|
let action;
|
|
5693
|
-
if (
|
|
5694
|
-
action = ""; // 续写批次只读:生成/重生成集中在头部「回首批任务」入口
|
|
5695
|
-
} else if (st === "running") {
|
|
5764
|
+
if (st === "running") {
|
|
5696
5765
|
action = '<button class="ghost" disabled><svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + t("生成中…") + "</button>";
|
|
5697
5766
|
} else if (st === "done") {
|
|
5698
5767
|
action = '<button class="ghost" onclick="bmGen(\'' + esc(task.id) + "', '" + p.id + '\')" title="' + esc(t("重新生成会覆盖现有内容")) + '">' +
|
|
@@ -5748,6 +5817,7 @@ function renderBookMetaPanel(task) {
|
|
|
5748
5817
|
bmStatusChip(cgSt) + '<span class="flex1"></span>' + cgAction + "</div>" + cgBody + "</div></div>";
|
|
5749
5818
|
box.classList.remove("hidden");
|
|
5750
5819
|
box.innerHTML = html;
|
|
5820
|
+
box.dataset.taskId = task.id; // 跨任务守卫标记:展开态/输入保护只对同一任务生效
|
|
5751
5821
|
// TAB 徽章:已生成平台数(生成中显示 ●,随下次轮询刷新)
|
|
5752
5822
|
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="bookmeta"] .rd-badge');
|
|
5753
5823
|
if (badge) {
|
|
@@ -5768,6 +5838,34 @@ window.bmGen = async function (taskId, platform) {
|
|
|
5768
5838
|
} catch (e) { toast(t("生成失败:") + e.message, true); }
|
|
5769
5839
|
};
|
|
5770
5840
|
|
|
5841
|
+
/* 换名(书名撞名专用):同步接口单次轻模型调用,成功后新书名随 SSE/轮询
|
|
5842
|
+
* 回填;失败 toast 报错,现有字段原样保留。期间 S.bmRenaming 让按钮禁用态
|
|
5843
|
+
* 跨轮询重绘保持,防连点打两个换名请求。 */
|
|
5844
|
+
window.bmRename = async function (btn) {
|
|
5845
|
+
if (btn.disabled) return;
|
|
5846
|
+
// 立即禁用+换文案(防 await 期间双击发第二个请求);S.bmRenaming 让禁用态
|
|
5847
|
+
// 跨轮询重绘保持(轮询整块 innerHTML 重建会把按钮态复位,渲染端按它还原)
|
|
5848
|
+
btn.disabled = true;
|
|
5849
|
+
btn.innerHTML = '<svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名中…"));
|
|
5850
|
+
const taskId = btn.dataset.task, platform = btn.dataset.plat;
|
|
5851
|
+
S.bmRenaming = taskId + "|" + platform;
|
|
5852
|
+
try {
|
|
5853
|
+
const r = await api("/api/tasks/" + encodeURIComponent(taskId) + "/book-meta/rename",
|
|
5854
|
+
{ method: "POST", body: JSON.stringify({ platform }) });
|
|
5855
|
+
toast(t("已换名:") + "《" + (r.old || btn.dataset.old || "") + "》→《" + (r.new || "") + "》");
|
|
5856
|
+
} catch (e) {
|
|
5857
|
+
toast(t("换名失败:") + e.message, true);
|
|
5858
|
+
} finally {
|
|
5859
|
+
S.bmRenaming = "";
|
|
5860
|
+
// 就地恢复按钮:render 的详情签名守卫在服务端数据没变时(换名失败正是)
|
|
5861
|
+
// 会跳过重绘,按钮不能指望 render 归位;若 render 真重建了整卡,btn 已
|
|
5862
|
+
// detach,改它是无害空操作
|
|
5863
|
+
btn.disabled = false;
|
|
5864
|
+
btn.innerHTML = '<svg class="ico" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名"));
|
|
5865
|
+
try { await refreshState(); render(); } catch (e) { /* 拉状态失败不影响上面的恢复 */ }
|
|
5866
|
+
}
|
|
5867
|
+
};
|
|
5868
|
+
|
|
5771
5869
|
/* 封面图生成(covergen):curl 子进程直接落盘运行目录,Python 不经手图像字节 */
|
|
5772
5870
|
window.coverGen = async function (taskId) {
|
|
5773
5871
|
try {
|
|
@@ -7408,8 +7506,10 @@ function artifactsChips(runId, files, prevRun, hasPlan) {
|
|
|
7408
7506
|
(isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
|
|
7409
7507
|
'" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
7410
7508
|
'<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
|
|
7411
|
-
(diffable ? '<a class="file-chip prev artifact-file-diff" title="' +
|
|
7509
|
+
(diffable ? '<a class="file-chip prev artifact-file-diff" title="' +
|
|
7510
|
+
esc(prevRun.ver ? t("与第 {0} 版对比").replace("{0}", prevRun.ver) : t("与上一版对比")) +
|
|
7412
7511
|
'" data-file-run="' + esc(runId) + '" data-prev-run="' + esc(prevRun.id) +
|
|
7512
|
+
'" data-prev-ver="' + (prevRun.ver || "") +
|
|
7413
7513
|
'" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
7414
7514
|
'<svg class="ico" aria-hidden="true"><use href="#i-git-branch"/></svg>' + t("对比") + "</a>" : "") +
|
|
7415
7515
|
"</span>";
|
|
@@ -7446,12 +7546,17 @@ function artifactDiffBody(a, b) {
|
|
|
7446
7546
|
return '<pre class="art-diff">' + html + "</pre>";
|
|
7447
7547
|
}
|
|
7448
7548
|
|
|
7449
|
-
/* 对比上一版弹窗:取两版文本 → diff 渲染(复用成品弹窗骨架)。
|
|
7450
|
-
|
|
7549
|
+
/* 对比上一版弹窗:取两版文本 → diff 渲染(复用成品弹窗骨架)。
|
|
7550
|
+
* prevVer 在场时副标用「第 N 版 → 当前」(序号比 run_id 前 8 位对用户可读);
|
|
7551
|
+
* 缺号退回原 run_id 摘要,审计语义不丢。 */
|
|
7552
|
+
window.artDiffPopup = async function (runId, prevRunId, name, prevVer) {
|
|
7451
7553
|
_fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; _fpCopyText = null;
|
|
7452
7554
|
const nm = String(name).split("/").pop();
|
|
7453
|
-
|
|
7454
|
-
'<i>' + esc(
|
|
7555
|
+
const sub = prevVer
|
|
7556
|
+
? '<i>' + esc(t("第 {0} 版 → 当前").replace("{0}", prevVer)) +
|
|
7557
|
+
" · " + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>"
|
|
7558
|
+
: '<i>' + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>";
|
|
7559
|
+
_fpOpen(nm + " · " + t("与上一版对比"), sub, "");
|
|
7455
7560
|
_fpSetBody('<div class="fp-hint">' + esc(t("正在取两版内容…")) + "</div>");
|
|
7456
7561
|
const u1 = urlAuth("/api/runs/" + encodeURIComponent(prevRunId) + "/file?name=" + encodeURIComponent(name));
|
|
7457
7562
|
const u2 = urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" + encodeURIComponent(name));
|
|
@@ -7499,20 +7604,35 @@ async function loadArtifacts(runId) {
|
|
|
7499
7604
|
return;
|
|
7500
7605
|
}
|
|
7501
7606
|
const head = '<div class="files-head"><span class="sec-title">' + t("成品文件") + '</span>' +
|
|
7607
|
+
verBadge +
|
|
7502
7608
|
'<span class="wd" title="' + esc(t("点击复制")) + '" onclick="copyText(this.textContent)">' + esc(d.workdir) + "</span></div>";
|
|
7503
7609
|
// 上一版 run 的文件清单(详情上下文才有):同名文本文件给「对比」入口;
|
|
7504
|
-
//
|
|
7505
|
-
|
|
7610
|
+
// 拉不到/无历史静默降级——对比是增强不是闸门。
|
|
7611
|
+
// 版本序号(OpenCreator「版本化」剩余面):任务 run 列表最新在前,序号 =
|
|
7612
|
+
// run 总数 - 列表内序位;total 缺失(分页未拉过)就不编版本号,徽章/对比
|
|
7613
|
+
// 标题退回原样——序号是可读性增强不是闸门
|
|
7614
|
+
let prevRun = null, curVer = 0;
|
|
7506
7615
|
if (detailOwns && S.detailTaskKey) {
|
|
7507
|
-
const
|
|
7616
|
+
const runs = _taskRunsCache.get(S.detailTaskKey) || [];
|
|
7617
|
+
const total = ((_taskRunsPaging.get(S.detailTaskKey) || {}).total) || 0;
|
|
7618
|
+
const idx = runs.findIndex((r) => r.id === runId);
|
|
7619
|
+
if (total && idx >= 0) curVer = total - idx;
|
|
7620
|
+
const prev = runs.find((r) => r.id !== runId);
|
|
7508
7621
|
if (prev) {
|
|
7509
7622
|
try {
|
|
7510
7623
|
const pd = await api("/api/runs/" + encodeURIComponent(prev.id) + "/files");
|
|
7511
|
-
if (pd && (pd.files || []).length)
|
|
7512
|
-
|
|
7624
|
+
if (pd && (pd.files || []).length) {
|
|
7625
|
+
const pIdx = runs.indexOf(prev);
|
|
7626
|
+
prevRun = { id: prev.id, names: new Set(pd.files.map((f2) => f2.name)),
|
|
7627
|
+
ver: (total && pIdx >= 0) ? total - pIdx : 0 };
|
|
7628
|
+
}
|
|
7513
7629
|
} catch (e) { /* 上一版清单不可用就不给对比 */ }
|
|
7514
7630
|
}
|
|
7515
7631
|
}
|
|
7632
|
+
const verBadge = curVer > 1
|
|
7633
|
+
? '<span class="ver-badge" title="' + esc(t("该任务第 N 次运行的产出,序号含修订与重试")) + '">' +
|
|
7634
|
+
esc(t("第 {0} 版").replace("{0}", curVer)) + "</span>"
|
|
7635
|
+
: "";
|
|
7516
7636
|
const chips = artifactsChips(runId, files, prevRun, d.has_plan);
|
|
7517
7637
|
if (box) box.innerHTML = head + '<div class="file-chips">' + chips + "</div>";
|
|
7518
7638
|
if (mainBox) { mainBox.classList.remove("hidden");
|
|
@@ -8261,6 +8381,23 @@ async function cancelRun() {
|
|
|
8261
8381
|
toast(r && r.ok ? t("已强制终止运行") : t("该运行已结束,无需取消"));
|
|
8262
8382
|
}
|
|
8263
8383
|
|
|
8384
|
+
/* 立即重试:跳过自动续跑的退避等待,马上把续跑副本入队。任务级详情没有
|
|
8385
|
+
* S.detailRunId,目标用渲染时钉好的 resumeNowRunId(与 cancel 同款约定)。 */
|
|
8386
|
+
async function resumeNow() {
|
|
8387
|
+
const rid = S.detailRunId || S.resumeNowRunId;
|
|
8388
|
+
if (!rid) return;
|
|
8389
|
+
let r;
|
|
8390
|
+
try {
|
|
8391
|
+
r = await api("/api/runs/" + encodeURIComponent(rid) + "/resume_now", { method: "POST" });
|
|
8392
|
+
} catch (e) { toast(t("操作失败:") + (e && e.message ? e.message : t("网络异常")), true); return; }
|
|
8393
|
+
if (r && r.ok) {
|
|
8394
|
+
toast(t("已跳过等待,马上继续"));
|
|
8395
|
+
refreshState(); // 后端 bump 也会推 SSE,这里主动刷一轮让按钮立刻消失
|
|
8396
|
+
} else {
|
|
8397
|
+
toast((r && r.error) || t("操作失败"), true);
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8264
8401
|
/* ---------------------------------------------------- 运行中指挥(消息信箱) */
|
|
8265
8402
|
/* 用户在详情页往运行中的任务「递话」:文字 + 附件(截图/文件)。
|
|
8266
8403
|
* 无头 CLI 插不进正在跑的进程,指令在下一次步骤下达前由后端 drain 注入。
|
|
@@ -8965,6 +9102,7 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8965
9102
|
const duration = runDurationSeconds(run);
|
|
8966
9103
|
const etaText = runEtaText(run);
|
|
8967
9104
|
const goal = (task && task.goal) || run.goal || run.title || "";
|
|
9105
|
+
const contract = (task && task.contract) || S.taskContracts[(task && task.id) || run.task_id] || null;
|
|
8968
9106
|
const actions = [];
|
|
8969
9107
|
if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
|
|
8970
9108
|
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 +9125,63 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8987
9125
|
(etaText ? '<span class="rd-eta">' + esc(etaText) + '</span>' : "") +
|
|
8988
9126
|
'</div>' +
|
|
8989
9127
|
(lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
|
|
9128
|
+
(contract ? contractOverviewHtml(contract) : "") +
|
|
8990
9129
|
(actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
|
|
8991
9130
|
box.classList.remove("hidden");
|
|
8992
9131
|
}
|
|
8993
9132
|
|
|
9133
|
+
function contractOverviewHtml(contract) {
|
|
9134
|
+
const criteria = Array.isArray(contract.acceptance_criteria) ? contract.acceptance_criteria : [];
|
|
9135
|
+
const evidence = Array.isArray(contract.evidence) ? contract.evidence : [];
|
|
9136
|
+
const latest = {};
|
|
9137
|
+
evidence.forEach((e) => { if (e && e.criterion) latest[e.criterion] = e; });
|
|
9138
|
+
const passed = criteria.filter((c) => latest[c] && latest[c].status === "passed").length;
|
|
9139
|
+
const approval = contract.approval_required
|
|
9140
|
+
? (contract.approval && contract.approval.status === "approved" ? t("已审批") : t("待审批")) : t("无需审批");
|
|
9141
|
+
const taskId = String(contract.task_id || "");
|
|
9142
|
+
const approvalAction = contract.approval_required && (!contract.approval || contract.approval.status !== "approved")
|
|
9143
|
+
? '<button type="button" class="ghost small" onclick="contractAction(\'' + jsq(taskId) + '\',\'approve\')">' + esc(t("批准发布")) + '</button>' : "";
|
|
9144
|
+
const evidenceText = criteria.map((c) => {
|
|
9145
|
+
const e = latest[c];
|
|
9146
|
+
const controls = '<button type="button" class="rd-contract-mark pass" title="' + esc(t("标记通过")) + '" onclick="contractEvidenceAction(\'' + jsq(taskId) + '\',\'' + jsq(c) + '\',\'passed\')">✓</button>' +
|
|
9147
|
+
'<button type="button" class="rd-contract-mark fail" title="' + esc(t("标记失败")) + '" onclick="contractEvidenceAction(\'' + jsq(taskId) + '\',\'' + jsq(c) + '\',\'failed\')">×</button>';
|
|
9148
|
+
return '<span class="rd-contract-criterion ' + (e ? esc(e.status) : "pending") + '">' + esc(c) + ': ' + esc(e ? e.status : t("待验收")) + controls + '</span>';
|
|
9149
|
+
}).join("");
|
|
9150
|
+
return '<div class="rd-contract"><b>' + esc(t("任务契约")) + '</b>' +
|
|
9151
|
+
'<span>' + esc(t("验收")) + ' ' + passed + '/' + criteria.length + '</span>' +
|
|
9152
|
+
'<span>' + esc(t("审批")) + ' ' + esc(approval) + '</span>' +
|
|
9153
|
+
(evidenceText ? '<div class="rd-contract-evidence">' + evidenceText + '</div>' : "") + approvalAction +
|
|
9154
|
+
(contract.blocked_reason ? '<span class="err">' + esc(t("Blocked:")) + esc(contract.blocked_reason) + '</span>' : "") +
|
|
9155
|
+
'</div>';
|
|
9156
|
+
}
|
|
9157
|
+
|
|
9158
|
+
async function contractAction(taskId, op) {
|
|
9159
|
+
try {
|
|
9160
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/contract", {
|
|
9161
|
+
method: "POST", body: JSON.stringify({ op: op, actor: "ui" })
|
|
9162
|
+
});
|
|
9163
|
+
if (data && data.contract) S.taskContracts[taskId] = data.contract;
|
|
9164
|
+
toast(op === "approve" ? t("已批准发布") : t("契约已更新"));
|
|
9165
|
+
refreshState();
|
|
9166
|
+
} catch (e) { toast((e && e.message) || t("操作失败"), true); }
|
|
9167
|
+
}
|
|
9168
|
+
window.contractAction = contractAction;
|
|
9169
|
+
|
|
9170
|
+
async function contractEvidenceAction(taskId, criterion, status) {
|
|
9171
|
+
const summary = await uiPrompt(t("证据说明(可空)"), "");
|
|
9172
|
+
if (summary === null) return;
|
|
9173
|
+
try {
|
|
9174
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/contract", {
|
|
9175
|
+
method: "POST", body: JSON.stringify({ op: "evidence", actor: "ui",
|
|
9176
|
+
evidence: { criterion: criterion, status: status, summary: summary, source: "manual" } })
|
|
9177
|
+
});
|
|
9178
|
+
if (data && data.contract) S.taskContracts[taskId] = data.contract;
|
|
9179
|
+
toast(t("证据已记录"));
|
|
9180
|
+
refreshState();
|
|
9181
|
+
} catch (e) { toast((e && e.message) || t("操作失败"), true); }
|
|
9182
|
+
}
|
|
9183
|
+
window.contractEvidenceAction = contractEvidenceAction;
|
|
9184
|
+
|
|
8994
9185
|
/* 对话条三件套:与任务参数双向同步(2026-09-21 用户反馈「这里面也需要同步改」)。
|
|
8995
9186
|
* 回填自当前任务;改动即写任务(空闲态),发送 /chat 前再兜底同步一次。 */
|
|
8996
9187
|
let rdPrefsTaskId = "";
|
|
@@ -15161,6 +15352,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15161
15352
|
b.addEventListener("click", () => setUsageHeatMode(b.dataset.heat)));
|
|
15162
15353
|
$("btn-back").addEventListener("click", closeRun);
|
|
15163
15354
|
$("btn-cancel").addEventListener("click", cancelRun);
|
|
15355
|
+
$("btn-kill-all").addEventListener("click", cancelAllRuns);
|
|
15164
15356
|
bindDirector();
|
|
15165
15357
|
bindChat();
|
|
15166
15358
|
const bindArtifactClicks = (root) => {
|
|
@@ -15169,7 +15361,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15169
15361
|
const diff = e.target.closest(".artifact-file-diff[data-prev-run]");
|
|
15170
15362
|
if (diff) { // 对比上一版:先于通用预览分支(chip 同样带 data-file-run/name)
|
|
15171
15363
|
e.preventDefault();
|
|
15172
|
-
artDiffPopup(diff.dataset.fileRun, diff.dataset.prevRun, diff.dataset.fileName || ""
|
|
15364
|
+
artDiffPopup(diff.dataset.fileRun, diff.dataset.prevRun, diff.dataset.fileName || "",
|
|
15365
|
+
Number(diff.dataset.prevVer) || 0);
|
|
15173
15366
|
return;
|
|
15174
15367
|
}
|
|
15175
15368
|
const file = e.target.closest("[data-file-run][data-file-name]");
|
|
@@ -15270,6 +15463,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15270
15463
|
? S.detailTaskKey : "");
|
|
15271
15464
|
if (taskId) retryTask(taskId);
|
|
15272
15465
|
});
|
|
15466
|
+
$("btn-resume-now").addEventListener("click", resumeNow);
|
|
15273
15467
|
/* 日志控制台高度:顶边手柄拖拽调整(localStorage 记忆),双击复位为默认弹性高度 */
|
|
15274
15468
|
(() => {
|
|
15275
15469
|
const grip = $("rd-log-grip"), box = $("rd-log");
|
package/app/ui/i18n.js
CHANGED
|
@@ -730,6 +730,12 @@
|
|
|
730
730
|
"删除这条教训?": "Delete this lesson?",
|
|
731
731
|
"删除全部已结束的运行记录(运行中的会保留)": "Delete all finished runs (active ones are kept)",
|
|
732
732
|
"清除全部运行记录(含日志与报告)?运行中的记录会保留,需先取消。不可恢复。": "Clear all runs (logs and reports)? Active runs are kept — cancel them first. This cannot be undone.",
|
|
733
|
+
"急停": "Kill all",
|
|
734
|
+
"急停:取消全部进行中(排队/运行)任务": "Kill switch: cancel all in-flight (queued/running) tasks",
|
|
735
|
+
"确认急停:取消全部进行中(排队/运行)任务?": "Kill all in-flight (queued/running) tasks?",
|
|
736
|
+
"已取消 ": "Cancelled ",
|
|
737
|
+
" 个进行中任务": " in-flight task(s)",
|
|
738
|
+
"急停失败:": "Kill switch failed: ",
|
|
733
739
|
"全选": "Select all",
|
|
734
740
|
"全不选": "Select none",
|
|
735
741
|
"取消选择": "Clear selection",
|
|
@@ -749,6 +755,9 @@
|
|
|
749
755
|
"「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。": "“Re-review” rewrites only the unqualified chapters and reviews them again (qualified chapters keep their drafts and scores). Publish again once it finishes.",
|
|
750
756
|
"已发起重新评审(重写未达标章)——评审完成后再来发布": "Re-review started (rewriting unqualified chapters) — publish again after it finishes",
|
|
751
757
|
"发起重新评审失败:": "Failed to start re-review: ",
|
|
758
|
+
"任务正在运行/排队中,预计 {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).",
|
|
759
|
+
"任务正在运行/排队中,暂不能重复发起评审;等这轮跑完再来点发布——若质量仍不达标,这里就能点「重新评审」(只重写未达标章)。": "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).",
|
|
760
|
+
"任务运行/排队中,暂不能发起;等这轮跑完再试": "Task is running or queued — wait for this run to finish, then try again",
|
|
752
761
|
"强制放行:请输入人工复核原因(必填)": "Force publish: enter the manual review reason (required)",
|
|
753
762
|
"未填写人工复核原因,已取消强制放行": "No manual review reason entered — force publish cancelled",
|
|
754
763
|
"开始建书": "Start book creation",
|
|
@@ -1926,6 +1935,10 @@
|
|
|
1926
1935
|
"读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)": "Reads the attached defect export → AI produces a Product/Dev/QA three-lens retrospective: overall profile, root-cause clusters, missed-test analysis, improvement actions (fast lane, report straight out)",
|
|
1927
1936
|
"复盘哪个版本/时间段的缺陷(建议附带禅道或 Jira 导出 CSV)": "Which version/time window to retrospect (attaching a ZenTao or Jira CSV export is recommended)",
|
|
1928
1937
|
"与上一版对比": "Diff vs previous run",
|
|
1938
|
+
"与第 {0} 版对比": "Diff vs version {0}",
|
|
1939
|
+
"第 {0} 版": "Version {0}",
|
|
1940
|
+
"第 {0} 版 → 当前": "Version {0} → current",
|
|
1941
|
+
"该任务第 N 次运行的产出,序号含修订与重试": "Output of this task's Nth run; the number counts revisions and retries too",
|
|
1929
1942
|
"对比": "Diff",
|
|
1930
1943
|
"两版内容一致。": "The two versions are identical.",
|
|
1931
1944
|
"正在取两版内容…": "Fetching both versions…",
|
|
@@ -2082,11 +2095,6 @@
|
|
|
2082
2095
|
"章节已就绪,创建作品还差开书资料。点平台按钮一键生成(书名/简介/标签/主角名),生成后逐字段复制进建书表单。": "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
2096
|
"章节已就绪,创建作品还差开书资料": "Chapters are ready — what's missing is the signup data",
|
|
2084
2097
|
"继承自首批": "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
2098
|
"点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。": "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
2099
|
"任务运行中,生成将在本轮结束后可用": "Task is running — generation unlocks after this round finishes",
|
|
2092
2100
|
"已生成": "Generated",
|
|
@@ -2096,6 +2104,11 @@
|
|
|
2096
2104
|
"生成": "Generate",
|
|
2097
2105
|
"重新生成": "Regenerate",
|
|
2098
2106
|
"重新生成会覆盖现有内容": "Regenerating overwrites the existing content",
|
|
2107
|
+
"换名": "Rename",
|
|
2108
|
+
"换名中…": "Renaming…",
|
|
2109
|
+
"已换名:": "Renamed: ",
|
|
2110
|
+
"换名失败:": "Rename failed: ",
|
|
2111
|
+
"书名与本站已有作品重名时,点这里换一个新书名(其余字段不动)": "If the title clashes with an existing work here, click to generate a new one (other fields stay untouched)",
|
|
2099
2112
|
"生成失败": "Generation failed",
|
|
2100
2113
|
"分享页": "Share page",
|
|
2101
2114
|
"生成自包含分享页(单文件 HTML),可直接发给他人查看任务成果": "Generate a self-contained share page (single-file HTML) to send to others",
|
|
@@ -2838,6 +2851,22 @@
|
|
|
2838
2851
|
"推荐": "Recommended",
|
|
2839
2852
|
"摘要列表": "Digest list",
|
|
2840
2853
|
"操作失败": "Operation failed",
|
|
2854
|
+
"立即重试": "Retry now",
|
|
2855
|
+
"不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写": "Skip the auto-resume backoff wait and continue right away; serial tasks resume from the breakpoint — finished chapters are never rewritten",
|
|
2856
|
+
"已跳过等待,马上继续": "Wait skipped — resuming now",
|
|
2857
|
+
"任务契约": "Task contract",
|
|
2858
|
+
"验收": "Acceptance",
|
|
2859
|
+
"审批": "Approval",
|
|
2860
|
+
"待审批": "Approval required",
|
|
2861
|
+
"已审批": "Approved",
|
|
2862
|
+
"无需审批": "No approval",
|
|
2863
|
+
"待验收": "Pending",
|
|
2864
|
+
"批准发布": "Approve release",
|
|
2865
|
+
"已批准发布": "Release approved",
|
|
2866
|
+
"标记通过": "Mark passed",
|
|
2867
|
+
"标记失败": "Mark failed",
|
|
2868
|
+
"证据说明(可空)": "Evidence note (optional)",
|
|
2869
|
+
"证据已记录": "Evidence recorded",
|
|
2841
2870
|
"新建任务": "New task",
|
|
2842
2871
|
"新建钩子": "New hook",
|
|
2843
2872
|
"正在删除任务…": "Deleting task…",
|
package/app/ui/index.html
CHANGED
|
@@ -206,6 +206,10 @@
|
|
|
206
206
|
</span>
|
|
207
207
|
</button>
|
|
208
208
|
<div class="side-foot">
|
|
209
|
+
<!-- 急停(kill switch):有进行中任务才现身;点击取消全部排队/运行中任务 -->
|
|
210
|
+
<button id="btn-kill-all" class="foot-btn kill hidden" title="急停:取消全部进行中(排队/运行)任务" aria-label="急停" data-i18n-aria="急停" data-i18n-title="急停:取消全部进行中(排队/运行)任务">
|
|
211
|
+
<svg class="ico ico-lg" aria-hidden="true"><use href="#i-x"></use></svg>
|
|
212
|
+
</button>
|
|
209
213
|
<button id="btn-settings" class="foot-btn" title="设置" aria-label="设置" data-i18n-aria="设置" data-i18n-title="设置">
|
|
210
214
|
<svg class="ico ico-lg" aria-hidden="true"><use href="#i-gear"></use></svg>
|
|
211
215
|
</button>
|
|
@@ -481,6 +485,10 @@
|
|
|
481
485
|
<div class="field"><label data-i18n="任务标题">任务标题</label><input id="f-title" placeholder="可空,自动取目标首行" data-i18n-ph="可空,自动取目标首行"></div>
|
|
482
486
|
</div>
|
|
483
487
|
<div class="field"><label data-i18n="上下文">上下文</label><textarea id="f-context" rows="2" placeholder="背景信息、约束、相关文件(可空)" data-i18n-ph="背景信息、约束、相关文件(可空)"></textarea></div>
|
|
488
|
+
<div class="grid-2">
|
|
489
|
+
<div class="field"><label>验收标准(每行一条)</label><textarea id="f-acceptance" rows="3" placeholder="例如:测试全部通过 例如:产出文件存在"></textarea></div>
|
|
490
|
+
<div class="field"><label class="checkline"><input id="f-approval-required" type="checkbox"> 完成前需要人工审批</label><p class="hint">启用后,Git 合并和发布会等待审批记录。</p></div>
|
|
491
|
+
</div>
|
|
484
492
|
|
|
485
493
|
<div id="f-code-only">
|
|
486
494
|
<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 +552,7 @@
|
|
|
544
552
|
<div class="rd-actions">
|
|
545
553
|
<button id="btn-pause" class="ghost hidden" data-i18n="暂停">暂停</button>
|
|
546
554
|
<button data-i18n="取消运行" id="btn-cancel" class="danger hidden">取消运行</button>
|
|
555
|
+
<button data-i18n="立即重试" id="btn-resume-now" class="primary hidden" data-i18n-title="不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写" title="不等自动续跑的退避等待,马上接着跑;连载任务从断点继续,已完成章节不重写">立即重试</button>
|
|
547
556
|
<button data-i18n="↻ 继续任务" id="btn-retry" class="primary hidden" data-i18n-title="再跑一次这个任务;连载任务会断点续跑,已完成章节不重写" title="再跑一次这个任务;连载任务会断点续跑,已完成章节不重写">↻ 继续任务</button>
|
|
548
557
|
<button data-i18n="✎ 编辑重试" id="btn-editretry" class="ghost hidden" data-i18n-title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑" title="取回原任务的目标/目录/评审设置,修改后提交即重新开跑">✎ 编辑重试</button>
|
|
549
558
|
<button data-i18n="继续连载" id="btn-continue" class="ghost hidden">继续连载</button>
|