codebee 0.1.72 → 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 +12 -0
- package/README.md +4 -4
- 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/flows.py +1 -1
- package/app/core/health.py +14 -1
- package/app/core/jobs.py +54 -1
- package/app/core/manager.py +106 -17
- package/app/core/market.py +2 -2
- package/app/core/modelhub.py +104 -9
- package/app/core/openapi.py +154 -0
- package/app/core/paihang.py +8 -1
- package/app/core/pipeline.py +41 -0
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- package/app/core/presence.py +77 -0
- package/app/core/publish/auto.py +11 -0
- package/app/core/publish/flow.py +54 -3
- package/app/core/publish/manager.py +215 -28
- package/app/core/publish/qimao.py +30 -0
- package/app/core/retrieval.py +137 -0
- package/app/core/runner.py +85 -2
- package/app/core/skillpacks/fanqie-novel.md +66 -48
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +241 -0
- package/app/main.py +205 -48
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +390 -101
- package/app/ui/i18n.js +36 -7
- package/app/ui/index.html +8 -3
- package/app/ui/style.css +7059 -7040
- 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: {}, 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";
|
|
@@ -432,7 +432,7 @@ function flowIconHtml(f) {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
function flowDesc(f) {
|
|
435
|
-
if (f.id === "rank_scan") return t("抓四平台榜 → AI
|
|
435
|
+
if (f.id === "rank_scan") return t("抓四平台榜 → AI 选题洞察·附证据分级");
|
|
436
436
|
if (f.engine === "direct") return t("单智能体直达(快)");
|
|
437
437
|
if (f.engine === "code") return t("实现 → 验证 → 评审");
|
|
438
438
|
// 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
|
|
@@ -1269,18 +1269,65 @@ async function api(path, opts) {
|
|
|
1269
1269
|
}
|
|
1270
1270
|
}
|
|
1271
1271
|
|
|
1272
|
-
|
|
1272
|
+
/* 质量门禁三选一弹框(替代原生 confirm 的单一「强制放行」出口):
|
|
1273
|
+
* 取消 / ↻ 重新评审 / 强制放行。「重新评审」= 重写未达标章再评(语义同详情页
|
|
1274
|
+
* 「↻ 重写未达标章」按钮,走 retry 的未达标分支),仅 serial 任务才给;任务
|
|
1275
|
+
* 运行/排队中按钮置灰(后端 retry 会拒),但必须显出来并写明出路——正在跑的
|
|
1276
|
+
* 这轮结束后再来发布,仍不达标时按钮即可点;无声藏按钮用户只会看到死路。
|
|
1277
|
+
* 返回 "review" | "force" | null(取消)。 */
|
|
1278
|
+
async function pbGateChoice(err, promptText, taskId) {
|
|
1279
|
+
const tk = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
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
|
+
: "";
|
|
1290
|
+
const msg = (promptText || t("质量门禁未通过")) + "\n\n" + ((err && err.message) || "");
|
|
1291
|
+
const v = await _askOpen({
|
|
1292
|
+
title: t("质量门禁未通过"),
|
|
1293
|
+
bodyHtml: '<div class="ask-msg">' + esc(msg) + "</div>" +
|
|
1294
|
+
(canReview ? '<div class="ask-msg">' +
|
|
1295
|
+
esc(t("「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。")) + "</div>" : "") +
|
|
1296
|
+
(busyHint ? '<div class="ask-msg">' + esc(busyHint) + "</div>" : ""),
|
|
1297
|
+
okText: t("强制放行…"),
|
|
1298
|
+
extra: (tk && tk.serial)
|
|
1299
|
+
? [{ text: "↻ " + t("重新评审"), value: "review", disabled: busy,
|
|
1300
|
+
title: busy ? t("任务运行/排队中,暂不能发起;等这轮跑完再试") : "" }]
|
|
1301
|
+
: [],
|
|
1302
|
+
onOpen: () => $("ask-yes").focus(),
|
|
1303
|
+
});
|
|
1304
|
+
return v === "review" ? "review" : (v === true ? "force" : null);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function pbApiWithQualityOverride(path, body, prompt, taskId) {
|
|
1273
1308
|
try {
|
|
1274
1309
|
return await api(path, { method: "POST", body: JSON.stringify(body) });
|
|
1275
1310
|
} catch (e) {
|
|
1276
1311
|
const gate = e && e.data && e.data.quality_gate;
|
|
1277
1312
|
if (!gate) throw e;
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1313
|
+
const pick = await pbGateChoice(e, prompt, taskId);
|
|
1314
|
+
if (pick === "review") {
|
|
1315
|
+
// 重写未达标章 + 重新评审:不继续本次发布,调用方按 gate_reviewed 跳过
|
|
1316
|
+
// 成功提示;评审完成后用户再点一次发布按钮即可。
|
|
1317
|
+
try {
|
|
1318
|
+
const r = await api("/api/tasks/" + encodeURIComponent(taskId) + "/retry",
|
|
1319
|
+
{ method: "POST" });
|
|
1320
|
+
jumpToRun(r.run_id);
|
|
1321
|
+
toast(t("已发起重新评审(重写未达标章)——评审完成后再来发布"));
|
|
1322
|
+
} catch (e2) { toast(t("发起重新评审失败:") + e2.message, true); }
|
|
1323
|
+
return { gate_reviewed: true };
|
|
1324
|
+
}
|
|
1325
|
+
if (pick !== "force") throw e;
|
|
1326
|
+
const reason = await uiPrompt(t("强制放行:请输入人工复核原因(必填)"),
|
|
1327
|
+
"已人工复核稿件与平台版本");
|
|
1328
|
+
if (!reason) throw new Error(t("未填写人工复核原因,已取消强制放行"));
|
|
1282
1329
|
return await api(path, { method: "POST", body: JSON.stringify({ ...body,
|
|
1283
|
-
force: true, force_confirmed: true, force_reason: reason
|
|
1330
|
+
force: true, force_confirmed: true, force_reason: reason }) });
|
|
1284
1331
|
}
|
|
1285
1332
|
}
|
|
1286
1333
|
|
|
@@ -1321,7 +1368,20 @@ function _askOpen(opts) {
|
|
|
1321
1368
|
_askResolve = resolve;
|
|
1322
1369
|
$("ask-title").textContent = opts.title || t("确认");
|
|
1323
1370
|
$("ask-body").innerHTML = opts.bodyHtml || "";
|
|
1371
|
+
// 三态弹框:extra 按钮插在「取消」左侧,点按以 opts 里的 value 结束
|
|
1372
|
+
// (非布尔,只有用到 extra 的调用方需要判读);开新框先清上一帧残留。
|
|
1324
1373
|
const yes = $("ask-yes");
|
|
1374
|
+
const foot = yes.parentNode;
|
|
1375
|
+
foot.querySelectorAll(".ask-extra").forEach((b) => b.remove());
|
|
1376
|
+
(opts.extra || []).forEach((b) => {
|
|
1377
|
+
const el = document.createElement("button");
|
|
1378
|
+
el.className = (b.cls || "ghost") + " ask-extra";
|
|
1379
|
+
el.textContent = b.text;
|
|
1380
|
+
el.disabled = !!b.disabled; // 置灰态:看得见点不了(配合 title 说明原因)
|
|
1381
|
+
if (b.title) el.title = b.title;
|
|
1382
|
+
el.addEventListener("click", () => _askClose(b.value));
|
|
1383
|
+
foot.insertBefore(el, $("ask-no"));
|
|
1384
|
+
});
|
|
1325
1385
|
yes.textContent = opts.okText || t("确定");
|
|
1326
1386
|
yes.className = opts.danger ? "danger" : "primary";
|
|
1327
1387
|
$("ask").classList.remove("hidden");
|
|
@@ -1937,11 +1997,16 @@ function renderProvList() {
|
|
|
1937
1997
|
const box = $("prov-list");
|
|
1938
1998
|
if (!box) return;
|
|
1939
1999
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1940
|
-
const
|
|
2000
|
+
const kwHit = (p) => !kw ||
|
|
1941
2001
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1942
2002
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
1943
2003
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
1944
|
-
(p.base_url || "").toLowerCase().includes(kw)
|
|
2004
|
+
(p.base_url || "").toLowerCase().includes(kw);
|
|
2005
|
+
const view = S.provStatus || "enabled";
|
|
2006
|
+
// 状态过滤默认只看启用(2026-09-30 用户拍板):停用的点「已停用/全部」chip 才现身
|
|
2007
|
+
const stHit = (p) => view === "all" || (view === "off" ? p.enabled === false : p.enabled !== false);
|
|
2008
|
+
const hit = (S.providers || []).filter(kwHit);
|
|
2009
|
+
const provs = hit.filter(stHit);
|
|
1945
2010
|
if (!S.selProv && provs.length) S.selProv = provs[0].id;
|
|
1946
2011
|
if (S.selProv && !provs.some((p) => p.id === S.selProv)) {
|
|
1947
2012
|
S.selProv = provs.length ? provs[0].id : null;
|
|
@@ -1952,6 +2017,13 @@ function renderProvList() {
|
|
|
1952
2017
|
}
|
|
1953
2018
|
const selN = Object.keys(S.selProvs || {}).length;
|
|
1954
2019
|
box.classList.toggle("has-sel", selN > 0);
|
|
2020
|
+
const chips = hit.length ? '<div class="cat-chips prov-status">' +
|
|
2021
|
+
'<button class="cat-chip' + (view === "enabled" ? " active" : "") + '" onclick="setProvStatus(\'enabled\')">' +
|
|
2022
|
+
t("已启用") + " <b>" + hit.filter((p) => p.enabled !== false).length + "</b></button>" +
|
|
2023
|
+
'<button class="cat-chip' + (view === "off" ? " active" : "") + '" onclick="setProvStatus(\'off\')">' +
|
|
2024
|
+
t("已停用") + " <b>" + hit.filter((p) => p.enabled === false).length + "</b></button>" +
|
|
2025
|
+
'<button class="cat-chip' + (view === "all" ? " active" : "") + '" onclick="setProvStatus(\'all\')">' +
|
|
2026
|
+
t("全部") + " <b>" + hit.length + "</b></button></div>" : "";
|
|
1955
2027
|
const batch = selN ? '<div class="prov-batch"><span class="n">' + t("已选 ") + selN + t(" 个") + "</span>" +
|
|
1956
2028
|
'<button class="ghost small" onclick="batchProvOp(\'enable\')">' + t("启用") + '</button>' +
|
|
1957
2029
|
'<button class="ghost small" onclick="batchProvOp(\'disable\')">' + t("停用") + '</button>' +
|
|
@@ -1960,9 +2032,10 @@ function renderProvList() {
|
|
|
1960
2032
|
const allBox = provs.length ? '<label class="prov-all"><input type="checkbox"' +
|
|
1961
2033
|
(selN === provs.length && selN > 0 ? " checked" : "") +
|
|
1962
2034
|
' onchange="toggleAllProvSel(this.checked)">' + t(" 全选") +
|
|
1963
|
-
(kw ? t("(筛选后 ") + provs.length + t(" 个)") : t("(") + provs.length + t(")")) + "</label>" : "";
|
|
1964
|
-
box.innerHTML = batch + allBox + (provs.map((p) => {
|
|
1965
|
-
|
|
2035
|
+
(kw || view !== "all" ? t("(筛选后 ") + provs.length + t(" 个)") : t("(") + provs.length + t(")")) + "</label>" : "";
|
|
2036
|
+
box.innerHTML = chips + batch + allBox + (provs.map((p) => {
|
|
2037
|
+
// 卡片计数只数启用中的模型(列表默认也只展示启用中的,两边口径一致)
|
|
2038
|
+
const n = p.models == null ? null : p.models.filter((m) => !m.hidden && m.enabled !== false).length;
|
|
1966
2039
|
const st = n == null ? t("未获取") : n + t(" 模型");
|
|
1967
2040
|
const off = p.enabled === false;
|
|
1968
2041
|
return '<div class="prov-item' + (p.id === S.selProv ? " active" : "") + (off ? " off" : "") +
|
|
@@ -1980,7 +2053,16 @@ function renderProvList() {
|
|
|
1980
2053
|
}).join("") ||
|
|
1981
2054
|
'<div class="hint" style="padding:8px">' + (kw && (S.providers || []).length
|
|
1982
2055
|
? t("没有匹配「") + esc(S.provFilter) + t("」的供应商。")
|
|
1983
|
-
:
|
|
2056
|
+
: (view === "enabled" && hit.some((p) => p.enabled === false)
|
|
2057
|
+
? t("%1 个供应商已停用——点上方「已停用」查看。").replace("%1", hit.filter((p) => p.enabled === false).length)
|
|
2058
|
+
: t("暂无供应商——点上方「导入」。"))) + "</div>");
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function setProvStatus(v) {
|
|
2062
|
+
S.provStatus = v;
|
|
2063
|
+
S.modelsSig = null;
|
|
2064
|
+
renderProvList(); // 可能收敛 S.selProv,详情跟着换人
|
|
2065
|
+
renderProvDetail();
|
|
1984
2066
|
}
|
|
1985
2067
|
|
|
1986
2068
|
function toggleProvSel(id, on) {
|
|
@@ -1994,11 +2076,13 @@ function toggleAllProvSel(on) {
|
|
|
1994
2076
|
S.selProvs = {};
|
|
1995
2077
|
if (on) {
|
|
1996
2078
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1997
|
-
|
|
2079
|
+
const view = S.provStatus || "enabled";
|
|
2080
|
+
for (const p of (S.providers || []).filter((p) => (!kw ||
|
|
1998
2081
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1999
2082
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
2000
2083
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
2001
|
-
(p.base_url || "").toLowerCase().includes(kw))
|
|
2084
|
+
(p.base_url || "").toLowerCase().includes(kw)) &&
|
|
2085
|
+
(view === "all" || (view === "off" ? p.enabled === false : p.enabled !== false)))) S.selProvs[p.id] = true;
|
|
2002
2086
|
}
|
|
2003
2087
|
S.modelsSig = null;
|
|
2004
2088
|
renderProvList();
|
|
@@ -2105,6 +2189,18 @@ function renderProvDetail() {
|
|
|
2105
2189
|
' value="' + esc(kw) + '" oninput="filterModels(\'' + esc(p.id) + '\', this.value)">' +
|
|
2106
2190
|
'<span class="pm-count" id="pm-count-' + esc(p.id) + '"></span>' +
|
|
2107
2191
|
pmAddHtml(p) + '</div>';
|
|
2192
|
+
// 模型列表状态过滤:默认只看启用(与供应商列表同一套 chip 口径)
|
|
2193
|
+
const vis = (p.models || []).filter((m) => !m.hidden);
|
|
2194
|
+
if (vis.length) {
|
|
2195
|
+
const mview = S.mlistStatus || "enabled";
|
|
2196
|
+
html += '<div class="cat-chips pm-status">' +
|
|
2197
|
+
'<button class="cat-chip' + (mview === "enabled" ? " active" : "") + '" onclick="setModelStatus(\'enabled\')">' +
|
|
2198
|
+
t("已启用") + " <b>" + vis.filter((m) => m.enabled !== false).length + "</b></button>" +
|
|
2199
|
+
'<button class="cat-chip' + (mview === "off" ? " active" : "") + '" onclick="setModelStatus(\'off\')">' +
|
|
2200
|
+
t("已停用") + " <b>" + vis.filter((m) => m.enabled === false).length + "</b></button>" +
|
|
2201
|
+
'<button class="cat-chip' + (mview === "all" ? " active" : "") + '" onclick="setModelStatus(\'all\')">' +
|
|
2202
|
+
t("全部") + " <b>" + vis.length + "</b></button></div>";
|
|
2203
|
+
}
|
|
2108
2204
|
html += '<div id="pm-groups" class="pm-groups' + (selN ? " has-sel" : "") + '">' +
|
|
2109
2205
|
provModelGroupsHtml(p) + "</div>";
|
|
2110
2206
|
if (hidden.length) {
|
|
@@ -2258,7 +2354,9 @@ function wireCapTags(p, excludeProto) {
|
|
|
2258
2354
|
/* 模型分组区 HTML(全量重绘与过滤重绘共用;过滤时暂停拖拽排序) */
|
|
2259
2355
|
function provModelGroupsHtml(p) {
|
|
2260
2356
|
const kw = ((S.modelFilter || {})[p.id] || "").trim().toLowerCase();
|
|
2357
|
+
const mview = S.mlistStatus || "enabled";
|
|
2261
2358
|
const models = (p.models || []).filter((m) => !m.hidden)
|
|
2359
|
+
.filter((m) => mview === "all" || (mview === "off" ? m.enabled === false : m.enabled !== false))
|
|
2262
2360
|
.filter((m) => !kw || (m.name || "").toLowerCase().includes(kw))
|
|
2263
2361
|
.sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
2264
2362
|
const sel = modelSel(p.id);
|
|
@@ -2269,9 +2367,15 @@ function provModelGroupsHtml(p) {
|
|
|
2269
2367
|
}
|
|
2270
2368
|
const gnames = Object.keys(groups).sort();
|
|
2271
2369
|
if (!gnames.length) {
|
|
2272
|
-
|
|
2370
|
+
const offAll = (p.models || []).filter((m) => !m.hidden && m.enabled === false).length;
|
|
2371
|
+
const msg = kw
|
|
2273
2372
|
? t("没有匹配「") + esc(kw) + t("」的模型。")
|
|
2274
|
-
:
|
|
2373
|
+
: mview === "off"
|
|
2374
|
+
? t("没有已停用的模型。")
|
|
2375
|
+
: (mview === "enabled" && offAll
|
|
2376
|
+
? t("%1 个模型已停用——点上方「已停用」查看。").replace("%1", offAll)
|
|
2377
|
+
: t("该供应商没有可用模型(或全部被停用)。"));
|
|
2378
|
+
return '<div class="empty">' + msg + "</div>";
|
|
2275
2379
|
}
|
|
2276
2380
|
let html = "";
|
|
2277
2381
|
for (const g of gnames) {
|
|
@@ -2307,9 +2411,12 @@ function updateModelCount(pid) {
|
|
|
2307
2411
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2308
2412
|
if (!el || !p || !p.models) return;
|
|
2309
2413
|
const kw = (S.modelFilter || {})[pid] || "";
|
|
2310
|
-
const
|
|
2414
|
+
const mview = S.mlistStatus || "enabled";
|
|
2415
|
+
const inView = (m) => !m.hidden &&
|
|
2416
|
+
(mview === "all" || (mview === "off" ? m.enabled === false : m.enabled !== false));
|
|
2417
|
+
const total = p.models.filter(inView).length;
|
|
2311
2418
|
const shown = total && kw.trim()
|
|
2312
|
-
? p.models.filter((m) =>
|
|
2419
|
+
? p.models.filter((m) => inView(m) &&
|
|
2313
2420
|
(m.name || "").toLowerCase().includes(kw.trim().toLowerCase())).length
|
|
2314
2421
|
: total;
|
|
2315
2422
|
el.textContent = kw.trim() ? (shown + " / " + total + t(" 个模型")) : (total + t(" 个模型"));
|
|
@@ -2508,9 +2615,11 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2508
2615
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2509
2616
|
if (!p) return;
|
|
2510
2617
|
const kw = ((S.modelFilter || {})[pid] || "").trim().toLowerCase();
|
|
2618
|
+
const mview = S.mlistStatus || "enabled";
|
|
2511
2619
|
const s = modelSelSet(pid);
|
|
2512
2620
|
for (const m of p.models || []) {
|
|
2513
2621
|
if (m.hidden) continue;
|
|
2622
|
+
if (mview !== "all" && (mview === "off" ? m.enabled !== false : m.enabled === false)) continue;
|
|
2514
2623
|
if ((m.protocol || p.protocol || t("其他")) !== group) continue;
|
|
2515
2624
|
if (kw && !(m.name || "").toLowerCase().includes(kw)) continue; // 过滤时只作用于可见行
|
|
2516
2625
|
if (on) s[m.name] = true; else delete s[m.name];
|
|
@@ -2519,6 +2628,13 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2519
2628
|
renderProvDetail();
|
|
2520
2629
|
}
|
|
2521
2630
|
|
|
2631
|
+
/* 模型列表状态过滤 chip:默认只看启用 */
|
|
2632
|
+
function setModelStatus(v) {
|
|
2633
|
+
S.mlistStatus = v;
|
|
2634
|
+
S.modelsSig = null;
|
|
2635
|
+
renderProvDetail();
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2522
2638
|
function clearModelSel(pid) {
|
|
2523
2639
|
if (S.selModels) delete S.selModels[pid];
|
|
2524
2640
|
S.modelsSig = null;
|
|
@@ -3029,6 +3145,9 @@ async function createTask() {
|
|
|
3029
3145
|
S.clarifyDone = false;
|
|
3030
3146
|
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
3031
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);
|
|
3032
3151
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
3033
3152
|
// 附件与代码版本:任务创建时服务端把待提交附件移入 _attachments/ 并注入上下文
|
|
3034
3153
|
if ((S.atts || []).length) payload.attachments = S.atts.filter((a) => a.id).map((a) => a.id);
|
|
@@ -4417,6 +4536,7 @@ window.sideOpenTask = function (key) {
|
|
|
4417
4536
|
S.detailRunId = null;
|
|
4418
4537
|
S.focusStep = 0;
|
|
4419
4538
|
S.taskSig = "";
|
|
4539
|
+
S.runDetailSig = ""; // 详情在任务态/运行态间共用同一套 DOM:切走必须作废运行态签名
|
|
4420
4540
|
rdTabReset();
|
|
4421
4541
|
if (!S.histJump && typeof histPush === "function") histPush({ m: "main", tab: "run-detail" });
|
|
4422
4542
|
showDetailInMain();
|
|
@@ -4506,6 +4626,20 @@ function setRetryBtn(run, task) {
|
|
|
4506
4626
|
: t("再跑一次这个任务;连载任务会断点续跑,已完成章节不重写");
|
|
4507
4627
|
}
|
|
4508
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
|
+
|
|
4509
4643
|
/* 任务级详情:聚合该任务所有 run 的步骤。
|
|
4510
4644
|
* 有任务档案的从 /api/tasks/<id>/runs 拉全量——前端 run 窗口只有最近 40 条,
|
|
4511
4645
|
* 窗口外的历史会被算丢;无主运行组(管理操作/任务已删)仍用窗口数据。
|
|
@@ -4523,6 +4657,7 @@ function renderTaskDetail() {
|
|
|
4523
4657
|
setRetryBtn(retrySnapshotForTask(key, tk, latestSummary), tk);
|
|
4524
4658
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
4525
4659
|
syncArchBtn(tk, tk && (tk.status === "running" || tk.status === "queued"));
|
|
4660
|
+
setResumeNowBtn(null); // 退避与否要看 run 数据,先收起防上一个任务残留
|
|
4526
4661
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
4527
4662
|
if (isTask) {
|
|
4528
4663
|
loadTaskRuns(key, latestSummary);
|
|
@@ -4586,6 +4721,7 @@ function loadTaskRuns(key, latestSummary) {
|
|
|
4586
4721
|
offset: Number(data.offset) + runs.length, total: Number(data.total),
|
|
4587
4722
|
hasMore: !!data.has_more, totals: data.totals || null, loading: false,
|
|
4588
4723
|
});
|
|
4724
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4589
4725
|
_taskRunsCache.delete(key);
|
|
4590
4726
|
_taskRunsCache.set(key, runs);
|
|
4591
4727
|
while (_taskRunsCache.size > 3) {
|
|
@@ -4635,6 +4771,7 @@ async function loadEarlierTaskRuns(key) {
|
|
|
4635
4771
|
page.offset = Number(data.offset) + older.length;
|
|
4636
4772
|
page.total = Number(data.total) || page.total;
|
|
4637
4773
|
page.hasMore = !!data.has_more;
|
|
4774
|
+
if (data && data.contract) S.taskContracts[key] = data.contract;
|
|
4638
4775
|
drawTaskDetail(key, _taskRunsCache.get(key), appendFrom);
|
|
4639
4776
|
} catch (e) {
|
|
4640
4777
|
toast(t("加载历史运行失败:") + e.message, true);
|
|
@@ -4662,11 +4799,23 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4662
4799
|
const lr0 = runs[0] || {};
|
|
4663
4800
|
const resumePending = (lr0.resume_enqueue_at &&
|
|
4664
4801
|
Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
|
|
4802
|
+
// cost_usd/tokens 故意不入签名:运行中每次记账都在变,入签名会把整帧
|
|
4803
|
+
// 重绘变回「几秒一闪」;步骤状态翻转自然带上新数字,任务累计统计另有
|
|
4804
|
+
// detailSide 通道(2s 节流)保实时,两者都够新鲜。
|
|
4805
|
+
// 消息数/verdict 必须归一化再入签名:loadTaskRuns 每轮把 state 的摘要
|
|
4806
|
+
// 投影(message_count 等)assign 进缓存 run,下一轮干净 run 又洗掉这些
|
|
4807
|
+
// 键——裸取字段会在 null↔0 / 全量↔精简 verdict 间来回摆,每 8s 必重画
|
|
4808
|
+
// 一帧(2026-09-30 任务页闪烁实案)。归一后两种形状取值一致。
|
|
4809
|
+
const msgN = (r) => r.message_count != null ? Number(r.message_count)
|
|
4810
|
+
: (r.messages || []).length;
|
|
4811
|
+
const msgP = (r) => r.pending_message_count != null ? Number(r.pending_message_count)
|
|
4812
|
+
: (r.messages || []).filter((m) => !m.consumed).length;
|
|
4813
|
+
const vcmp = (r) => { const v = r.verdict || null;
|
|
4814
|
+
return v ? JSON.stringify([v.pass, v.publishable, v.verify_pass, v.review_pass]) : null; };
|
|
4665
4815
|
const sig = JSON.stringify(runs.map((r) => [r.id, r.status, (r.steps || []).length,
|
|
4666
4816
|
(r.steps || []).map((s) => s.status).join(""),
|
|
4667
|
-
(r
|
|
4668
|
-
r.
|
|
4669
|
-
r.summary, r.error, r.cost_usd, r.tokens, JSON.stringify(r.verdict || {})])
|
|
4817
|
+
msgN(r), msgP(r),
|
|
4818
|
+
r.summary, r.error, vcmp(r)])
|
|
4670
4819
|
.concat([JSON.stringify([(bmRoot || {}).book_meta || null,
|
|
4671
4820
|
(bmTask || {}).cover_gen || null]), resumePending]));
|
|
4672
4821
|
if (sig === S.taskSig) return;
|
|
@@ -4717,6 +4866,7 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4717
4866
|
// S.detailRunId,cancelRun 靠 S.cancelTargetRunId 知道取消谁
|
|
4718
4867
|
$("btn-cancel").classList.toggle("hidden", !activeRun);
|
|
4719
4868
|
S.cancelTargetRunId = activeRun ? activeRun.id : null;
|
|
4869
|
+
setResumeNowBtn(activeRun && activeRun.status === "queued" ? activeRun : null);
|
|
4720
4870
|
const bp2 = $("btn-pause");
|
|
4721
4871
|
if (bp2 && activeRun) {
|
|
4722
4872
|
bp2.classList.toggle("hidden", false);
|
|
@@ -4915,6 +5065,7 @@ async function openRun(id, pinTab) {
|
|
|
4915
5065
|
_taskStepRenderToken++;
|
|
4916
5066
|
S.detailRunId = id;
|
|
4917
5067
|
S.detailTaskKey = null;
|
|
5068
|
+
S.runDetailSig = ""; // 新目标首帧必画;也兜住「任务态改过 DOM 后回到本运行」的串台
|
|
4918
5069
|
renderPageCrumb(); // 标题「运行详情」;必须在赋值后画——showDetailInMain 先于本函数跑,那边画不到
|
|
4919
5070
|
rdTabReset(pinTab || null); // sideOpenRun 带步骤号时钉住步骤分区
|
|
4920
5071
|
document.querySelector("#sub-runs .panel:first-child").classList.add("hidden");
|
|
@@ -4929,6 +5080,7 @@ function closeRun() {
|
|
|
4929
5080
|
S.detailRunId = null;
|
|
4930
5081
|
S.detailTaskKey = null;
|
|
4931
5082
|
S.taskSig = "";
|
|
5083
|
+
S.runDetailSig = "";
|
|
4932
5084
|
S.focusStep = 0;
|
|
4933
5085
|
rdTabReset();
|
|
4934
5086
|
stopLogLive();
|
|
@@ -5272,10 +5424,20 @@ async function renderRunDetail() {
|
|
|
5272
5424
|
const active = run.status === "queued" || run.status === "running";
|
|
5273
5425
|
$("btn-cancel").classList.toggle("hidden", !active);
|
|
5274
5426
|
S.cancelTargetRunId = active ? run.id : null;
|
|
5427
|
+
setResumeNowBtn(run.status === "queued" ? run : null); // 在 sig 守卫前:退避相位由 etaBucket 分钟桶兜底刷新
|
|
5275
5428
|
$("btn-delete").classList.toggle("hidden", active);
|
|
5276
5429
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5277
5430
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
5278
5431
|
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
5432
|
+
// 签名守卫(模型页/检查器同款):数据没变不重绘。详情区是全页最重的
|
|
5433
|
+
// 渲染面,此前 SSE 推送+轮询每 8s 无差别整块重写,是「页面一闪一闪、
|
|
5434
|
+
// 发布表单填一半被清空」的总根源(2026-09-30 用户实案)。运行中 ETA
|
|
5435
|
+
// 随墙钟走,签名带 1 分钟桶让「预计剩余」每分钟翻新,不必跟轮询同频。
|
|
5436
|
+
const etaBucket = (run.status === "running" || run.status === "queued")
|
|
5437
|
+
? Math.floor(Date.now() / 60000) : 0;
|
|
5438
|
+
const rdSig = JSON.stringify([run, rcTask || null, etaBucket]);
|
|
5439
|
+
if (rdSig === S.runDetailSig) return;
|
|
5440
|
+
S.runDetailSig = rdSig;
|
|
5279
5441
|
// 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
|
|
5280
5442
|
// 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
|
|
5281
5443
|
syncArchBtn(rcTask, active);
|
|
@@ -5487,18 +5649,27 @@ function bmRootTask(task) {
|
|
|
5487
5649
|
return cur || task;
|
|
5488
5650
|
}
|
|
5489
5651
|
|
|
5490
|
-
window.bmGoRoot = function (rootId) {
|
|
5491
|
-
const t = (((S.state || {}).tasks || []).find((x) => x.id === rootId)) || null;
|
|
5492
|
-
if (!t) { toast(t("找不到首批任务(可能已删除)"), true); return; }
|
|
5493
|
-
browserOpenTaskRun(t, "bookmeta");
|
|
5494
|
-
};
|
|
5495
|
-
|
|
5496
5652
|
function renderBookMetaPanel(task) {
|
|
5497
5653
|
const box = $("rd-bookmeta");
|
|
5498
5654
|
if (!box || !(task || {}).id || !bmNeedsPanel(task)) {
|
|
5499
5655
|
if (box) box.classList.add("hidden");
|
|
5500
5656
|
return;
|
|
5501
5657
|
}
|
|
5658
|
+
// 输入/展开态保护(检查器 insp-msg-input 同款纪律):面板里有输入焦点
|
|
5659
|
+
//(正在填登记表单)或章节清单正展开时跳过本帧整块重绘——innerHTML
|
|
5660
|
+
// 重建会把没提交的字清掉、把展开列表折回去。轮询链继续转,blur/收起
|
|
5661
|
+
// 后下一轮数据变化自然跟上;登记草稿另有 S.pbRegDraft 兜底回填。
|
|
5662
|
+
// 守卫只保护同一任务(box.dataset.taskId):切到别的任务的详情时,
|
|
5663
|
+
// 上一任务的展开态没有保留价值,放行重绘——否则新任务详情的作品信息
|
|
5664
|
+
// 页签一直挂着上一本书的建书/发布数据(2026-09-30 用户实案)。
|
|
5665
|
+
const ae = document.activeElement;
|
|
5666
|
+
if (box.dataset.taskId === task.id &&
|
|
5667
|
+
((ae && box.contains(ae) &&
|
|
5668
|
+
(ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) ||
|
|
5669
|
+
box.querySelector(".pb-chapters:not(.hidden)"))) {
|
|
5670
|
+
pbSyncState(task);
|
|
5671
|
+
return;
|
|
5672
|
+
}
|
|
5502
5673
|
const src = bmRootTask(task); // 都是用第一个:开书资料读链上根任务
|
|
5503
5674
|
const inherited = src.id !== task.id;
|
|
5504
5675
|
const bm = src.book_meta || {};
|
|
@@ -5511,21 +5682,12 @@ function renderBookMetaPanel(task) {
|
|
|
5511
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>' +
|
|
5512
5683
|
'<span class="flex1"></span>';
|
|
5513
5684
|
if (taskBusy) html += '<span class="bm-warn">' + esc(t("任务运行中,生成将在本轮结束后可用")) + "</span>";
|
|
5514
|
-
// 续写批次只读沿用:生成/重生成回首批任务做,这里给指路入口(一本书一份资料)
|
|
5515
|
-
if (inherited) html += '<button class="ghost" onclick="bmGoRoot(\'' + esc(src.id) + '\')" title="' +
|
|
5516
|
-
esc(t("开书资料在首批任务上生成与维护")) + '">' +
|
|
5517
|
-
'<svg class="ico" aria-hidden="true"><use href="#i-arrow-right"/></svg>' + t("回首批任务") + "</button>";
|
|
5518
5685
|
html += "</div>";
|
|
5519
5686
|
if (!bm.fanqie && !bm.qimao && !taskBusy) {
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
} else {
|
|
5525
|
-
html += '<div class="bm-guide"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5526
|
-
'<div><b>' + esc(t("章节已就绪,创建作品还差开书资料")) + "</b>" +
|
|
5527
|
-
"<p>" + esc(t("点平台卡片上的「生成」按钮,一键产出书名/简介/标签/主角名等建书资料,生成后逐字段复制进建书表单。")) + "</p></div></div>";
|
|
5528
|
-
}
|
|
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>";
|
|
5529
5691
|
}
|
|
5530
5692
|
html += '<div class="bm-cards">' + BOOKMETA_PLATFORMS.map((p) => {
|
|
5531
5693
|
const entry = bm[p.id] || {};
|
|
@@ -5555,9 +5717,7 @@ function renderBookMetaPanel(task) {
|
|
|
5555
5717
|
body = '<div class="bm-empty">' + esc(t("尚未生成")) + "</div>";
|
|
5556
5718
|
}
|
|
5557
5719
|
let action;
|
|
5558
|
-
if (
|
|
5559
|
-
action = ""; // 续写批次只读:生成/重生成集中在头部「回首批任务」入口
|
|
5560
|
-
} else if (st === "running") {
|
|
5720
|
+
if (st === "running") {
|
|
5561
5721
|
action = '<button class="ghost" disabled><svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + t("生成中…") + "</button>";
|
|
5562
5722
|
} else if (st === "done") {
|
|
5563
5723
|
action = '<button class="ghost" onclick="bmGen(\'' + esc(task.id) + "', '" + p.id + '\')" title="' + esc(t("重新生成会覆盖现有内容")) + '">' +
|
|
@@ -5613,6 +5773,7 @@ function renderBookMetaPanel(task) {
|
|
|
5613
5773
|
bmStatusChip(cgSt) + '<span class="flex1"></span>' + cgAction + "</div>" + cgBody + "</div></div>";
|
|
5614
5774
|
box.classList.remove("hidden");
|
|
5615
5775
|
box.innerHTML = html;
|
|
5776
|
+
box.dataset.taskId = task.id; // 跨任务守卫标记:展开态/输入保护只对同一任务生效
|
|
5616
5777
|
// TAB 徽章:已生成平台数(生成中显示 ●,随下次轮询刷新)
|
|
5617
5778
|
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="bookmeta"] .rd-badge');
|
|
5618
5779
|
if (badge) {
|
|
@@ -5644,8 +5805,8 @@ window.coverGen = async function (taskId) {
|
|
|
5644
5805
|
};
|
|
5645
5806
|
|
|
5646
5807
|
window.bmCopyBtn = function (btn) {
|
|
5647
|
-
|
|
5648
|
-
toast(t("已复制:") + (btn.dataset.label || ""));
|
|
5808
|
+
// 只在真实复制成功后报「已复制」——剪贴板不可用时 fallbackCopy 会弹失败提示
|
|
5809
|
+
copyText(btn.dataset.text || "", () => toast(t("已复制:") + (btn.dataset.label || "")));
|
|
5649
5810
|
};
|
|
5650
5811
|
|
|
5651
5812
|
/* ---------------- 一键发布(bookmeta 分区内每平台卡的发布行) ----------------
|
|
@@ -5751,7 +5912,7 @@ function pbBlock(task, platform) {
|
|
|
5751
5912
|
}
|
|
5752
5913
|
btns += '<button class="primary" ' + (busy ? "disabled" : "") +
|
|
5753
5914
|
' onclick="pbCreateBook(\'' + esc(task.id) + "', '" + platform + '\')">' + t("创建作品") + "</button>";
|
|
5754
|
-
btns += '<button class="ghost" onclick="pbRegToggle(\'' + platform + '\')" title="' +
|
|
5915
|
+
btns += '<button class="ghost" onclick="pbRegToggle(\'' + esc(task.id) + "','" + platform + '\')" title="' +
|
|
5755
5916
|
esc(t("作品已在平台建好(建书流程没走通或手工建的)?补登记后直接发章,不会在平台新建")) + '">' + t("登记已有作品") + "</button>";
|
|
5756
5917
|
}
|
|
5757
5918
|
let extra = "";
|
|
@@ -5759,9 +5920,16 @@ function pbBlock(task, platform) {
|
|
|
5759
5920
|
extra += '<div class="pb-hint">' + esc(t("若作品其实已在平台建好,点「登记已有作品」补登记,勿重复创建")) + "</div>";
|
|
5760
5921
|
}
|
|
5761
5922
|
if (!bookReady) {
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5923
|
+
// 展开态+草稿值外置(S.pbRegDraft,按任务+平台键):整块重绘不再清空
|
|
5924
|
+
// 用户填了一半的作品名/ID——输入即存,重绘按草稿回填,登记成功才清。
|
|
5925
|
+
const reg = (S.pbRegDraft || {})[task.id + ":" + platform] || {};
|
|
5926
|
+
extra += '<div class="pb-reg' + (reg.open ? "" : " hidden") + '" id="pb-reg-' + platform + '">' +
|
|
5927
|
+
'<input id="pb-reg-title-' + platform + '" value="' + esc(reg.title || "") +
|
|
5928
|
+
'" placeholder="' + esc(t("平台上的作品名(必填)")) +
|
|
5929
|
+
'" oninput="pbRegDraftSave(\'' + esc(task.id) + "','" + platform + '\',\'title\',this.value)">' +
|
|
5930
|
+
'<input id="pb-reg-id-' + platform + '" value="' + esc(reg.id || "") +
|
|
5931
|
+
'" placeholder="' + esc(t("作品ID(选填,管理页URL里有)")) +
|
|
5932
|
+
'" oninput="pbRegDraftSave(\'' + esc(task.id) + "','" + platform + '\',\'id\',this.value)">' +
|
|
5765
5933
|
'<button class="ghost" onclick="pbRegister(\'' + esc(task.id) + "', '" + platform + '\')">' + t("确认登记") + "</button>" +
|
|
5766
5934
|
"</div>";
|
|
5767
5935
|
}
|
|
@@ -5777,18 +5945,27 @@ function pbBlock(task, platform) {
|
|
|
5777
5945
|
let _pbTimer = 0;
|
|
5778
5946
|
function pbSyncState(task) {
|
|
5779
5947
|
clearTimeout(_pbTimer);
|
|
5948
|
+
// 5s 节流折进调度间隔:距上一拉未满 5s 就等满再拉(pbKick 置 0 可立即拉)
|
|
5949
|
+
const delay = Math.max(120, 5000 - (Date.now() - (S.pubFetchAt || 0)));
|
|
5780
5950
|
_pbTimer = setTimeout(async () => {
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5951
|
+
// 自续轮询链:详情区签名守卫上线后,主渲染泵不再每 8s 带跑本面板,
|
|
5952
|
+
// 发布状态自己转——面板可见且仍指向同一任务就 5s 一拉,收起/切走
|
|
5953
|
+
// 自然停,下次 renderBookMetaPanel 重新点火。task 对象可能已老,
|
|
5954
|
+
// 一律按 id 从 S.state 重解析,渲染吃的永远是新数据。
|
|
5955
|
+
const box = $("rd-bookmeta");
|
|
5956
|
+
if (!task || !task.id || !box || box.classList.contains("hidden") ||
|
|
5957
|
+
$("run-detail").classList.contains("hidden")) return;
|
|
5958
|
+
const tk = ((S.state || {}).tasks || []).find((x) => x.id === task.id);
|
|
5959
|
+
if (!tk || ((S.lastRunTask || {}).id || "") !== tk.id) return;
|
|
5960
|
+
S.pubFetchAt = Date.now();
|
|
5784
5961
|
let sig = "";
|
|
5785
5962
|
try {
|
|
5786
5963
|
const v = await api("/api/publish");
|
|
5787
5964
|
S.pubState = v;
|
|
5788
5965
|
sig += JSON.stringify(v.platforms || {});
|
|
5789
|
-
} catch (e) { return; }
|
|
5966
|
+
} catch (e) { pbSyncState(tk); return; }
|
|
5790
5967
|
try {
|
|
5791
|
-
const ti = await api("/api/publish/task/" + encodeURIComponent(
|
|
5968
|
+
const ti = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/history");
|
|
5792
5969
|
S.pubTaskInfo = ti;
|
|
5793
5970
|
sig += "#" + JSON.stringify(ti.books || {}) + "#" + (ti.history || []).length;
|
|
5794
5971
|
// 已发章数自动校准:平台在线且登记在案、超 10 分钟没对过账就静默对一次
|
|
@@ -5803,22 +5980,23 @@ function pbSyncState(task) {
|
|
|
5803
5980
|
if (ps2.status === "connected" && nowMs - at > 600000 &&
|
|
5804
5981
|
nowMs - (S["_pbSyncAt_" + p] || 0) > 600000) {
|
|
5805
5982
|
S["_pbSyncAt_" + p] = nowMs;
|
|
5806
|
-
api("/api/publish/task/" + encodeURIComponent(
|
|
5983
|
+
api("/api/publish/task/" + encodeURIComponent(tk.id) + "/sync-published",
|
|
5807
5984
|
{ method: "POST", body: JSON.stringify({ platform: p }) }).catch(() => {});
|
|
5808
5985
|
}
|
|
5809
5986
|
});
|
|
5810
5987
|
} catch (e) { /* 任务级失败不阻塞平台状态 */ }
|
|
5811
5988
|
try {
|
|
5812
5989
|
// 批量发布视图(待发数/护栏/进度)——进行中时靠本节流轮询自然刷新
|
|
5813
|
-
const au = await api("/api/publish/task/" + encodeURIComponent(
|
|
5990
|
+
const au = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/pending");
|
|
5814
5991
|
S.pubAuto = au;
|
|
5815
5992
|
sig += "@" + JSON.stringify(au.books || {}) + "@" + JSON.stringify(au.running || {});
|
|
5816
5993
|
} catch (e) { /* 视图缺失(老服务)不阻塞 */ }
|
|
5817
|
-
if (sig !== (S._pbSig || "")
|
|
5994
|
+
if (sig !== (S._pbSig || "")) {
|
|
5818
5995
|
S._pbSig = sig;
|
|
5819
|
-
renderBookMetaPanel(
|
|
5996
|
+
renderBookMetaPanel(tk);
|
|
5820
5997
|
} else S._pbSig = sig;
|
|
5821
|
-
|
|
5998
|
+
pbSyncState(tk); // 续链:面板还开着就下一轮 5s 后再来
|
|
5999
|
+
}, delay);
|
|
5822
6000
|
}
|
|
5823
6001
|
|
|
5824
6002
|
window.pbSyncPublished = async function (taskId, platform) {
|
|
@@ -5856,19 +6034,34 @@ window.pbProbe = async function (platform) {
|
|
|
5856
6034
|
};
|
|
5857
6035
|
|
|
5858
6036
|
window.pbCreateBook = async function (taskId, platform) {
|
|
5859
|
-
if (!
|
|
6037
|
+
if (!(await uiConfirm(t("将用生成的作品信息在平台自动填建书表单。填好后会停在最后一步,由你在浏览器里人工点提交。继续?"),
|
|
6038
|
+
{ ok: t("开始建书") }))) return;
|
|
5860
6039
|
try {
|
|
5861
|
-
await pbApiWithQualityOverride(
|
|
6040
|
+
const r = await pbApiWithQualityOverride(
|
|
5862
6041
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/create-book",
|
|
5863
|
-
{ platform }, "创建作品前的质量门禁未通过");
|
|
5864
|
-
|
|
6042
|
+
{ platform }, "创建作品前的质量门禁未通过", taskId);
|
|
6043
|
+
if (!(r && r.gate_reviewed))
|
|
6044
|
+
toast(t("正在自动填写建书表单…(完成后请在浏览器里确认提交)"));
|
|
5865
6045
|
} catch (e) { toast(t("建书失败:") + e.message, true); }
|
|
5866
6046
|
S._pbSig = ""; pbKick();
|
|
5867
6047
|
};
|
|
5868
6048
|
|
|
5869
|
-
|
|
6049
|
+
/* 登记草稿随输随存:重绘回填的写侧(读侧在 pbBlock 模板里 value=/open) */
|
|
6050
|
+
window.pbRegDraftSave = function (taskId, platform, key, val) {
|
|
6051
|
+
S.pbRegDraft = S.pbRegDraft || {};
|
|
6052
|
+
const k = taskId + ":" + platform;
|
|
6053
|
+
const d = S.pbRegDraft[k] = Object.assign({}, S.pbRegDraft[k]);
|
|
6054
|
+
d[key] = String(val == null ? "" : val);
|
|
6055
|
+
};
|
|
6056
|
+
|
|
6057
|
+
window.pbRegToggle = function (taskId, platform) {
|
|
5870
6058
|
const box = $("pb-reg-" + platform);
|
|
5871
|
-
if (box)
|
|
6059
|
+
if (!box) return;
|
|
6060
|
+
box.classList.toggle("hidden");
|
|
6061
|
+
S.pbRegDraft = S.pbRegDraft || {};
|
|
6062
|
+
const k = taskId + ":" + platform;
|
|
6063
|
+
const d = S.pbRegDraft[k] = Object.assign({}, S.pbRegDraft[k]);
|
|
6064
|
+
d.open = !box.classList.contains("hidden"); // 展开态也外置:重绘不再把表单折回去
|
|
5872
6065
|
};
|
|
5873
6066
|
|
|
5874
6067
|
window.pbRegister = async function (taskId, platform) {
|
|
@@ -5881,6 +6074,7 @@ window.pbRegister = async function (taskId, platform) {
|
|
|
5881
6074
|
toast(t("已登记,可直接发章"));
|
|
5882
6075
|
const box = $("pb-reg-" + platform);
|
|
5883
6076
|
if (box) box.classList.add("hidden");
|
|
6077
|
+
if (S.pbRegDraft) delete S.pbRegDraft[taskId + ":" + platform]; // 登记落定,草稿使命完成
|
|
5884
6078
|
} catch (e) { toast(t("登记失败:") + e.message, true); }
|
|
5885
6079
|
S._pbSig = ""; pbKick();
|
|
5886
6080
|
};
|
|
@@ -5938,20 +6132,23 @@ window.pbUploadChapter = async function (taskId, platform) {
|
|
|
5938
6132
|
};
|
|
5939
6133
|
|
|
5940
6134
|
window.pbSendChapter = async function (taskId, platform, relName) {
|
|
5941
|
-
if (!
|
|
6135
|
+
if (!(await uiConfirm(t("将把《{0}》填进平台章节编辑器,填好后由你人工提交。继续?", relName),
|
|
6136
|
+
{ ok: t("开始填稿") }))) return;
|
|
5942
6137
|
try {
|
|
5943
|
-
await pbApiWithQualityOverride(
|
|
6138
|
+
const r = await pbApiWithQualityOverride(
|
|
5944
6139
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/chapter",
|
|
5945
|
-
{ platform, file: relName }, "发章前的质量门禁未通过");
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
'
|
|
5953
|
-
|
|
5954
|
-
|
|
6140
|
+
{ platform, file: relName }, "发章前的质量门禁未通过", taskId);
|
|
6141
|
+
if (!(r && r.gate_reviewed)) {
|
|
6142
|
+
toast(t("正在填写章节…(完成后请在浏览器里确认提交)"));
|
|
6143
|
+
const box = $("pb-ch-" + platform);
|
|
6144
|
+
if (box) {
|
|
6145
|
+
const n = chapterNumOf(relName);
|
|
6146
|
+
box.classList.remove("hidden");
|
|
6147
|
+
box.innerHTML = '<div class="pb-hint">' + esc(t("填稿已完成,请先在浏览器里提交,再点击确认")) +
|
|
6148
|
+
'</div><button class="ghost pb-tool" onclick="pbConfirmChapter(\'' +
|
|
6149
|
+
esc(taskId) + "', '" + platform + "', " + Number(n || 0) +
|
|
6150
|
+
')">' + t("我已在平台提交") + "</button>";
|
|
6151
|
+
}
|
|
5955
6152
|
}
|
|
5956
6153
|
} catch (e) { toast(t("发章失败:") + e.message, true); }
|
|
5957
6154
|
S._pbSig = ""; pbKick();
|
|
@@ -5970,12 +6167,14 @@ window.pbPublishAll = async function (taskId, platform) {
|
|
|
5970
6167
|
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
5971
6168
|
.find((b) => b.platform === platform)) || {};
|
|
5972
6169
|
if (!au.pending) return;
|
|
5973
|
-
if (!
|
|
6170
|
+
if (!(await uiConfirm(t("将从最靠前的待发章节开始填稿(共 {0} 章待发)。本轮只填一章并停在表单页,由你在浏览器里确认提交;提交后点击“我已在平台提交”,再选择下一章。护栏(每日上限/连续失败暂停)生效。继续?", au.pending),
|
|
6171
|
+
{ ok: t("开始发布") }))) return;
|
|
5974
6172
|
try {
|
|
5975
|
-
await pbApiWithQualityOverride(
|
|
6173
|
+
const r = await pbApiWithQualityOverride(
|
|
5976
6174
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
5977
|
-
{ platform }, "批量发布前的质量门禁未通过");
|
|
5978
|
-
|
|
6175
|
+
{ platform }, "批量发布前的质量门禁未通过", taskId);
|
|
6176
|
+
if (!(r && r.gate_reviewed))
|
|
6177
|
+
toast(t("正在填第一章稿——填好后请在浏览器窗口里确认提交,再重新校准"));
|
|
5979
6178
|
} catch (e) { toast(t("自动发布失败:") + e.message, true); }
|
|
5980
6179
|
S._pbSig = ""; pbKick();
|
|
5981
6180
|
};
|
|
@@ -7471,13 +7670,20 @@ async function renderPreview(runId) {
|
|
|
7471
7670
|
rdTabsSync(); // 预览可用性(rdTabAvail 认 #rd-preview 的显隐)随之更新
|
|
7472
7671
|
}
|
|
7473
7672
|
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7673
|
+
/* 复制文本到剪贴板:writeText 只在安全上下文(https / localhost)可用,
|
|
7674
|
+
* 用局域网 IP 或远程地址打开页面时 navigator.clipboard 是 undefined 或直接
|
|
7675
|
+
* 拒绝——必须回退 execCommand,否则按钮点了、剪贴板却是空的。done=真实
|
|
7676
|
+
* 复制成功后的回调;失败由 fallbackCopy 统一弹错误提示。 */
|
|
7677
|
+
window.copyText = function (t, done) {
|
|
7678
|
+
const ok = () => { if (done) done(); };
|
|
7477
7679
|
try {
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7680
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
7681
|
+
// 未处理的 promise 拒绝会以控制台错误冒出来(测试断言 0 错误会被它打爆)
|
|
7682
|
+
navigator.clipboard.writeText(t).then(ok, () => fallbackCopy(t, ok));
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
} catch (e) { /* 落到 execCommand 兜底 */ }
|
|
7686
|
+
fallbackCopy(t, ok);
|
|
7481
7687
|
};
|
|
7482
7688
|
|
|
7483
7689
|
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
@@ -8081,6 +8287,23 @@ async function cancelRun() {
|
|
|
8081
8287
|
toast(r && r.ok ? t("已强制终止运行") : t("该运行已结束,无需取消"));
|
|
8082
8288
|
}
|
|
8083
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
|
+
|
|
8084
8307
|
/* ---------------------------------------------------- 运行中指挥(消息信箱) */
|
|
8085
8308
|
/* 用户在详情页往运行中的任务「递话」:文字 + 附件(截图/文件)。
|
|
8086
8309
|
* 无头 CLI 插不进正在跑的进程,指令在下一次步骤下达前由后端 drain 注入。
|
|
@@ -8785,6 +9008,7 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8785
9008
|
const duration = runDurationSeconds(run);
|
|
8786
9009
|
const etaText = runEtaText(run);
|
|
8787
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;
|
|
8788
9012
|
const actions = [];
|
|
8789
9013
|
if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
|
|
8790
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>");
|
|
@@ -8807,10 +9031,63 @@ function renderDetailOverview(run, runs, task, aggregate) {
|
|
|
8807
9031
|
(etaText ? '<span class="rd-eta">' + esc(etaText) + '</span>' : "") +
|
|
8808
9032
|
'</div>' +
|
|
8809
9033
|
(lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
|
|
9034
|
+
(contract ? contractOverviewHtml(contract) : "") +
|
|
8810
9035
|
(actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
|
|
8811
9036
|
box.classList.remove("hidden");
|
|
8812
9037
|
}
|
|
8813
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
|
+
|
|
8814
9091
|
/* 对话条三件套:与任务参数双向同步(2026-09-21 用户反馈「这里面也需要同步改」)。
|
|
8815
9092
|
* 回填自当前任务;改动即写任务(空闲态),发送 /chat 前再兜底同步一次。 */
|
|
8816
9093
|
let rdPrefsTaskId = "";
|
|
@@ -9187,8 +9464,7 @@ function bindChat() {
|
|
|
9187
9464
|
if (!it) return;
|
|
9188
9465
|
const txt = it.kind === "user" ? String(it.text || "") : chatCleanText(it.text);
|
|
9189
9466
|
if (act.dataset.cact === "copy") {
|
|
9190
|
-
copyText(txt);
|
|
9191
|
-
toast(t("已复制"));
|
|
9467
|
+
copyText(txt, () => toast(t("已复制")));
|
|
9192
9468
|
} else if (act.dataset.cact === "edit") {
|
|
9193
9469
|
const ta = $("rd-chat-input");
|
|
9194
9470
|
if (ta && txt) {
|
|
@@ -13328,7 +13604,8 @@ async function scanPorts() {
|
|
|
13328
13604
|
}
|
|
13329
13605
|
|
|
13330
13606
|
async function closePort(port) {
|
|
13331
|
-
if (!
|
|
13607
|
+
if (!(await uiConfirm(t("结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)").replace("%1", port),
|
|
13608
|
+
{ ok: t("结束进程") }))) return;
|
|
13332
13609
|
try {
|
|
13333
13610
|
const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
|
|
13334
13611
|
toast(r.message || (r.ok ? t("已关闭") : t("未能关闭")), !r.ok);
|
|
@@ -13643,7 +13920,7 @@ function setLangBtn(lang) {
|
|
|
13643
13920
|
if (getLang() === lang) return;
|
|
13644
13921
|
setLang(lang);
|
|
13645
13922
|
// 失效全部渲染签名,让下一次 render() 把所有动态内容重画一遍
|
|
13646
|
-
S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.mgmt = {};
|
|
13923
|
+
S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.runDetailSig = ""; S.mgmt = {};
|
|
13647
13924
|
syncLangMode();
|
|
13648
13925
|
// 浏览器标签标题
|
|
13649
13926
|
document.title = codebeeDocumentTitle(lang);
|
|
@@ -14488,9 +14765,14 @@ function copyConnUrl() {
|
|
|
14488
14765
|
function fallbackCopy(text, done) {
|
|
14489
14766
|
const ta = document.createElement("textarea");
|
|
14490
14767
|
ta.value = text;
|
|
14768
|
+
ta.style.position = "fixed";
|
|
14769
|
+
ta.style.opacity = "0"; // 藏起来但不 display:none(后者 select 不到)
|
|
14491
14770
|
document.body.appendChild(ta);
|
|
14492
14771
|
ta.select();
|
|
14493
|
-
try {
|
|
14772
|
+
try {
|
|
14773
|
+
if (!document.execCommand("copy")) throw new Error("copy rejected");
|
|
14774
|
+
if (done) done();
|
|
14775
|
+
} catch (e) { toast(t("复制失败,请手动长按/右键复制"), true); }
|
|
14494
14776
|
ta.remove();
|
|
14495
14777
|
}
|
|
14496
14778
|
|
|
@@ -14548,7 +14830,7 @@ window.exportBackup = async function () {
|
|
|
14548
14830
|
: '<div><span class="bad">' + esc(t("☁ 远程推送失败:") + (remote.error || "")) + "</span></div>"))
|
|
14549
14831
|
+ (ext.length ? '<div><span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
|
|
14550
14832
|
+ esc(ext.join("、")) + "</span></div>" : "")
|
|
14551
|
-
+ '<button class="ghost small" onclick="copyText(this)" data-copy="' + esc(r.path) + '">'
|
|
14833
|
+
+ '<button class="ghost small" onclick="copyText(this.getAttribute(\'data-copy\'))" data-copy="' + esc(r.path) + '">'
|
|
14552
14834
|
+ esc(t("复制路径")) + "</button>";
|
|
14553
14835
|
} catch (e) {
|
|
14554
14836
|
$("bk-result").innerHTML = '<span class="bad">' + esc(e.message || String(e)) + "</span>";
|
|
@@ -14623,12 +14905,10 @@ async function pullRemoteBackup(name) {
|
|
|
14623
14905
|
if (typeof inspectBackup === "function") inspectBackup(); // 直接进预览向导
|
|
14624
14906
|
}
|
|
14625
14907
|
|
|
14626
|
-
/*
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
catch (e) { fallbackCopy(text, () => toast(t("已复制"))); }
|
|
14631
|
-
};
|
|
14908
|
+
/* ⚠️ 不要在这里再定义 window.copyText——文件前部已有一个字符串版实现
|
|
14909
|
+
* (含 execCommand 兜底 + 成功回调);曾在文件尾再定义一次元素版(读
|
|
14910
|
+
* data-copy 属性),后定义覆盖前定义,导致所有传字符串的复制入口
|
|
14911
|
+
* (作品简介/对话/工作目录路径)一概 TypeError 静默失灵。 */
|
|
14632
14912
|
|
|
14633
14913
|
window.pickBackupFile = async function () {
|
|
14634
14914
|
const cur = (($("bi-path") || {}).value || "").trim();
|
|
@@ -14764,6 +15044,13 @@ function collapseDrawerIfMobile() {
|
|
|
14764
15044
|
function switchTab(name, shell) {
|
|
14765
15045
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
14766
15046
|
if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
|
|
15047
|
+
// 详情开着时切去别的页:详情一并关掉(它是 runs 子页的住户,name==="runs" 除外)。
|
|
15048
|
+
// 不关会留两个残留:S.detailRunId 让顶栏标题卡在「运行详情」;对话态详情给 main
|
|
15049
|
+
// 挂的 chat-fill(overflow:hidden 锁滚动,见 renderChatNav)留在原地——设置/概览
|
|
15050
|
+
// 等子页整页滚不动(2026-09-30 设置页滚不动实案)。closeRun 里的 renderChatNav
|
|
15051
|
+
// 会看到详情已隐而解锚。此处用旧的 S.mainPage 走 closeRun 归位分支,随后本函数
|
|
15052
|
+
// 再落到用户真正点的目标页。
|
|
15053
|
+
if ((S.detailRunId || S.detailTaskKey) && name !== "runs") closeRun();
|
|
14767
15054
|
// 形态由目标页决定:快捷导航页(概览/运行记录/自动化)走 main——只换主栏内容、
|
|
14768
15055
|
// 左栏任务树原地不动;其余页进设置导航。调用方可用 shell 显式覆盖(如已在设置里
|
|
14769
15056
|
// 点运行详情,仍要留在设置侧栏)。用户主动进设置时左栏一定会换成设置导航。
|
|
@@ -14807,6 +15094,7 @@ function switchTab(name, shell) {
|
|
|
14807
15094
|
|
|
14808
15095
|
/* 退出设置/回到新建任务表单:左栏恢复任务树,主栏回到任务页 */
|
|
14809
15096
|
function exitSettings() {
|
|
15097
|
+
if (S.detailRunId || S.detailTaskKey) closeRun(); // 设置运行列表开着详情就回表单:一并关掉,防 chat-fill 锁滚动与标题残留
|
|
14810
15098
|
S.tab = "tasks";
|
|
14811
15099
|
S.mainPage = ""; // 主栏回到表单:不再是快捷导航页
|
|
14812
15100
|
stopAutoPoll(); // 离开设置视图:自动化页轮询一并停掉
|
|
@@ -15079,6 +15367,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15079
15367
|
? S.detailTaskKey : "");
|
|
15080
15368
|
if (taskId) retryTask(taskId);
|
|
15081
15369
|
});
|
|
15370
|
+
$("btn-resume-now").addEventListener("click", resumeNow);
|
|
15082
15371
|
/* 日志控制台高度:顶边手柄拖拽调整(localStorage 记忆),双击复位为默认弹性高度 */
|
|
15083
15372
|
(() => {
|
|
15084
15373
|
const grip = $("rd-log-grip"), box = $("rd-log");
|