codebee 0.1.71 → 0.1.73
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 +601 -589
- package/LICENSE +21 -21
- package/README.md +508 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +935 -935
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1673
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -190
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/evalbench.py +836 -720
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +8 -8
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +25 -1
- package/app/core/manager.py +2208 -2143
- package/app/core/market.py +90 -4
- package/app/core/market_remote.py +16 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3 -2
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +189 -189
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -138
- package/app/core/pipeline.py +102 -30
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -0
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +396 -367
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/fanqie.py +165 -165
- package/app/core/publish/flow.py +640 -589
- package/app/core/publish/flows-fanqie-calibrated.json +336 -336
- package/app/core/publish/ledger.py +307 -229
- package/app/core/publish/manager.py +1118 -824
- package/app/core/publish/qimao.py +176 -146
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -0
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2142
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -122
- package/app/core/skillpacks/fanqie-novel.md +98 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/core/skills.py +58 -8
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +51 -0
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -1035
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2614
- package/app/main.py +110 -39
- package/app/mcp_server.py +175 -175
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +509 -114
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +41 -2
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +8 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +74 -26
- package/bin/tutti.js +147 -147
- package/package.json +48 -47
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +0 -18
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, _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: {}, 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";
|
|
@@ -167,7 +167,11 @@ function browserRenderTaskContext() {
|
|
|
167
167
|
const verifyIcon = verifyPass === true ? "i-check" : (verifyPass === false ? "i-x" : "i-gauge");
|
|
168
168
|
const verifyText = verifyPass === true ? "验证通过" : (verifyPass === false ? "验证未通过" : "等待验证");
|
|
169
169
|
const command = task.verify_command || "";
|
|
170
|
+
const qualityLabels = { continue_only: "继续打磨(暂不可发布)",
|
|
171
|
+
publish_ready: "可发布,签约检查点待完成", signing_ready: "可申请签约" };
|
|
172
|
+
const qualityStatus = verdict.quality_status && qualityLabels[verdict.quality_status];
|
|
170
173
|
verify.innerHTML = '<div class="browser-verify-line ' + verifyClass + '"><svg class="ico" aria-hidden="true"><use href="#' + verifyIcon + '"></use></svg><span>' + esc(t(verifyText)) + "</span></div>" +
|
|
174
|
+
(qualityStatus ? '<div class="browser-verify-command quality-status">' + esc(t(qualityStatus)) + "</div>" : "") +
|
|
171
175
|
(command ? '<div class="browser-verify-command" title="' + esc(command) + '">' + esc(command) + "</div>" : '<span>' + esc(t("未配置验证命令")) + "</span>");
|
|
172
176
|
const platforms = ["fanqie", "qimao"], ps = (S.pubState && S.pubState.platforms) || {};
|
|
173
177
|
publish.innerHTML = '<div class="browser-publish-platforms">' + platforms.map((platform) => {
|
|
@@ -428,7 +432,7 @@ function flowIconHtml(f) {
|
|
|
428
432
|
}
|
|
429
433
|
|
|
430
434
|
function flowDesc(f) {
|
|
431
|
-
if (f.id === "rank_scan") return t("抓四平台榜 → AI
|
|
435
|
+
if (f.id === "rank_scan") return t("抓四平台榜 → AI 选题洞察·附证据分级");
|
|
432
436
|
if (f.engine === "direct") return t("单智能体直达(快)");
|
|
433
437
|
if (f.engine === "code") return t("实现 → 验证 → 评审");
|
|
434
438
|
// 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
|
|
@@ -1243,7 +1247,12 @@ async function api(path, opts) {
|
|
|
1243
1247
|
}
|
|
1244
1248
|
}
|
|
1245
1249
|
}
|
|
1246
|
-
if (!res.ok)
|
|
1250
|
+
if (!res.ok) {
|
|
1251
|
+
const failure = new Error((data && data.error) || ("HTTP " + res.status));
|
|
1252
|
+
failure.status = res.status;
|
|
1253
|
+
failure.data = data;
|
|
1254
|
+
throw failure;
|
|
1255
|
+
}
|
|
1247
1256
|
const method = String(fetchOpts.method || "GET").toUpperCase();
|
|
1248
1257
|
if (method !== "GET") {
|
|
1249
1258
|
if (String(path).startsWith("/api/catalog/")) S._catalogLoaded = false;
|
|
@@ -1260,6 +1269,54 @@ async function api(path, opts) {
|
|
|
1260
1269
|
}
|
|
1261
1270
|
}
|
|
1262
1271
|
|
|
1272
|
+
/* 质量门禁三选一弹框(替代原生 confirm 的单一「强制放行」出口):
|
|
1273
|
+
* 取消 / ↻ 重新评审 / 强制放行。「重新评审」= 重写未达标章再评(语义同详情页
|
|
1274
|
+
* 「↻ 重写未达标章」按钮,走 retry 的未达标分支),仅 serial 任务且非运行中
|
|
1275
|
+
* 才给(运行中后端 retry 也会拒)。返回 "review" | "force" | null(取消)。 */
|
|
1276
|
+
async function pbGateChoice(err, promptText, taskId) {
|
|
1277
|
+
const tk = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
1278
|
+
const canReview = !!(tk && tk.serial &&
|
|
1279
|
+
tk.status !== "running" && tk.status !== "queued");
|
|
1280
|
+
const msg = (promptText || t("质量门禁未通过")) + "\n\n" + ((err && err.message) || "");
|
|
1281
|
+
const v = await _askOpen({
|
|
1282
|
+
title: t("质量门禁未通过"),
|
|
1283
|
+
bodyHtml: '<div class="ask-msg">' + esc(msg) + "</div>" +
|
|
1284
|
+
(canReview ? '<div class="ask-msg">' +
|
|
1285
|
+
esc(t("「重新评审」只重写未达标章节并重新评审(已过线章节沿用成稿与评分),跑完后再来发布。")) + "</div>" : ""),
|
|
1286
|
+
okText: t("强制放行…"),
|
|
1287
|
+
extra: canReview ? [{ text: "↻ " + t("重新评审"), value: "review" }] : [],
|
|
1288
|
+
onOpen: () => $("ask-yes").focus(),
|
|
1289
|
+
});
|
|
1290
|
+
return v === "review" ? "review" : (v === true ? "force" : null);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async function pbApiWithQualityOverride(path, body, prompt, taskId) {
|
|
1294
|
+
try {
|
|
1295
|
+
return await api(path, { method: "POST", body: JSON.stringify(body) });
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
const gate = e && e.data && e.data.quality_gate;
|
|
1298
|
+
if (!gate) throw e;
|
|
1299
|
+
const pick = await pbGateChoice(e, prompt, taskId);
|
|
1300
|
+
if (pick === "review") {
|
|
1301
|
+
// 重写未达标章 + 重新评审:不继续本次发布,调用方按 gate_reviewed 跳过
|
|
1302
|
+
// 成功提示;评审完成后用户再点一次发布按钮即可。
|
|
1303
|
+
try {
|
|
1304
|
+
const r = await api("/api/tasks/" + encodeURIComponent(taskId) + "/retry",
|
|
1305
|
+
{ method: "POST" });
|
|
1306
|
+
jumpToRun(r.run_id);
|
|
1307
|
+
toast(t("已发起重新评审(重写未达标章)——评审完成后再来发布"));
|
|
1308
|
+
} catch (e2) { toast(t("发起重新评审失败:") + e2.message, true); }
|
|
1309
|
+
return { gate_reviewed: true };
|
|
1310
|
+
}
|
|
1311
|
+
if (pick !== "force") throw e;
|
|
1312
|
+
const reason = await uiPrompt(t("强制放行:请输入人工复核原因(必填)"),
|
|
1313
|
+
"已人工复核稿件与平台版本");
|
|
1314
|
+
if (!reason) throw new Error(t("未填写人工复核原因,已取消强制放行"));
|
|
1315
|
+
return await api(path, { method: "POST", body: JSON.stringify({ ...body,
|
|
1316
|
+
force: true, force_confirmed: true, force_reason: reason }) });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1263
1320
|
/* 轻提示:3.5s 自动消失 */
|
|
1264
1321
|
function toast(msg, bad) {
|
|
1265
1322
|
let el = $("toast");
|
|
@@ -1297,7 +1354,18 @@ function _askOpen(opts) {
|
|
|
1297
1354
|
_askResolve = resolve;
|
|
1298
1355
|
$("ask-title").textContent = opts.title || t("确认");
|
|
1299
1356
|
$("ask-body").innerHTML = opts.bodyHtml || "";
|
|
1357
|
+
// 三态弹框:extra 按钮插在「取消」左侧,点按以 opts 里的 value 结束
|
|
1358
|
+
// (非布尔,只有用到 extra 的调用方需要判读);开新框先清上一帧残留。
|
|
1300
1359
|
const yes = $("ask-yes");
|
|
1360
|
+
const foot = yes.parentNode;
|
|
1361
|
+
foot.querySelectorAll(".ask-extra").forEach((b) => b.remove());
|
|
1362
|
+
(opts.extra || []).forEach((b) => {
|
|
1363
|
+
const el = document.createElement("button");
|
|
1364
|
+
el.className = (b.cls || "ghost") + " ask-extra";
|
|
1365
|
+
el.textContent = b.text;
|
|
1366
|
+
el.addEventListener("click", () => _askClose(b.value));
|
|
1367
|
+
foot.insertBefore(el, $("ask-no"));
|
|
1368
|
+
});
|
|
1301
1369
|
yes.textContent = opts.okText || t("确定");
|
|
1302
1370
|
yes.className = opts.danger ? "danger" : "primary";
|
|
1303
1371
|
$("ask").classList.remove("hidden");
|
|
@@ -1913,11 +1981,16 @@ function renderProvList() {
|
|
|
1913
1981
|
const box = $("prov-list");
|
|
1914
1982
|
if (!box) return;
|
|
1915
1983
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1916
|
-
const
|
|
1984
|
+
const kwHit = (p) => !kw ||
|
|
1917
1985
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1918
1986
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
1919
1987
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
1920
|
-
(p.base_url || "").toLowerCase().includes(kw)
|
|
1988
|
+
(p.base_url || "").toLowerCase().includes(kw);
|
|
1989
|
+
const view = S.provStatus || "enabled";
|
|
1990
|
+
// 状态过滤默认只看启用(2026-09-30 用户拍板):停用的点「已停用/全部」chip 才现身
|
|
1991
|
+
const stHit = (p) => view === "all" || (view === "off" ? p.enabled === false : p.enabled !== false);
|
|
1992
|
+
const hit = (S.providers || []).filter(kwHit);
|
|
1993
|
+
const provs = hit.filter(stHit);
|
|
1921
1994
|
if (!S.selProv && provs.length) S.selProv = provs[0].id;
|
|
1922
1995
|
if (S.selProv && !provs.some((p) => p.id === S.selProv)) {
|
|
1923
1996
|
S.selProv = provs.length ? provs[0].id : null;
|
|
@@ -1928,6 +2001,13 @@ function renderProvList() {
|
|
|
1928
2001
|
}
|
|
1929
2002
|
const selN = Object.keys(S.selProvs || {}).length;
|
|
1930
2003
|
box.classList.toggle("has-sel", selN > 0);
|
|
2004
|
+
const chips = hit.length ? '<div class="cat-chips prov-status">' +
|
|
2005
|
+
'<button class="cat-chip' + (view === "enabled" ? " active" : "") + '" onclick="setProvStatus(\'enabled\')">' +
|
|
2006
|
+
t("已启用") + " <b>" + hit.filter((p) => p.enabled !== false).length + "</b></button>" +
|
|
2007
|
+
'<button class="cat-chip' + (view === "off" ? " active" : "") + '" onclick="setProvStatus(\'off\')">' +
|
|
2008
|
+
t("已停用") + " <b>" + hit.filter((p) => p.enabled === false).length + "</b></button>" +
|
|
2009
|
+
'<button class="cat-chip' + (view === "all" ? " active" : "") + '" onclick="setProvStatus(\'all\')">' +
|
|
2010
|
+
t("全部") + " <b>" + hit.length + "</b></button></div>" : "";
|
|
1931
2011
|
const batch = selN ? '<div class="prov-batch"><span class="n">' + t("已选 ") + selN + t(" 个") + "</span>" +
|
|
1932
2012
|
'<button class="ghost small" onclick="batchProvOp(\'enable\')">' + t("启用") + '</button>' +
|
|
1933
2013
|
'<button class="ghost small" onclick="batchProvOp(\'disable\')">' + t("停用") + '</button>' +
|
|
@@ -1936,9 +2016,10 @@ function renderProvList() {
|
|
|
1936
2016
|
const allBox = provs.length ? '<label class="prov-all"><input type="checkbox"' +
|
|
1937
2017
|
(selN === provs.length && selN > 0 ? " checked" : "") +
|
|
1938
2018
|
' onchange="toggleAllProvSel(this.checked)">' + t(" 全选") +
|
|
1939
|
-
(kw ? t("(筛选后 ") + provs.length + t(" 个)") : t("(") + provs.length + t(")")) + "</label>" : "";
|
|
1940
|
-
box.innerHTML = batch + allBox + (provs.map((p) => {
|
|
1941
|
-
|
|
2019
|
+
(kw || view !== "all" ? t("(筛选后 ") + provs.length + t(" 个)") : t("(") + provs.length + t(")")) + "</label>" : "";
|
|
2020
|
+
box.innerHTML = chips + batch + allBox + (provs.map((p) => {
|
|
2021
|
+
// 卡片计数只数启用中的模型(列表默认也只展示启用中的,两边口径一致)
|
|
2022
|
+
const n = p.models == null ? null : p.models.filter((m) => !m.hidden && m.enabled !== false).length;
|
|
1942
2023
|
const st = n == null ? t("未获取") : n + t(" 模型");
|
|
1943
2024
|
const off = p.enabled === false;
|
|
1944
2025
|
return '<div class="prov-item' + (p.id === S.selProv ? " active" : "") + (off ? " off" : "") +
|
|
@@ -1956,7 +2037,16 @@ function renderProvList() {
|
|
|
1956
2037
|
}).join("") ||
|
|
1957
2038
|
'<div class="hint" style="padding:8px">' + (kw && (S.providers || []).length
|
|
1958
2039
|
? t("没有匹配「") + esc(S.provFilter) + t("」的供应商。")
|
|
1959
|
-
:
|
|
2040
|
+
: (view === "enabled" && hit.some((p) => p.enabled === false)
|
|
2041
|
+
? t("%1 个供应商已停用——点上方「已停用」查看。").replace("%1", hit.filter((p) => p.enabled === false).length)
|
|
2042
|
+
: t("暂无供应商——点上方「导入」。"))) + "</div>");
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function setProvStatus(v) {
|
|
2046
|
+
S.provStatus = v;
|
|
2047
|
+
S.modelsSig = null;
|
|
2048
|
+
renderProvList(); // 可能收敛 S.selProv,详情跟着换人
|
|
2049
|
+
renderProvDetail();
|
|
1960
2050
|
}
|
|
1961
2051
|
|
|
1962
2052
|
function toggleProvSel(id, on) {
|
|
@@ -1970,11 +2060,13 @@ function toggleAllProvSel(on) {
|
|
|
1970
2060
|
S.selProvs = {};
|
|
1971
2061
|
if (on) {
|
|
1972
2062
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1973
|
-
|
|
2063
|
+
const view = S.provStatus || "enabled";
|
|
2064
|
+
for (const p of (S.providers || []).filter((p) => (!kw ||
|
|
1974
2065
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1975
2066
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
1976
2067
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
1977
|
-
(p.base_url || "").toLowerCase().includes(kw))
|
|
2068
|
+
(p.base_url || "").toLowerCase().includes(kw)) &&
|
|
2069
|
+
(view === "all" || (view === "off" ? p.enabled === false : p.enabled !== false)))) S.selProvs[p.id] = true;
|
|
1978
2070
|
}
|
|
1979
2071
|
S.modelsSig = null;
|
|
1980
2072
|
renderProvList();
|
|
@@ -2081,6 +2173,18 @@ function renderProvDetail() {
|
|
|
2081
2173
|
' value="' + esc(kw) + '" oninput="filterModels(\'' + esc(p.id) + '\', this.value)">' +
|
|
2082
2174
|
'<span class="pm-count" id="pm-count-' + esc(p.id) + '"></span>' +
|
|
2083
2175
|
pmAddHtml(p) + '</div>';
|
|
2176
|
+
// 模型列表状态过滤:默认只看启用(与供应商列表同一套 chip 口径)
|
|
2177
|
+
const vis = (p.models || []).filter((m) => !m.hidden);
|
|
2178
|
+
if (vis.length) {
|
|
2179
|
+
const mview = S.mlistStatus || "enabled";
|
|
2180
|
+
html += '<div class="cat-chips pm-status">' +
|
|
2181
|
+
'<button class="cat-chip' + (mview === "enabled" ? " active" : "") + '" onclick="setModelStatus(\'enabled\')">' +
|
|
2182
|
+
t("已启用") + " <b>" + vis.filter((m) => m.enabled !== false).length + "</b></button>" +
|
|
2183
|
+
'<button class="cat-chip' + (mview === "off" ? " active" : "") + '" onclick="setModelStatus(\'off\')">' +
|
|
2184
|
+
t("已停用") + " <b>" + vis.filter((m) => m.enabled === false).length + "</b></button>" +
|
|
2185
|
+
'<button class="cat-chip' + (mview === "all" ? " active" : "") + '" onclick="setModelStatus(\'all\')">' +
|
|
2186
|
+
t("全部") + " <b>" + vis.length + "</b></button></div>";
|
|
2187
|
+
}
|
|
2084
2188
|
html += '<div id="pm-groups" class="pm-groups' + (selN ? " has-sel" : "") + '">' +
|
|
2085
2189
|
provModelGroupsHtml(p) + "</div>";
|
|
2086
2190
|
if (hidden.length) {
|
|
@@ -2234,7 +2338,9 @@ function wireCapTags(p, excludeProto) {
|
|
|
2234
2338
|
/* 模型分组区 HTML(全量重绘与过滤重绘共用;过滤时暂停拖拽排序) */
|
|
2235
2339
|
function provModelGroupsHtml(p) {
|
|
2236
2340
|
const kw = ((S.modelFilter || {})[p.id] || "").trim().toLowerCase();
|
|
2341
|
+
const mview = S.mlistStatus || "enabled";
|
|
2237
2342
|
const models = (p.models || []).filter((m) => !m.hidden)
|
|
2343
|
+
.filter((m) => mview === "all" || (mview === "off" ? m.enabled === false : m.enabled !== false))
|
|
2238
2344
|
.filter((m) => !kw || (m.name || "").toLowerCase().includes(kw))
|
|
2239
2345
|
.sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
2240
2346
|
const sel = modelSel(p.id);
|
|
@@ -2245,9 +2351,15 @@ function provModelGroupsHtml(p) {
|
|
|
2245
2351
|
}
|
|
2246
2352
|
const gnames = Object.keys(groups).sort();
|
|
2247
2353
|
if (!gnames.length) {
|
|
2248
|
-
|
|
2354
|
+
const offAll = (p.models || []).filter((m) => !m.hidden && m.enabled === false).length;
|
|
2355
|
+
const msg = kw
|
|
2249
2356
|
? t("没有匹配「") + esc(kw) + t("」的模型。")
|
|
2250
|
-
:
|
|
2357
|
+
: mview === "off"
|
|
2358
|
+
? t("没有已停用的模型。")
|
|
2359
|
+
: (mview === "enabled" && offAll
|
|
2360
|
+
? t("%1 个模型已停用——点上方「已停用」查看。").replace("%1", offAll)
|
|
2361
|
+
: t("该供应商没有可用模型(或全部被停用)。"));
|
|
2362
|
+
return '<div class="empty">' + msg + "</div>";
|
|
2251
2363
|
}
|
|
2252
2364
|
let html = "";
|
|
2253
2365
|
for (const g of gnames) {
|
|
@@ -2283,9 +2395,12 @@ function updateModelCount(pid) {
|
|
|
2283
2395
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2284
2396
|
if (!el || !p || !p.models) return;
|
|
2285
2397
|
const kw = (S.modelFilter || {})[pid] || "";
|
|
2286
|
-
const
|
|
2398
|
+
const mview = S.mlistStatus || "enabled";
|
|
2399
|
+
const inView = (m) => !m.hidden &&
|
|
2400
|
+
(mview === "all" || (mview === "off" ? m.enabled === false : m.enabled !== false));
|
|
2401
|
+
const total = p.models.filter(inView).length;
|
|
2287
2402
|
const shown = total && kw.trim()
|
|
2288
|
-
? p.models.filter((m) =>
|
|
2403
|
+
? p.models.filter((m) => inView(m) &&
|
|
2289
2404
|
(m.name || "").toLowerCase().includes(kw.trim().toLowerCase())).length
|
|
2290
2405
|
: total;
|
|
2291
2406
|
el.textContent = kw.trim() ? (shown + " / " + total + t(" 个模型")) : (total + t(" 个模型"));
|
|
@@ -2484,9 +2599,11 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2484
2599
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2485
2600
|
if (!p) return;
|
|
2486
2601
|
const kw = ((S.modelFilter || {})[pid] || "").trim().toLowerCase();
|
|
2602
|
+
const mview = S.mlistStatus || "enabled";
|
|
2487
2603
|
const s = modelSelSet(pid);
|
|
2488
2604
|
for (const m of p.models || []) {
|
|
2489
2605
|
if (m.hidden) continue;
|
|
2606
|
+
if (mview !== "all" && (mview === "off" ? m.enabled !== false : m.enabled === false)) continue;
|
|
2490
2607
|
if ((m.protocol || p.protocol || t("其他")) !== group) continue;
|
|
2491
2608
|
if (kw && !(m.name || "").toLowerCase().includes(kw)) continue; // 过滤时只作用于可见行
|
|
2492
2609
|
if (on) s[m.name] = true; else delete s[m.name];
|
|
@@ -2495,6 +2612,13 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2495
2612
|
renderProvDetail();
|
|
2496
2613
|
}
|
|
2497
2614
|
|
|
2615
|
+
/* 模型列表状态过滤 chip:默认只看启用 */
|
|
2616
|
+
function setModelStatus(v) {
|
|
2617
|
+
S.mlistStatus = v;
|
|
2618
|
+
S.modelsSig = null;
|
|
2619
|
+
renderProvDetail();
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2498
2622
|
function clearModelSel(pid) {
|
|
2499
2623
|
if (S.selModels) delete S.selModels[pid];
|
|
2500
2624
|
S.modelsSig = null;
|
|
@@ -3368,7 +3492,8 @@ async function archiveTask(id, archived) {
|
|
|
3368
3492
|
S.sideSig = "";
|
|
3369
3493
|
}
|
|
3370
3494
|
render();
|
|
3371
|
-
|
|
3495
|
+
return true;
|
|
3496
|
+
} catch (e) { toast(t("操作失败:") + e.message, true); return false; }
|
|
3372
3497
|
poll();
|
|
3373
3498
|
}
|
|
3374
3499
|
|
|
@@ -3982,7 +4107,14 @@ async function removeSideDir(ids, dir) {
|
|
|
3982
4107
|
.replace("%1", ids.length),
|
|
3983
4108
|
{ ok: t("移除") });
|
|
3984
4109
|
if (!ok) return;
|
|
3985
|
-
|
|
4110
|
+
// 有归档失败就不能藏文件夹:藏是 localStorage 假成功,服务端任务没归档,
|
|
4111
|
+
// 手机端等其他设备照样看得到这批任务(服务重启窗口实测案)。留文件夹让用户重试。
|
|
4112
|
+
let failed = 0;
|
|
4113
|
+
for (const id of ids) if (!await archiveTask(id, true)) failed++;
|
|
4114
|
+
if (failed) {
|
|
4115
|
+
toast(t("有 {0} 个任务归档失败,文件夹先保留;任务与文件都没动,稍后重试即可").replace("{0}", failed), true);
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
3986
4118
|
if (dir) hideSideDir(dir);
|
|
3987
4119
|
}
|
|
3988
4120
|
|
|
@@ -4385,6 +4517,7 @@ window.sideOpenTask = function (key) {
|
|
|
4385
4517
|
S.detailRunId = null;
|
|
4386
4518
|
S.focusStep = 0;
|
|
4387
4519
|
S.taskSig = "";
|
|
4520
|
+
S.runDetailSig = ""; // 详情在任务态/运行态间共用同一套 DOM:切走必须作废运行态签名
|
|
4388
4521
|
rdTabReset();
|
|
4389
4522
|
if (!S.histJump && typeof histPush === "function") histPush({ m: "main", tab: "run-detail" });
|
|
4390
4523
|
showDetailInMain();
|
|
@@ -4394,7 +4527,8 @@ window.sideOpenTask = function (key) {
|
|
|
4394
4527
|
detailSideReset(); // 换详情目标:任务级 side 缓存作废,等首拉
|
|
4395
4528
|
const olderRunsButton = $("rd-load-earlier");
|
|
4396
4529
|
if (olderRunsButton) olderRunsButton.hidden = true;
|
|
4397
|
-
S.
|
|
4530
|
+
S.rdHiveFirst = true; // 侧栏点开任务详情:首拉后默认落「蜂巢」(用户 2026-09-29)
|
|
4531
|
+
S.rdTab = null;
|
|
4398
4532
|
S.rdTabSig = "";
|
|
4399
4533
|
applyRdTabs();
|
|
4400
4534
|
syncInspectorVis(); // 详情已铺开:检查器让位(选中保留,返回列表自动滑回)
|
|
@@ -4621,18 +4755,33 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4621
4755
|
if (!runs.length) return;
|
|
4622
4756
|
// 消息数/未消费数也入签名:新指令或被 drain 后重画指挥区;步骤状态入签名:
|
|
4623
4757
|
// 取消收尾把僵尸步骤落成「已取消」时要立即重画,不等条数变化;
|
|
4624
|
-
// 作品信息状态入签名:后台一键生成 running→done
|
|
4758
|
+
// 作品信息状态入签名:后台一键生成 running→done 要立刻反映到成果区面板。
|
|
4759
|
+
// 续写批次签名盯的是链上根任务的 book_meta(都是用第一个)+ 本任务封面状态
|
|
4625
4760
|
const bmTask = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
4761
|
+
const bmRoot = bmRootTask(bmTask || {});
|
|
4626
4762
|
// 自动续跑退避相位入签名:预定启动时刻过了之后 chip 翻为「正在启动」
|
|
4627
4763
|
const lr0 = runs[0] || {};
|
|
4628
4764
|
const resumePending = (lr0.resume_enqueue_at &&
|
|
4629
4765
|
Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
|
|
4766
|
+
// cost_usd/tokens 故意不入签名:运行中每次记账都在变,入签名会把整帧
|
|
4767
|
+
// 重绘变回「几秒一闪」;步骤状态翻转自然带上新数字,任务累计统计另有
|
|
4768
|
+
// detailSide 通道(2s 节流)保实时,两者都够新鲜。
|
|
4769
|
+
// 消息数/verdict 必须归一化再入签名:loadTaskRuns 每轮把 state 的摘要
|
|
4770
|
+
// 投影(message_count 等)assign 进缓存 run,下一轮干净 run 又洗掉这些
|
|
4771
|
+
// 键——裸取字段会在 null↔0 / 全量↔精简 verdict 间来回摆,每 8s 必重画
|
|
4772
|
+
// 一帧(2026-09-30 任务页闪烁实案)。归一后两种形状取值一致。
|
|
4773
|
+
const msgN = (r) => r.message_count != null ? Number(r.message_count)
|
|
4774
|
+
: (r.messages || []).length;
|
|
4775
|
+
const msgP = (r) => r.pending_message_count != null ? Number(r.pending_message_count)
|
|
4776
|
+
: (r.messages || []).filter((m) => !m.consumed).length;
|
|
4777
|
+
const vcmp = (r) => { const v = r.verdict || null;
|
|
4778
|
+
return v ? JSON.stringify([v.pass, v.publishable, v.verify_pass, v.review_pass]) : null; };
|
|
4630
4779
|
const sig = JSON.stringify(runs.map((r) => [r.id, r.status, (r.steps || []).length,
|
|
4631
4780
|
(r.steps || []).map((s) => s.status).join(""),
|
|
4632
|
-
(r
|
|
4633
|
-
r.
|
|
4634
|
-
|
|
4635
|
-
|
|
4781
|
+
msgN(r), msgP(r),
|
|
4782
|
+
r.summary, r.error, vcmp(r)])
|
|
4783
|
+
.concat([JSON.stringify([(bmRoot || {}).book_meta || null,
|
|
4784
|
+
(bmTask || {}).cover_gen || null]), resumePending]));
|
|
4636
4785
|
if (sig === S.taskSig) return;
|
|
4637
4786
|
S.taskSig = sig;
|
|
4638
4787
|
const latest = runs[0]; // runs 新→旧
|
|
@@ -4879,6 +5028,7 @@ async function openRun(id, pinTab) {
|
|
|
4879
5028
|
_taskStepRenderToken++;
|
|
4880
5029
|
S.detailRunId = id;
|
|
4881
5030
|
S.detailTaskKey = null;
|
|
5031
|
+
S.runDetailSig = ""; // 新目标首帧必画;也兜住「任务态改过 DOM 后回到本运行」的串台
|
|
4882
5032
|
renderPageCrumb(); // 标题「运行详情」;必须在赋值后画——showDetailInMain 先于本函数跑,那边画不到
|
|
4883
5033
|
rdTabReset(pinTab || null); // sideOpenRun 带步骤号时钉住步骤分区
|
|
4884
5034
|
document.querySelector("#sub-runs .panel:first-child").classList.add("hidden");
|
|
@@ -4893,6 +5043,7 @@ function closeRun() {
|
|
|
4893
5043
|
S.detailRunId = null;
|
|
4894
5044
|
S.detailTaskKey = null;
|
|
4895
5045
|
S.taskSig = "";
|
|
5046
|
+
S.runDetailSig = "";
|
|
4896
5047
|
S.focusStep = 0;
|
|
4897
5048
|
rdTabReset();
|
|
4898
5049
|
stopLogLive();
|
|
@@ -5022,12 +5173,14 @@ function renderChatNav() {
|
|
|
5022
5173
|
window.rdChatNavGo = function (tab) {
|
|
5023
5174
|
S.rdTab = tab;
|
|
5024
5175
|
S.rdTabPin = true; // 用户主动去看其它分区:别被自动选卡拽回对话
|
|
5176
|
+
S.rdHiveFirst = false;
|
|
5025
5177
|
applyRdTabs();
|
|
5026
5178
|
};
|
|
5027
5179
|
window.rdChatNavBack = function () {
|
|
5028
5180
|
S.rdTab = "chat";
|
|
5029
5181
|
S.rdTabPin = false;
|
|
5030
5182
|
S.rdTabSig = "";
|
|
5183
|
+
S.rdHiveFirst = false;
|
|
5031
5184
|
applyRdTabs();
|
|
5032
5185
|
};
|
|
5033
5186
|
|
|
@@ -5061,20 +5214,26 @@ function rdTabsSync(ctx) {
|
|
|
5061
5214
|
S.rdTabSig = sig;
|
|
5062
5215
|
const avail = rdTabAvail();
|
|
5063
5216
|
const direct = chatAvail && chatEngineIsDirect(S.lastRun);
|
|
5064
|
-
// 所有任务的主问题都是「继续聊什么、各智能体输出了什么」——默认把
|
|
5065
|
-
// 对话放在第一视线;蜂巢/步骤/成果保留为侧边工作台入口。
|
|
5066
|
-
// 没跑到终态时成果分区是空的,避免打开详情先看到白板。
|
|
5067
5217
|
const finishing = !c.running && ["done", "failed", "cancelled", "timeout"].includes(c.status);
|
|
5068
|
-
|
|
5218
|
+
// 侧栏点开任务详情默认落「蜂巢」工作台(用户 2026-09-29):一次性偏好,
|
|
5219
|
+
// sideOpenTask 置位、蜂巢可用即兑现并钉住(后续状态翻转不再拽走);
|
|
5220
|
+
// ctx 明确 steps=0(纯聊天无蜂巢可看)或手选页签/rdTabReset 时放弃。
|
|
5221
|
+
// 兑现前先留在手上的页签,不让对话抢跑——其余选卡优先级不变。
|
|
5222
|
+
const hivePending = S.rdHiveFirst;
|
|
5223
|
+
const hiveReady = hivePending && avail.hive;
|
|
5224
|
+
if (hiveReady || (hivePending && c.steps === 0)) S.rdHiveFirst = false;
|
|
5225
|
+
S.rdTab = (hiveReady ? "hive" : null)
|
|
5226
|
+
|| (hivePending && S.rdTab && avail[S.rdTab] ? S.rdTab : null)
|
|
5227
|
+
|| (direct && avail.chat ? "chat" : null)
|
|
5069
5228
|
// 跑出了能打开的网页成品:第一视线给「预览」——这正是用户做完一个
|
|
5070
5229
|
// 前端任务最想看到的(借鉴对话式编程产品:跑完先看东西跑起来)。
|
|
5071
|
-
// 对话与待裁决的版本仍排在它前面:那两个是"要用户说话/动手"。
|
|
5072
5230
|
|| (finishing && avail.preview ? "preview" : null)
|
|
5073
5231
|
|| (avail.hive ? "hive" : null)
|
|
5074
5232
|
|| (avail.chat ? "chat" : null)
|
|
5075
|
-
|| (c.running ?
|
|
5233
|
+
|| (c.running ? "steps"
|
|
5076
5234
|
: (c.gitState === "isolated" && avail.git ? "git"
|
|
5077
5235
|
: (c.hasResult && finishing ? "result" : "steps")));
|
|
5236
|
+
if (hiveReady) S.rdTabPin = true;
|
|
5078
5237
|
}
|
|
5079
5238
|
}
|
|
5080
5239
|
applyRdTabs();
|
|
@@ -5098,6 +5257,7 @@ function rdTabReset(pin) {
|
|
|
5098
5257
|
S.rdTab = pin || null;
|
|
5099
5258
|
S.rdTabSig = "";
|
|
5100
5259
|
S.rdTabPin = !!pin;
|
|
5260
|
+
S.rdHiveFirst = false;
|
|
5101
5261
|
S._rdCtx = {};
|
|
5102
5262
|
S._rdHeavy = { runId: "", report: false, artifacts: false, preview: false };
|
|
5103
5263
|
S._rdDetailToken = (S._rdDetailToken || 0) + 1;
|
|
@@ -5231,6 +5391,15 @@ async function renderRunDetail() {
|
|
|
5231
5391
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5232
5392
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
5233
5393
|
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
5394
|
+
// 签名守卫(模型页/检查器同款):数据没变不重绘。详情区是全页最重的
|
|
5395
|
+
// 渲染面,此前 SSE 推送+轮询每 8s 无差别整块重写,是「页面一闪一闪、
|
|
5396
|
+
// 发布表单填一半被清空」的总根源(2026-09-30 用户实案)。运行中 ETA
|
|
5397
|
+
// 随墙钟走,签名带 1 分钟桶让「预计剩余」每分钟翻新,不必跟轮询同频。
|
|
5398
|
+
const etaBucket = (run.status === "running" || run.status === "queued")
|
|
5399
|
+
? Math.floor(Date.now() / 60000) : 0;
|
|
5400
|
+
const rdSig = JSON.stringify([run, rcTask || null, etaBucket]);
|
|
5401
|
+
if (rdSig === S.runDetailSig) return;
|
|
5402
|
+
S.runDetailSig = rdSig;
|
|
5234
5403
|
// 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
|
|
5235
5404
|
// 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
|
|
5236
5405
|
syncArchBtn(rcTask, active);
|
|
@@ -5420,39 +5589,78 @@ function bmStatusChip(st) {
|
|
|
5420
5589
|
return m ? '<span class="bm-st ' + m[0] + '">' + (m[1] === "生成中" ? "● " : "") + t(m[1]) + "</span>" : "";
|
|
5421
5590
|
}
|
|
5422
5591
|
|
|
5423
|
-
/* 作品信息 TAB
|
|
5424
|
-
* 续写批次(serial.
|
|
5425
|
-
*
|
|
5592
|
+
/* 作品信息 TAB:所有连载任务都展示(一本书一份开书资料,都是用第一个)。
|
|
5593
|
+
* 续写批次(serial.continues 指向首批)只读沿用根任务的资料,生成/重生成回
|
|
5594
|
+
* 首批任务做——面板上给指路入口。非连载任务不渲染。 */
|
|
5426
5595
|
function bmNeedsPanel(task) {
|
|
5427
|
-
|
|
5428
|
-
if (!s) return false;
|
|
5429
|
-
try { return intOf(s.start_chapter, 1) <= 1; } catch (e) { return true; }
|
|
5596
|
+
return !!((task || {}).serial);
|
|
5430
5597
|
}
|
|
5431
5598
|
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5599
|
+
/* 连载链根任务:沿 serial.continues 上溯(环/断链停在当前)。状态里找不到
|
|
5600
|
+
* 父任务(已删除等)返回当前任务,面板退化为自身数据。 */
|
|
5601
|
+
function bmRootTask(task) {
|
|
5602
|
+
let cur = task || null;
|
|
5603
|
+
const seen = {};
|
|
5604
|
+
while (cur && ((cur.serial || {}).continues) && !seen[cur.id]) {
|
|
5605
|
+
seen[cur.id] = 1;
|
|
5606
|
+
const parent = (((S.state || {}).tasks || [])
|
|
5607
|
+
.find((x) => x.id === cur.serial.continues)) || null;
|
|
5608
|
+
if (!parent) break;
|
|
5609
|
+
cur = parent;
|
|
5610
|
+
}
|
|
5611
|
+
return cur || task;
|
|
5435
5612
|
}
|
|
5436
5613
|
|
|
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
|
+
|
|
5437
5620
|
function renderBookMetaPanel(task) {
|
|
5438
5621
|
const box = $("rd-bookmeta");
|
|
5439
5622
|
if (!box || !(task || {}).id || !bmNeedsPanel(task)) {
|
|
5440
5623
|
if (box) box.classList.add("hidden");
|
|
5441
5624
|
return;
|
|
5442
5625
|
}
|
|
5443
|
-
|
|
5626
|
+
// 输入/展开态保护(检查器 insp-msg-input 同款纪律):面板里有输入焦点
|
|
5627
|
+
//(正在填登记表单)或章节清单正展开时跳过本帧整块重绘——innerHTML
|
|
5628
|
+
// 重建会把没提交的字清掉、把展开列表折回去。轮询链继续转,blur/收起
|
|
5629
|
+
// 后下一轮数据变化自然跟上;登记草稿另有 S.pbRegDraft 兜底回填。
|
|
5630
|
+
const ae = document.activeElement;
|
|
5631
|
+
if ((ae && box.contains(ae) &&
|
|
5632
|
+
(ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) ||
|
|
5633
|
+
box.querySelector(".pb-chapters:not(.hidden)")) {
|
|
5634
|
+
pbSyncState(task);
|
|
5635
|
+
return;
|
|
5636
|
+
}
|
|
5637
|
+
const src = bmRootTask(task); // 都是用第一个:开书资料读链上根任务
|
|
5638
|
+
const inherited = src.id !== task.id;
|
|
5639
|
+
const bm = src.book_meta || {};
|
|
5444
5640
|
const taskBusy = task.status === "running" || task.status === "queued";
|
|
5445
5641
|
let html = '<div class="bm-head"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5446
5642
|
'<span class="sec-title">' + t("作品信息") + "</span>" +
|
|
5643
|
+
(inherited ? '<span class="bm-inherit" title="' + esc(src.title || src.id) + '">' +
|
|
5644
|
+
esc(t("继承自首批") + "《" + (src.title || src.id) + "》") + "</span>" : "") +
|
|
5447
5645
|
'<span class="hint">' + esc(t("按发布平台生成建书表单资料,逐字段复制过去")) + "</span>" +
|
|
5448
5646
|
'<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>' +
|
|
5449
5647
|
'<span class="flex1"></span>';
|
|
5450
5648
|
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>";
|
|
5451
5653
|
html += "</div>";
|
|
5452
5654
|
if (!bm.fanqie && !bm.qimao && !taskBusy) {
|
|
5453
|
-
|
|
5454
|
-
'<div><
|
|
5455
|
-
|
|
5655
|
+
if (inherited) {
|
|
5656
|
+
html += '<div class="bm-guide"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
|
|
5657
|
+
'<div><b>' + esc(t("首批任务还没生成开书资料")) + "</b>" +
|
|
5658
|
+
"<p>" + esc(t("开书资料属于这本书,在首批任务上生成后整条连载链都能看到。")) + "</p></div></div>";
|
|
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
|
+
}
|
|
5456
5664
|
}
|
|
5457
5665
|
html += '<div class="bm-cards">' + BOOKMETA_PLATFORMS.map((p) => {
|
|
5458
5666
|
const entry = bm[p.id] || {};
|
|
@@ -5482,7 +5690,9 @@ function renderBookMetaPanel(task) {
|
|
|
5482
5690
|
body = '<div class="bm-empty">' + esc(t("尚未生成")) + "</div>";
|
|
5483
5691
|
}
|
|
5484
5692
|
let action;
|
|
5485
|
-
if (
|
|
5693
|
+
if (inherited) {
|
|
5694
|
+
action = ""; // 续写批次只读:生成/重生成集中在头部「回首批任务」入口
|
|
5695
|
+
} else if (st === "running") {
|
|
5486
5696
|
action = '<button class="ghost" disabled><svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + t("生成中…") + "</button>";
|
|
5487
5697
|
} else if (st === "done") {
|
|
5488
5698
|
action = '<button class="ghost" onclick="bmGen(\'' + esc(task.id) + "', '" + p.id + '\')" title="' + esc(t("重新生成会覆盖现有内容")) + '">' +
|
|
@@ -5502,8 +5712,11 @@ function renderBookMetaPanel(task) {
|
|
|
5502
5712
|
"</div>";
|
|
5503
5713
|
}).join("") + "</div>";
|
|
5504
5714
|
// 封面卡(covergen,借鉴 oh-story 封面图环节):curl 落盘运行目录,
|
|
5505
|
-
// done 后卡片内直接嵌缩略图(/api/runs/<id>/cover
|
|
5506
|
-
|
|
5715
|
+
// done 后卡片内直接嵌缩略图(/api/runs/<id>/cover 专用通道),点开新页看大图。
|
|
5716
|
+
// 封面也是「这本书」的:本任务没生成过就沿用根任务的封面;点生成则落本任务自己的
|
|
5717
|
+
const cgSrc = (task.cover_gen && (task.cover_gen.status || task.cover_gen.run_id)) ? task : src;
|
|
5718
|
+
const cg = cgSrc.cover_gen || {};
|
|
5719
|
+
const cgInherited = cgSrc !== task;
|
|
5507
5720
|
const cgSt = cg.status || "";
|
|
5508
5721
|
let cgBody = "";
|
|
5509
5722
|
if (cgSt === "running") {
|
|
@@ -5516,7 +5729,8 @@ function renderBookMetaPanel(task) {
|
|
|
5516
5729
|
'<img class="bm-cover-img" src="' + esc(urlAuth(covUrl)) + '" alt="cover.png" ' +
|
|
5517
5730
|
'onerror="this.closest(".bm-cover-wrap").classList.add("noimg")"></a>' +
|
|
5518
5731
|
'<span class="bm-cover-fallback">' + esc(t("cover.png 已不在运行目录")) + "</span>" +
|
|
5519
|
-
'<div class="bm-cover-meta">' + esc([cg.model, cg.provider, cg.size
|
|
5732
|
+
'<div class="bm-cover-meta">' + esc([cg.model, cg.provider, cg.size,
|
|
5733
|
+
cgInherited ? t("继承自首批") : ""].filter(Boolean).join(" · ")) + "</div></div>";
|
|
5520
5734
|
} else if (cgSt === "done") {
|
|
5521
5735
|
cgBody = '<div class="bm-empty">' + esc(t("封面已生成,但缺少运行记录归属")) + "</div>";
|
|
5522
5736
|
} else if (cgSt === "failed") {
|
|
@@ -5565,8 +5779,8 @@ window.coverGen = async function (taskId) {
|
|
|
5565
5779
|
};
|
|
5566
5780
|
|
|
5567
5781
|
window.bmCopyBtn = function (btn) {
|
|
5568
|
-
|
|
5569
|
-
toast(t("已复制:") + (btn.dataset.label || ""));
|
|
5782
|
+
// 只在真实复制成功后报「已复制」——剪贴板不可用时 fallbackCopy 会弹失败提示
|
|
5783
|
+
copyText(btn.dataset.text || "", () => toast(t("已复制:") + (btn.dataset.label || "")));
|
|
5570
5784
|
};
|
|
5571
5785
|
|
|
5572
5786
|
/* ---------------- 一键发布(bookmeta 分区内每平台卡的发布行) ----------------
|
|
@@ -5585,6 +5799,9 @@ function pbBlock(task, platform) {
|
|
|
5585
5799
|
const [label, cls] = PB_ST[st] || PB_ST.none;
|
|
5586
5800
|
const books = (S.pubTaskInfo && S.pubTaskInfo.books) || {};
|
|
5587
5801
|
const book = books[platform];
|
|
5802
|
+
// A create-book flow can finish without a remote id. Keep that binding
|
|
5803
|
+
// retryable instead of presenting it as a ready-to-publish book.
|
|
5804
|
+
const bookReady = !!(book && (book.book_id || book.source === "manual"));
|
|
5588
5805
|
const hist = (S.pubTaskInfo && S.pubTaskInfo.history) || [];
|
|
5589
5806
|
const pubCnt = (S.pubTaskInfo && S.pubTaskInfo.published) || {};
|
|
5590
5807
|
// 已发口径:平台实况(点过校准)优先;否则本地台账去重章号数
|
|
@@ -5621,7 +5838,7 @@ function pbBlock(task, platform) {
|
|
|
5621
5838
|
}
|
|
5622
5839
|
btns += '<button class="ghost pb-tool" onclick="pbProbe(\'' + platform + '\')" title="' +
|
|
5623
5840
|
esc(t("dump 平台表单结构(校准自动填表用)")) + '">' + t("探测") + "</button>";
|
|
5624
|
-
if (
|
|
5841
|
+
if (bookReady) {
|
|
5625
5842
|
btns += '<span class="pb-book" title="' + esc(t("已在此平台创建的作品")) + '">' +
|
|
5626
5843
|
esc(t("已建书:") + (book.title || "")) + "</span>";
|
|
5627
5844
|
btns += ' <button class="primary" ' + (busy ? "disabled" : "") +
|
|
@@ -5651,6 +5868,11 @@ function pbBlock(task, platform) {
|
|
|
5651
5868
|
// 人工模式一轮只填一章:等用户在浏览器提交后再发起(连发会导航离开
|
|
5652
5869
|
// 未提交的编辑器丢稿)。message 是下一步指引,不是错误。
|
|
5653
5870
|
btns += '<div class="pb-hint">' + esc(run.message || t("已填好一章,请在浏览器确认提交")) + "</div>";
|
|
5871
|
+
if (run.last_chapter) {
|
|
5872
|
+
btns += '<button class="ghost pb-tool" onclick="pbConfirmChapter(\'' +
|
|
5873
|
+
esc(task.id) + "', '" + platform + "', " + Number(run.last_chapter) +
|
|
5874
|
+
')">' + t("我已在平台提交") + "</button>";
|
|
5875
|
+
}
|
|
5654
5876
|
} else if (run && run.status !== "running") {
|
|
5655
5877
|
const doneLine = run.status === "done"
|
|
5656
5878
|
? t("自动发布完成:") + run.done + "/" + run.total + (run.message ? t("。") + run.message : "")
|
|
@@ -5659,19 +5881,29 @@ function pbBlock(task, platform) {
|
|
|
5659
5881
|
esc(doneLine + t("(") + (run.at || "") + t(")")) + "</div>";
|
|
5660
5882
|
}
|
|
5661
5883
|
} else {
|
|
5884
|
+
if (book && book.title) {
|
|
5885
|
+
btns += '<span class="pb-book pb-err">' + esc(t("建书未确认:") + book.title) + "</span>";
|
|
5886
|
+
}
|
|
5662
5887
|
btns += '<button class="primary" ' + (busy ? "disabled" : "") +
|
|
5663
5888
|
' onclick="pbCreateBook(\'' + esc(task.id) + "', '" + platform + '\')">' + t("创建作品") + "</button>";
|
|
5664
|
-
btns += '<button class="ghost" onclick="pbRegToggle(\'' + platform + '\')" title="' +
|
|
5889
|
+
btns += '<button class="ghost" onclick="pbRegToggle(\'' + esc(task.id) + "','" + platform + '\')" title="' +
|
|
5665
5890
|
esc(t("作品已在平台建好(建书流程没走通或手工建的)?补登记后直接发章,不会在平台新建")) + '">' + t("登记已有作品") + "</button>";
|
|
5666
5891
|
}
|
|
5667
5892
|
let extra = "";
|
|
5668
|
-
if (st === "error" && !
|
|
5893
|
+
if (st === "error" && !bookReady && ps.last_action === "create_book") {
|
|
5669
5894
|
extra += '<div class="pb-hint">' + esc(t("若作品其实已在平台建好,点「登记已有作品」补登记,勿重复创建")) + "</div>";
|
|
5670
5895
|
}
|
|
5671
|
-
if (!
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5896
|
+
if (!bookReady) {
|
|
5897
|
+
// 展开态+草稿值外置(S.pbRegDraft,按任务+平台键):整块重绘不再清空
|
|
5898
|
+
// 用户填了一半的作品名/ID——输入即存,重绘按草稿回填,登记成功才清。
|
|
5899
|
+
const reg = (S.pbRegDraft || {})[task.id + ":" + platform] || {};
|
|
5900
|
+
extra += '<div class="pb-reg' + (reg.open ? "" : " hidden") + '" id="pb-reg-' + platform + '">' +
|
|
5901
|
+
'<input id="pb-reg-title-' + platform + '" value="' + esc(reg.title || "") +
|
|
5902
|
+
'" placeholder="' + esc(t("平台上的作品名(必填)")) +
|
|
5903
|
+
'" oninput="pbRegDraftSave(\'' + esc(task.id) + "','" + platform + '\',\'title\',this.value)">' +
|
|
5904
|
+
'<input id="pb-reg-id-' + platform + '" value="' + esc(reg.id || "") +
|
|
5905
|
+
'" placeholder="' + esc(t("作品ID(选填,管理页URL里有)")) +
|
|
5906
|
+
'" oninput="pbRegDraftSave(\'' + esc(task.id) + "','" + platform + '\',\'id\',this.value)">' +
|
|
5675
5907
|
'<button class="ghost" onclick="pbRegister(\'' + esc(task.id) + "', '" + platform + '\')">' + t("确认登记") + "</button>" +
|
|
5676
5908
|
"</div>";
|
|
5677
5909
|
}
|
|
@@ -5687,18 +5919,27 @@ function pbBlock(task, platform) {
|
|
|
5687
5919
|
let _pbTimer = 0;
|
|
5688
5920
|
function pbSyncState(task) {
|
|
5689
5921
|
clearTimeout(_pbTimer);
|
|
5922
|
+
// 5s 节流折进调度间隔:距上一拉未满 5s 就等满再拉(pbKick 置 0 可立即拉)
|
|
5923
|
+
const delay = Math.max(120, 5000 - (Date.now() - (S.pubFetchAt || 0)));
|
|
5690
5924
|
_pbTimer = setTimeout(async () => {
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5925
|
+
// 自续轮询链:详情区签名守卫上线后,主渲染泵不再每 8s 带跑本面板,
|
|
5926
|
+
// 发布状态自己转——面板可见且仍指向同一任务就 5s 一拉,收起/切走
|
|
5927
|
+
// 自然停,下次 renderBookMetaPanel 重新点火。task 对象可能已老,
|
|
5928
|
+
// 一律按 id 从 S.state 重解析,渲染吃的永远是新数据。
|
|
5929
|
+
const box = $("rd-bookmeta");
|
|
5930
|
+
if (!task || !task.id || !box || box.classList.contains("hidden") ||
|
|
5931
|
+
$("run-detail").classList.contains("hidden")) return;
|
|
5932
|
+
const tk = ((S.state || {}).tasks || []).find((x) => x.id === task.id);
|
|
5933
|
+
if (!tk || ((S.lastRunTask || {}).id || "") !== tk.id) return;
|
|
5934
|
+
S.pubFetchAt = Date.now();
|
|
5694
5935
|
let sig = "";
|
|
5695
5936
|
try {
|
|
5696
5937
|
const v = await api("/api/publish");
|
|
5697
5938
|
S.pubState = v;
|
|
5698
5939
|
sig += JSON.stringify(v.platforms || {});
|
|
5699
|
-
} catch (e) { return; }
|
|
5940
|
+
} catch (e) { pbSyncState(tk); return; }
|
|
5700
5941
|
try {
|
|
5701
|
-
const ti = await api("/api/publish/task/" + encodeURIComponent(
|
|
5942
|
+
const ti = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/history");
|
|
5702
5943
|
S.pubTaskInfo = ti;
|
|
5703
5944
|
sig += "#" + JSON.stringify(ti.books || {}) + "#" + (ti.history || []).length;
|
|
5704
5945
|
// 已发章数自动校准:平台在线且登记在案、超 10 分钟没对过账就静默对一次
|
|
@@ -5713,22 +5954,23 @@ function pbSyncState(task) {
|
|
|
5713
5954
|
if (ps2.status === "connected" && nowMs - at > 600000 &&
|
|
5714
5955
|
nowMs - (S["_pbSyncAt_" + p] || 0) > 600000) {
|
|
5715
5956
|
S["_pbSyncAt_" + p] = nowMs;
|
|
5716
|
-
api("/api/publish/task/" + encodeURIComponent(
|
|
5957
|
+
api("/api/publish/task/" + encodeURIComponent(tk.id) + "/sync-published",
|
|
5717
5958
|
{ method: "POST", body: JSON.stringify({ platform: p }) }).catch(() => {});
|
|
5718
5959
|
}
|
|
5719
5960
|
});
|
|
5720
5961
|
} catch (e) { /* 任务级失败不阻塞平台状态 */ }
|
|
5721
5962
|
try {
|
|
5722
5963
|
// 批量发布视图(待发数/护栏/进度)——进行中时靠本节流轮询自然刷新
|
|
5723
|
-
const au = await api("/api/publish/task/" + encodeURIComponent(
|
|
5964
|
+
const au = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/pending");
|
|
5724
5965
|
S.pubAuto = au;
|
|
5725
5966
|
sig += "@" + JSON.stringify(au.books || {}) + "@" + JSON.stringify(au.running || {});
|
|
5726
5967
|
} catch (e) { /* 视图缺失(老服务)不阻塞 */ }
|
|
5727
|
-
if (sig !== (S._pbSig || "")
|
|
5968
|
+
if (sig !== (S._pbSig || "")) {
|
|
5728
5969
|
S._pbSig = sig;
|
|
5729
|
-
renderBookMetaPanel(
|
|
5970
|
+
renderBookMetaPanel(tk);
|
|
5730
5971
|
} else S._pbSig = sig;
|
|
5731
|
-
|
|
5972
|
+
pbSyncState(tk); // 续链:面板还开着就下一轮 5s 后再来
|
|
5973
|
+
}, delay);
|
|
5732
5974
|
}
|
|
5733
5975
|
|
|
5734
5976
|
window.pbSyncPublished = async function (taskId, platform) {
|
|
@@ -5766,18 +6008,34 @@ window.pbProbe = async function (platform) {
|
|
|
5766
6008
|
};
|
|
5767
6009
|
|
|
5768
6010
|
window.pbCreateBook = async function (taskId, platform) {
|
|
5769
|
-
if (!
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
6011
|
+
if (!(await uiConfirm(t("将用生成的作品信息在平台自动填建书表单。填好后会停在最后一步,由你在浏览器里人工点提交。继续?"),
|
|
6012
|
+
{ ok: t("开始建书") }))) return;
|
|
6013
|
+
try {
|
|
6014
|
+
const r = await pbApiWithQualityOverride(
|
|
6015
|
+
"/api/publish/task/" + encodeURIComponent(taskId) + "/create-book",
|
|
6016
|
+
{ platform }, "创建作品前的质量门禁未通过", taskId);
|
|
6017
|
+
if (!(r && r.gate_reviewed))
|
|
6018
|
+
toast(t("正在自动填写建书表单…(完成后请在浏览器里确认提交)"));
|
|
5774
6019
|
} catch (e) { toast(t("建书失败:") + e.message, true); }
|
|
5775
6020
|
S._pbSig = ""; pbKick();
|
|
5776
6021
|
};
|
|
5777
6022
|
|
|
5778
|
-
|
|
6023
|
+
/* 登记草稿随输随存:重绘回填的写侧(读侧在 pbBlock 模板里 value=/open) */
|
|
6024
|
+
window.pbRegDraftSave = function (taskId, platform, key, val) {
|
|
6025
|
+
S.pbRegDraft = S.pbRegDraft || {};
|
|
6026
|
+
const k = taskId + ":" + platform;
|
|
6027
|
+
const d = S.pbRegDraft[k] = Object.assign({}, S.pbRegDraft[k]);
|
|
6028
|
+
d[key] = String(val == null ? "" : val);
|
|
6029
|
+
};
|
|
6030
|
+
|
|
6031
|
+
window.pbRegToggle = function (taskId, platform) {
|
|
5779
6032
|
const box = $("pb-reg-" + platform);
|
|
5780
|
-
if (box)
|
|
6033
|
+
if (!box) return;
|
|
6034
|
+
box.classList.toggle("hidden");
|
|
6035
|
+
S.pbRegDraft = S.pbRegDraft || {};
|
|
6036
|
+
const k = taskId + ":" + platform;
|
|
6037
|
+
const d = S.pbRegDraft[k] = Object.assign({}, S.pbRegDraft[k]);
|
|
6038
|
+
d.open = !box.classList.contains("hidden"); // 展开态也外置:重绘不再把表单折回去
|
|
5781
6039
|
};
|
|
5782
6040
|
|
|
5783
6041
|
window.pbRegister = async function (taskId, platform) {
|
|
@@ -5790,6 +6048,7 @@ window.pbRegister = async function (taskId, platform) {
|
|
|
5790
6048
|
toast(t("已登记,可直接发章"));
|
|
5791
6049
|
const box = $("pb-reg-" + platform);
|
|
5792
6050
|
if (box) box.classList.add("hidden");
|
|
6051
|
+
if (S.pbRegDraft) delete S.pbRegDraft[taskId + ":" + platform]; // 登记落定,草稿使命完成
|
|
5793
6052
|
} catch (e) { toast(t("登记失败:") + e.message, true); }
|
|
5794
6053
|
S._pbSig = ""; pbKick();
|
|
5795
6054
|
};
|
|
@@ -5847,26 +6106,49 @@ window.pbUploadChapter = async function (taskId, platform) {
|
|
|
5847
6106
|
};
|
|
5848
6107
|
|
|
5849
6108
|
window.pbSendChapter = async function (taskId, platform, relName) {
|
|
5850
|
-
if (!
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
if (
|
|
6109
|
+
if (!(await uiConfirm(t("将把《{0}》填进平台章节编辑器,填好后由你人工提交。继续?", relName),
|
|
6110
|
+
{ ok: t("开始填稿") }))) return;
|
|
6111
|
+
try {
|
|
6112
|
+
const r = await pbApiWithQualityOverride(
|
|
6113
|
+
"/api/publish/task/" + encodeURIComponent(taskId) + "/chapter",
|
|
6114
|
+
{ platform, file: relName }, "发章前的质量门禁未通过", taskId);
|
|
6115
|
+
if (!(r && r.gate_reviewed)) {
|
|
6116
|
+
toast(t("正在填写章节…(完成后请在浏览器里确认提交)"));
|
|
6117
|
+
const box = $("pb-ch-" + platform);
|
|
6118
|
+
if (box) {
|
|
6119
|
+
const n = chapterNumOf(relName);
|
|
6120
|
+
box.classList.remove("hidden");
|
|
6121
|
+
box.innerHTML = '<div class="pb-hint">' + esc(t("填稿已完成,请先在浏览器里提交,再点击确认")) +
|
|
6122
|
+
'</div><button class="ghost pb-tool" onclick="pbConfirmChapter(\'' +
|
|
6123
|
+
esc(taskId) + "', '" + platform + "', " + Number(n || 0) +
|
|
6124
|
+
')">' + t("我已在平台提交") + "</button>";
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
5857
6127
|
} catch (e) { toast(t("发章失败:") + e.message, true); }
|
|
5858
6128
|
S._pbSig = ""; pbKick();
|
|
5859
6129
|
};
|
|
5860
6130
|
|
|
6131
|
+
window.pbConfirmChapter = async function (taskId, platform, chapterNo) {
|
|
6132
|
+
try {
|
|
6133
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/confirm-chapter",
|
|
6134
|
+
{ method: "POST", body: JSON.stringify({ platform, chapter_no: chapterNo }) });
|
|
6135
|
+
toast(t("已确认提交,台账已更新"));
|
|
6136
|
+
} catch (e) { toast(t("确认提交失败:") + e.message, true); }
|
|
6137
|
+
S._pbSig = ""; pbKick();
|
|
6138
|
+
};
|
|
6139
|
+
|
|
5861
6140
|
window.pbPublishAll = async function (taskId, platform) {
|
|
5862
6141
|
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
5863
6142
|
.find((b) => b.platform === platform)) || {};
|
|
5864
6143
|
if (!au.pending) return;
|
|
5865
|
-
if (!
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
6144
|
+
if (!(await uiConfirm(t("将从最靠前的待发章节开始填稿(共 {0} 章待发)。本轮只填一章并停在表单页,由你在浏览器里确认提交;提交后点击“我已在平台提交”,再选择下一章。护栏(每日上限/连续失败暂停)生效。继续?", au.pending),
|
|
6145
|
+
{ ok: t("开始发布") }))) return;
|
|
6146
|
+
try {
|
|
6147
|
+
const r = await pbApiWithQualityOverride(
|
|
6148
|
+
"/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
6149
|
+
{ platform }, "批量发布前的质量门禁未通过", taskId);
|
|
6150
|
+
if (!(r && r.gate_reviewed))
|
|
6151
|
+
toast(t("正在填第一章稿——填好后请在浏览器窗口里确认提交,再重新校准"));
|
|
5870
6152
|
} catch (e) { toast(t("自动发布失败:") + e.message, true); }
|
|
5871
6153
|
S._pbSig = ""; pbKick();
|
|
5872
6154
|
};
|
|
@@ -7362,13 +7644,20 @@ async function renderPreview(runId) {
|
|
|
7362
7644
|
rdTabsSync(); // 预览可用性(rdTabAvail 认 #rd-preview 的显隐)随之更新
|
|
7363
7645
|
}
|
|
7364
7646
|
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7647
|
+
/* 复制文本到剪贴板:writeText 只在安全上下文(https / localhost)可用,
|
|
7648
|
+
* 用局域网 IP 或远程地址打开页面时 navigator.clipboard 是 undefined 或直接
|
|
7649
|
+
* 拒绝——必须回退 execCommand,否则按钮点了、剪贴板却是空的。done=真实
|
|
7650
|
+
* 复制成功后的回调;失败由 fallbackCopy 统一弹错误提示。 */
|
|
7651
|
+
window.copyText = function (t, done) {
|
|
7652
|
+
const ok = () => { if (done) done(); };
|
|
7368
7653
|
try {
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7654
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
7655
|
+
// 未处理的 promise 拒绝会以控制台错误冒出来(测试断言 0 错误会被它打爆)
|
|
7656
|
+
navigator.clipboard.writeText(t).then(ok, () => fallbackCopy(t, ok));
|
|
7657
|
+
return;
|
|
7658
|
+
}
|
|
7659
|
+
} catch (e) { /* 落到 execCommand 兜底 */ }
|
|
7660
|
+
fallbackCopy(t, ok);
|
|
7372
7661
|
};
|
|
7373
7662
|
|
|
7374
7663
|
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
@@ -9078,8 +9367,7 @@ function bindChat() {
|
|
|
9078
9367
|
if (!it) return;
|
|
9079
9368
|
const txt = it.kind === "user" ? String(it.text || "") : chatCleanText(it.text);
|
|
9080
9369
|
if (act.dataset.cact === "copy") {
|
|
9081
|
-
copyText(txt);
|
|
9082
|
-
toast(t("已复制"));
|
|
9370
|
+
copyText(txt, () => toast(t("已复制")));
|
|
9083
9371
|
} else if (act.dataset.cact === "edit") {
|
|
9084
9372
|
const ta = $("rd-chat-input");
|
|
9085
9373
|
if (ta && txt) {
|
|
@@ -10498,14 +10786,30 @@ function renderSkills() {
|
|
|
10498
10786
|
const box = $("skill-packs");
|
|
10499
10787
|
if (!box || !S.skills) return;
|
|
10500
10788
|
const packs = S.skills.packs || [], lessons = S.skills.lessons || [];
|
|
10501
|
-
box.innerHTML = packs.map((p) =>
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10789
|
+
box.innerHTML = packs.map((p) => {
|
|
10790
|
+
const card = p.card;
|
|
10791
|
+
const cardHtml = card
|
|
10792
|
+
? '<div class="facts" style="margin-top:4px">' +
|
|
10793
|
+
'<span class="tag" title="' + esc(t("来源:") + (card.source && card.source.market || "-")) + '">' +
|
|
10794
|
+
t("来源 ") + esc((card.source && card.source.market) || "-") + "</span>" +
|
|
10795
|
+
(card.digest ? '<span class="tag" title="' + t("内容指纹(重装突变对账用)") + '">' +
|
|
10796
|
+
t("指纹 ") + esc(card.digest) + "</span>" : "") +
|
|
10797
|
+
(card.scan ? (card.scan.indexOf("高风险") >= 0
|
|
10798
|
+
? '<span class="tag" style="color:var(--err,#d33)">' + esc(card.scan.split(":")[0]) + "</span>"
|
|
10799
|
+
: '<span class="tag">' + esc(card.scan.split(":")[0]) + "</span>") : "") +
|
|
10800
|
+
"</div>" +
|
|
10801
|
+
(card.declares ? '<div class="note" style="margin-top:2px">📝 ' + t("自述:") +
|
|
10802
|
+
esc(card.declares.slice(0, 120)) + (card.declares.length > 120 ? "…" : "") + "</div>" : "")
|
|
10803
|
+
: "";
|
|
10804
|
+
return '<div class="card"><div class="head"><span class="name">📚 ' + esc(t(p.name)) + "</span>" +
|
|
10805
|
+
'<span class="tag">' + esc((p.scopes || []).map((s) => t(s)).join(" / ")) + "</span>" +
|
|
10806
|
+
(p.enabled ? '<span class="tag ok">' + t("启用中") + "</span>" : '<span class="tag">' + t("已停用") + "</span>") + "</div>" +
|
|
10807
|
+
'<div class="note">' + esc(t(p.note || "")) + "</div>" +
|
|
10808
|
+
'<div class="facts">' + t("正文 ") + "<b>" + p.chars + "</b>" + t(" 字(自动注入该类任务的规划与评审提示词)") + "</div>" +
|
|
10809
|
+
cardHtml +
|
|
10810
|
+
'<div class="ops"><button class="ghost small" onclick="skillPackOp(\'' + esc(p.id) + '\', \'' +
|
|
10811
|
+
(p.enabled ? "disable" : "enable") + '\')">' + (p.enabled ? t("停用") : t("启用")) + "</button></div></div>";
|
|
10812
|
+
}).join("")
|
|
10509
10813
|
|| '<div class="empty">' + t("暂无经验包") + "</div>";
|
|
10510
10814
|
// 分类 chip 过滤:药丸按钮 + 彩色圆点 + 计数,选中态高亮。值 = 分类名,空串 = 全部。
|
|
10511
10815
|
const cats = S.skills.categories || [];
|
|
@@ -11463,6 +11767,7 @@ bindHooksPanel();
|
|
|
11463
11767
|
function zentaoStateTag(st) {
|
|
11464
11768
|
const MAP = {
|
|
11465
11769
|
fixing: ["修复中", "zt-tag-fix"],
|
|
11770
|
+
retry: ["待重试", "zt-tag-warn"],
|
|
11466
11771
|
resolved: ["已解决", "zt-tag-ok"],
|
|
11467
11772
|
transferred: ["已转派", "zt-tag-fix"],
|
|
11468
11773
|
escalated: ["已升级转派", "zt-tag-warn"],
|
|
@@ -11471,6 +11776,7 @@ function zentaoStateTag(st) {
|
|
|
11471
11776
|
resolve_failed: ["回写失败", "zt-tag-warn"],
|
|
11472
11777
|
done_manual: ["待人工确认", "zt-tag-warn"],
|
|
11473
11778
|
need_manual: ["需人工", "zt-tag-warn"],
|
|
11779
|
+
failed: ["修复失败", "zt-tag-err"],
|
|
11474
11780
|
lost: ["记录丢失", "zt-tag-err"],
|
|
11475
11781
|
};
|
|
11476
11782
|
const m = MAP[st] || [st || "?", ""];
|
|
@@ -11537,6 +11843,20 @@ async function zentaoArchiveClaim(bugId, archived) {
|
|
|
11537
11843
|
} catch (e) { toast((e && e.message) || String(e), true); }
|
|
11538
11844
|
}
|
|
11539
11845
|
|
|
11846
|
+
/* 立即处理:不等下轮扫描,手工重触发失败/留人工的修复(后端按状态分流) */
|
|
11847
|
+
async function zentaoRetryClaim(bugId) {
|
|
11848
|
+
try {
|
|
11849
|
+
const r = await api("/api/zentao/claims/retry", { method: "POST",
|
|
11850
|
+
body: JSON.stringify({ bug_id: bugId }) });
|
|
11851
|
+
toast((r && r.message) || t("已重新触发处理"));
|
|
11852
|
+
await loadZentao();
|
|
11853
|
+
} catch (e) { toast((e && e.message) || String(e), true); }
|
|
11854
|
+
}
|
|
11855
|
+
|
|
11856
|
+
/* 这些状态的记录可以「立即处理」(fixing/resolved/transferred/done_manual 不行) */
|
|
11857
|
+
const ZT_RETRYABLE = { retry: 1, failed: 1, escalated: 1, commented: 1,
|
|
11858
|
+
need_manual: 1, merge_failed: 1, resolve_failed: 1, lost: 1 };
|
|
11859
|
+
|
|
11540
11860
|
function zentaoClaimCard(c, archived) {
|
|
11541
11861
|
const tasks = (c.tasks || []).map((tk) =>
|
|
11542
11862
|
"<span>" + t("【") + (tk.side === "frontend" ? t("前端") : t("后端")) + t("】") +
|
|
@@ -11547,11 +11867,15 @@ function zentaoClaimCard(c, archived) {
|
|
|
11547
11867
|
? '<a class="zt-bug-link" href="' + esc(bugUrl) + '" target="_blank" rel="noopener" title="' +
|
|
11548
11868
|
esc(t("在禅道中打开 Bug 详情")) + '">' + name + "</a>"
|
|
11549
11869
|
: name;
|
|
11870
|
+
const retryBtn = (!archived && ZT_RETRYABLE[c.state])
|
|
11871
|
+
? '<button class="ghost small" title="' + esc(t("不等下轮扫描,立即重新触发修复")) +
|
|
11872
|
+
'" onclick="zentaoRetryClaim(\'' + esc(c.bug_id) + '\')">' + t("立即处理") + "</button>"
|
|
11873
|
+
: "";
|
|
11550
11874
|
const act = archived
|
|
11551
11875
|
? '<button class="ghost small" onclick="zentaoArchiveClaim(\'' + esc(c.bug_id) + '\',false)">' + t("取消归档") + "</button>"
|
|
11552
11876
|
: '<button class="ghost small" title="' + esc(t("移入已归档区,不再出现在进行中列表")) + '" onclick="zentaoArchiveClaim(\'' + esc(c.bug_id) + '\',true)">' + t("归档") + "</button>";
|
|
11553
11877
|
return '<div class="card' + (archived ? " zt-archived" : "") + '"><div class="head"><span class="name">' + headName + "</span>" +
|
|
11554
|
-
zentaoStateTag(c.state) + act + "</div>" +
|
|
11878
|
+
zentaoStateTag(c.state) + retryBtn + act + "</div>" +
|
|
11555
11879
|
'<div class="auto-meta">' +
|
|
11556
11880
|
(zentaoTriText(c) ? "<span>" + esc(zentaoTriText(c)) + "</span>" : "") +
|
|
11557
11881
|
(tasks || "") +
|
|
@@ -11857,7 +12181,11 @@ function renderMarket() {
|
|
|
11857
12181
|
async function mkInstall(id) {
|
|
11858
12182
|
try {
|
|
11859
12183
|
const r = await api("/api/market/" + encodeURIComponent(id) + "/install", { method: "POST", body: "{}" });
|
|
11860
|
-
|
|
12184
|
+
if (r && r.already) toast(t("该插件已安装过"));
|
|
12185
|
+
else {
|
|
12186
|
+
toast(t("安装成功,运行任务时自动注入提示词;可到「经验库」启停或查看"));
|
|
12187
|
+
(r && r.warnings || []).forEach((w) => toast(w, true)); // 供应链/内容警示逐条红牌
|
|
12188
|
+
}
|
|
11861
12189
|
} catch (e) { toast(e.message, true); return; }
|
|
11862
12190
|
loadMarket();
|
|
11863
12191
|
}
|
|
@@ -11943,13 +12271,22 @@ function mkrCardHtml(p) {
|
|
|
11943
12271
|
esc(p.block_reason || t("该插件无法从公开生态下载")) + '">' + t("不适配") + "</button>";
|
|
11944
12272
|
else ops = '<button class="primary small" data-mk="' + esc(p.id) + '" onclick="mkrInstall(\'' + esc(p.id) + '\')">' + t("安装") + "</button>";
|
|
11945
12273
|
const meta = [p.author, p.version ? "v" + p.version : ""].filter(Boolean).join(" · ");
|
|
12274
|
+
// 信任卡(ClawTrust 思路):热度(来源自报下载量,仅参考)+ 装前安检结论
|
|
12275
|
+
const trust = [
|
|
12276
|
+
p.downloads ? '<span class="tag" title="' + t("来源自报下载量,仅作热度参考") + '">▼ ' +
|
|
12277
|
+
esc(p.downloads) + "</span>" : "",
|
|
12278
|
+
p.scan_note ? (p.scan_note.indexOf("高风险") >= 0
|
|
12279
|
+
? '<span class="tag" style="color:var(--err,#d33)" title="' + esc(p.scan_note) + '">' +
|
|
12280
|
+
t("⚠ 安检高风险") + "</span>"
|
|
12281
|
+
: '<span class="tag" title="' + esc(p.scan_note) + '">' + t("安检:") + esc(p.scan_note.split(":")[0]) + "</span>")
|
|
12282
|
+
: ""].filter(Boolean).join("");
|
|
11946
12283
|
return '<div class="card mk-card' + (p.compat === "blocked" ? " blocked" : "") + '"><div class="head">' +
|
|
11947
12284
|
'<span class="name">' + esc(t(p.title || p.name)) + "</span>" +
|
|
11948
12285
|
'<span class="tag mk-src">' + esc(t(p.source_name || "")) + "</span>" +
|
|
11949
12286
|
(p.category ? '<span class="tag">' + esc(p.category) + "</span>" : "") +
|
|
11950
12287
|
"</div>" +
|
|
11951
12288
|
'<div class="note">' + esc(t(p.desc || "")) + "</div>" +
|
|
11952
|
-
'<div class="mk-meta"><span>' + esc(meta) + "</span
|
|
12289
|
+
'<div class="mk-meta"><span>' + esc(meta) + "</span>" + trust + "</div>" +
|
|
11953
12290
|
'<div class="ops">' + ops + "</div></div>";
|
|
11954
12291
|
}
|
|
11955
12292
|
|
|
@@ -12096,6 +12433,13 @@ function mkrShowInstallPreview(p) {
|
|
|
12096
12433
|
: '<div class="mkpv-sec">' + esc(t(p.risk_label)) + ":" + t("部分内容值得留意识别") +
|
|
12097
12434
|
'<ul class="mkpv-strip">' + riskFindings + "</ul></div>")
|
|
12098
12435
|
: "";
|
|
12436
|
+
// 供应链对账(AST10 #2):内容突变 / 近似名仿冒特征
|
|
12437
|
+
const chainHtml =
|
|
12438
|
+
(p.changed ? '<div class="mkpv-sec" style="color:var(--err,#d33)">⚠ ' +
|
|
12439
|
+
t("内容较上次安装发生变化(指纹 ") + esc(p.digest || "") + t("),建议核对后再启用") + "</div>" : "") +
|
|
12440
|
+
((p.near_names || []).map((n) =>
|
|
12441
|
+
'<div class="mkpv-sec">⚠ ' + t("与已装技能「") + esc(n[0]) + t("」名称过近(相似度 ") +
|
|
12442
|
+
Math.round(n[1] * 100) + t("%),注意甄别仿冒") + "</div>").join(""));
|
|
12099
12443
|
openModal(t("安装前确认"),
|
|
12100
12444
|
'<div class="mkpv">' +
|
|
12101
12445
|
'<div class="mkpv-name">' + esc(p.title || p.id || "") + "</div>" +
|
|
@@ -12103,7 +12447,7 @@ function mkrShowInstallPreview(p) {
|
|
|
12103
12447
|
(p.version ? "<span>v" + esc(p.version) + "</span>" : "") +
|
|
12104
12448
|
"<span>" + esc(_mkrIntegrityText(p.integrity)) + "</span></div>" +
|
|
12105
12449
|
(skills ? '<div class="mkpv-skills">' + t("包含技能:") + skills + "</div>" : "") +
|
|
12106
|
-
riskHtml +
|
|
12450
|
+
riskHtml + chainHtml +
|
|
12107
12451
|
'<div class="mkpv-sum">' + t("共 ") + (p.files_total || 0) + t(" 个文件 · ") + esc(mkCharsText(p.total_chars || 0)) + "</div>" +
|
|
12108
12452
|
(rows ? '<table class="mkpv-files"><thead><tr><th>' + t("文件") + "</th><th>" + t("体量") +
|
|
12109
12453
|
"</th></tr></thead><tbody>" + rows + moreF + "</tbody></table>" : "") +
|
|
@@ -12543,6 +12887,7 @@ function renderEvalBench() {
|
|
|
12543
12887
|
samplesBox.innerHTML = eb.samples.map((s) =>
|
|
12544
12888
|
'<div class="item"><div class="t"><span class="name">' + esc(t(s.name)) + "</span>" +
|
|
12545
12889
|
(s.verify ? '<span class="tag ok">' + t("带客观验证") + "</span>" : "") +
|
|
12890
|
+
(s.redteam ? '<span class="tag" style="color:var(--err,#d33)">' + t("安全测试") + "</span>" : "") +
|
|
12546
12891
|
(s.builtin ? "" : '<span class="tag">' + t("自定义") + "</span>" +
|
|
12547
12892
|
'<button class="ghost small" onclick="sampleExport(\'' + esc(s.id) + '\')">' + t("导出") + "</button>" +
|
|
12548
12893
|
'<button class="danger small" onclick="sampleDelete(\'' + esc(s.id) + '\')">' + t("删除") + "</button>") +
|
|
@@ -13162,7 +13507,8 @@ async function scanPorts() {
|
|
|
13162
13507
|
}
|
|
13163
13508
|
|
|
13164
13509
|
async function closePort(port) {
|
|
13165
|
-
if (!
|
|
13510
|
+
if (!(await uiConfirm(t("结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)").replace("%1", port),
|
|
13511
|
+
{ ok: t("结束进程") }))) return;
|
|
13166
13512
|
try {
|
|
13167
13513
|
const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
|
|
13168
13514
|
toast(r.message || (r.ok ? t("已关闭") : t("未能关闭")), !r.ok);
|
|
@@ -13477,7 +13823,7 @@ function setLangBtn(lang) {
|
|
|
13477
13823
|
if (getLang() === lang) return;
|
|
13478
13824
|
setLang(lang);
|
|
13479
13825
|
// 失效全部渲染签名,让下一次 render() 把所有动态内容重画一遍
|
|
13480
|
-
S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.mgmt = {};
|
|
13826
|
+
S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.runDetailSig = ""; S.mgmt = {};
|
|
13481
13827
|
syncLangMode();
|
|
13482
13828
|
// 浏览器标签标题
|
|
13483
13829
|
document.title = codebeeDocumentTitle(lang);
|
|
@@ -14322,9 +14668,14 @@ function copyConnUrl() {
|
|
|
14322
14668
|
function fallbackCopy(text, done) {
|
|
14323
14669
|
const ta = document.createElement("textarea");
|
|
14324
14670
|
ta.value = text;
|
|
14671
|
+
ta.style.position = "fixed";
|
|
14672
|
+
ta.style.opacity = "0"; // 藏起来但不 display:none(后者 select 不到)
|
|
14325
14673
|
document.body.appendChild(ta);
|
|
14326
14674
|
ta.select();
|
|
14327
|
-
try {
|
|
14675
|
+
try {
|
|
14676
|
+
if (!document.execCommand("copy")) throw new Error("copy rejected");
|
|
14677
|
+
if (done) done();
|
|
14678
|
+
} catch (e) { toast(t("复制失败,请手动长按/右键复制"), true); }
|
|
14328
14679
|
ta.remove();
|
|
14329
14680
|
}
|
|
14330
14681
|
|
|
@@ -14382,7 +14733,7 @@ window.exportBackup = async function () {
|
|
|
14382
14733
|
: '<div><span class="bad">' + esc(t("☁ 远程推送失败:") + (remote.error || "")) + "</span></div>"))
|
|
14383
14734
|
+ (ext.length ? '<div><span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
|
|
14384
14735
|
+ esc(ext.join("、")) + "</span></div>" : "")
|
|
14385
|
-
+ '<button class="ghost small" onclick="copyText(this)" data-copy="' + esc(r.path) + '">'
|
|
14736
|
+
+ '<button class="ghost small" onclick="copyText(this.getAttribute(\'data-copy\'))" data-copy="' + esc(r.path) + '">'
|
|
14386
14737
|
+ esc(t("复制路径")) + "</button>";
|
|
14387
14738
|
} catch (e) {
|
|
14388
14739
|
$("bk-result").innerHTML = '<span class="bad">' + esc(e.message || String(e)) + "</span>";
|
|
@@ -14457,12 +14808,10 @@ async function pullRemoteBackup(name) {
|
|
|
14457
14808
|
if (typeof inspectBackup === "function") inspectBackup(); // 直接进预览向导
|
|
14458
14809
|
}
|
|
14459
14810
|
|
|
14460
|
-
/*
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
catch (e) { fallbackCopy(text, () => toast(t("已复制"))); }
|
|
14465
|
-
};
|
|
14811
|
+
/* ⚠️ 不要在这里再定义 window.copyText——文件前部已有一个字符串版实现
|
|
14812
|
+
* (含 execCommand 兜底 + 成功回调);曾在文件尾再定义一次元素版(读
|
|
14813
|
+
* data-copy 属性),后定义覆盖前定义,导致所有传字符串的复制入口
|
|
14814
|
+
* (作品简介/对话/工作目录路径)一概 TypeError 静默失灵。 */
|
|
14466
14815
|
|
|
14467
14816
|
window.pickBackupFile = async function () {
|
|
14468
14817
|
const cur = (($("bi-path") || {}).value || "").trim();
|
|
@@ -14598,6 +14947,13 @@ function collapseDrawerIfMobile() {
|
|
|
14598
14947
|
function switchTab(name, shell) {
|
|
14599
14948
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
14600
14949
|
if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
|
|
14950
|
+
// 详情开着时切去别的页:详情一并关掉(它是 runs 子页的住户,name==="runs" 除外)。
|
|
14951
|
+
// 不关会留两个残留:S.detailRunId 让顶栏标题卡在「运行详情」;对话态详情给 main
|
|
14952
|
+
// 挂的 chat-fill(overflow:hidden 锁滚动,见 renderChatNav)留在原地——设置/概览
|
|
14953
|
+
// 等子页整页滚不动(2026-09-30 设置页滚不动实案)。closeRun 里的 renderChatNav
|
|
14954
|
+
// 会看到详情已隐而解锚。此处用旧的 S.mainPage 走 closeRun 归位分支,随后本函数
|
|
14955
|
+
// 再落到用户真正点的目标页。
|
|
14956
|
+
if ((S.detailRunId || S.detailTaskKey) && name !== "runs") closeRun();
|
|
14601
14957
|
// 形态由目标页决定:快捷导航页(概览/运行记录/自动化)走 main——只换主栏内容、
|
|
14602
14958
|
// 左栏任务树原地不动;其余页进设置导航。调用方可用 shell 显式覆盖(如已在设置里
|
|
14603
14959
|
// 点运行详情,仍要留在设置侧栏)。用户主动进设置时左栏一定会换成设置导航。
|
|
@@ -14641,6 +14997,7 @@ function switchTab(name, shell) {
|
|
|
14641
14997
|
|
|
14642
14998
|
/* 退出设置/回到新建任务表单:左栏恢复任务树,主栏回到任务页 */
|
|
14643
14999
|
function exitSettings() {
|
|
15000
|
+
if (S.detailRunId || S.detailTaskKey) closeRun(); // 设置运行列表开着详情就回表单:一并关掉,防 chat-fill 锁滚动与标题残留
|
|
14644
15001
|
S.tab = "tasks";
|
|
14645
15002
|
S.mainPage = ""; // 主栏回到表单:不再是快捷导航页
|
|
14646
15003
|
stopAutoPoll(); // 离开设置视图:自动化页轮询一并停掉
|
|
@@ -14878,6 +15235,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
14878
15235
|
if (!b || b.classList.contains("hidden")) return;
|
|
14879
15236
|
S.rdTab = b.dataset.tab;
|
|
14880
15237
|
S.rdTabPin = true;
|
|
15238
|
+
S.rdHiveFirst = false;
|
|
14881
15239
|
applyRdTabs();
|
|
14882
15240
|
rdEnsureActiveTabData();
|
|
14883
15241
|
});
|
|
@@ -15041,6 +15399,43 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
15041
15399
|
document.querySelector("main").addEventListener("scroll", apnSyncActive, { passive: true });
|
|
15042
15400
|
syncLangMode();
|
|
15043
15401
|
$("btn-menu").addEventListener("click", () => document.body.classList.toggle("side-collapsed"));
|
|
15402
|
+
/* 侧栏宽度拖拽:grid 列宽走 --side-w,拖动实时跟手,松手落 localStorage,双击复原。
|
|
15403
|
+
* 仅桌面网格布局生效(窄屏侧栏是固定宽抽屉,手柄已 display:none) */
|
|
15404
|
+
(() => {
|
|
15405
|
+
const KEY = "orch.sideW", MIN = 190, MAX = 460; // 复原默认值=CSS 回退 216
|
|
15406
|
+
const root = document.documentElement;
|
|
15407
|
+
const apply = (w) => root.style.setProperty("--side-w", w + "px");
|
|
15408
|
+
const saved = parseInt(localStorage.getItem(KEY), 10);
|
|
15409
|
+
if (saved >= MIN && saved <= MAX && window.innerWidth >= 900) apply(saved);
|
|
15410
|
+
const el = $("side-resizer");
|
|
15411
|
+
let active = false, startX = 0, startW = 0;
|
|
15412
|
+
const onMove = (e) => {
|
|
15413
|
+
if (!active) return;
|
|
15414
|
+
apply(Math.round(Math.min(MAX, Math.max(MIN, startW + e.clientX - startX))));
|
|
15415
|
+
};
|
|
15416
|
+
const onFinish = () => {
|
|
15417
|
+
if (!active) return;
|
|
15418
|
+
active = false;
|
|
15419
|
+
document.body.classList.remove("side-resizing");
|
|
15420
|
+
localStorage.setItem(KEY, String(Math.round($("sidebar").getBoundingClientRect().width)));
|
|
15421
|
+
};
|
|
15422
|
+
el.addEventListener("pointerdown", (e) => {
|
|
15423
|
+
if (window.innerWidth < 900) return;
|
|
15424
|
+
active = true; startX = e.clientX;
|
|
15425
|
+
startW = $("sidebar").getBoundingClientRect().width;
|
|
15426
|
+
document.body.classList.add("side-resizing");
|
|
15427
|
+
e.preventDefault();
|
|
15428
|
+
});
|
|
15429
|
+
/* move/up 挂 window:setPointerCapture 在部分环境(headless 真实输入实测)不生效,
|
|
15430
|
+
move 会按命中测试派发到主区元素,只有 window 级监听保证收到 */
|
|
15431
|
+
window.addEventListener("pointermove", onMove);
|
|
15432
|
+
window.addEventListener("pointerup", onFinish);
|
|
15433
|
+
window.addEventListener("pointercancel", onFinish);
|
|
15434
|
+
el.addEventListener("dblclick", () => {
|
|
15435
|
+
localStorage.removeItem(KEY);
|
|
15436
|
+
root.style.removeProperty("--side-w");
|
|
15437
|
+
});
|
|
15438
|
+
})();
|
|
15044
15439
|
// 手机抽屉:遮罩点击 / 侧栏内任何可点项(导航、任务树、设置入口)点击后都收回
|
|
15045
15440
|
$("drawer-mask").addEventListener("click", () => document.body.classList.add("side-collapsed"));
|
|
15046
15441
|
$("sidebar").addEventListener("click", (e) => {
|