codebee 0.1.72 → 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 +6 -0
- package/README.md +4 -4
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +4 -1
- package/app/core/manager.py +68 -3
- package/app/core/market.py +2 -2
- package/app/core/paihang.py +8 -1
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -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/skillpacks/fanqie-novel.md +66 -48
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/main.py +24 -28
- package/app/ui/app.js +270 -79
- package/app/ui/i18n.js +17 -2
- package/app/ui/index.html +3 -3
- package/app/ui/style.css +7049 -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: {}, 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,51 @@ async function api(path, opts) {
|
|
|
1269
1269
|
}
|
|
1270
1270
|
}
|
|
1271
1271
|
|
|
1272
|
-
|
|
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) {
|
|
1273
1294
|
try {
|
|
1274
1295
|
return await api(path, { method: "POST", body: JSON.stringify(body) });
|
|
1275
1296
|
} catch (e) {
|
|
1276
1297
|
const gate = e && e.data && e.data.quality_gate;
|
|
1277
1298
|
if (!gate) throw e;
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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("未填写人工复核原因,已取消强制放行"));
|
|
1282
1315
|
return await api(path, { method: "POST", body: JSON.stringify({ ...body,
|
|
1283
|
-
force: true, force_confirmed: true, force_reason: reason
|
|
1316
|
+
force: true, force_confirmed: true, force_reason: reason }) });
|
|
1284
1317
|
}
|
|
1285
1318
|
}
|
|
1286
1319
|
|
|
@@ -1321,7 +1354,18 @@ function _askOpen(opts) {
|
|
|
1321
1354
|
_askResolve = resolve;
|
|
1322
1355
|
$("ask-title").textContent = opts.title || t("确认");
|
|
1323
1356
|
$("ask-body").innerHTML = opts.bodyHtml || "";
|
|
1357
|
+
// 三态弹框:extra 按钮插在「取消」左侧,点按以 opts 里的 value 结束
|
|
1358
|
+
// (非布尔,只有用到 extra 的调用方需要判读);开新框先清上一帧残留。
|
|
1324
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
|
+
});
|
|
1325
1369
|
yes.textContent = opts.okText || t("确定");
|
|
1326
1370
|
yes.className = opts.danger ? "danger" : "primary";
|
|
1327
1371
|
$("ask").classList.remove("hidden");
|
|
@@ -1937,11 +1981,16 @@ function renderProvList() {
|
|
|
1937
1981
|
const box = $("prov-list");
|
|
1938
1982
|
if (!box) return;
|
|
1939
1983
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1940
|
-
const
|
|
1984
|
+
const kwHit = (p) => !kw ||
|
|
1941
1985
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1942
1986
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
1943
1987
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
1944
|
-
(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);
|
|
1945
1994
|
if (!S.selProv && provs.length) S.selProv = provs[0].id;
|
|
1946
1995
|
if (S.selProv && !provs.some((p) => p.id === S.selProv)) {
|
|
1947
1996
|
S.selProv = provs.length ? provs[0].id : null;
|
|
@@ -1952,6 +2001,13 @@ function renderProvList() {
|
|
|
1952
2001
|
}
|
|
1953
2002
|
const selN = Object.keys(S.selProvs || {}).length;
|
|
1954
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>" : "";
|
|
1955
2011
|
const batch = selN ? '<div class="prov-batch"><span class="n">' + t("已选 ") + selN + t(" 个") + "</span>" +
|
|
1956
2012
|
'<button class="ghost small" onclick="batchProvOp(\'enable\')">' + t("启用") + '</button>' +
|
|
1957
2013
|
'<button class="ghost small" onclick="batchProvOp(\'disable\')">' + t("停用") + '</button>' +
|
|
@@ -1960,9 +2016,10 @@ function renderProvList() {
|
|
|
1960
2016
|
const allBox = provs.length ? '<label class="prov-all"><input type="checkbox"' +
|
|
1961
2017
|
(selN === provs.length && selN > 0 ? " checked" : "") +
|
|
1962
2018
|
' 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
|
-
|
|
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;
|
|
1966
2023
|
const st = n == null ? t("未获取") : n + t(" 模型");
|
|
1967
2024
|
const off = p.enabled === false;
|
|
1968
2025
|
return '<div class="prov-item' + (p.id === S.selProv ? " active" : "") + (off ? " off" : "") +
|
|
@@ -1980,7 +2037,16 @@ function renderProvList() {
|
|
|
1980
2037
|
}).join("") ||
|
|
1981
2038
|
'<div class="hint" style="padding:8px">' + (kw && (S.providers || []).length
|
|
1982
2039
|
? t("没有匹配「") + esc(S.provFilter) + t("」的供应商。")
|
|
1983
|
-
:
|
|
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();
|
|
1984
2050
|
}
|
|
1985
2051
|
|
|
1986
2052
|
function toggleProvSel(id, on) {
|
|
@@ -1994,11 +2060,13 @@ function toggleAllProvSel(on) {
|
|
|
1994
2060
|
S.selProvs = {};
|
|
1995
2061
|
if (on) {
|
|
1996
2062
|
const kw = (S.provFilter || "").trim().toLowerCase();
|
|
1997
|
-
|
|
2063
|
+
const view = S.provStatus || "enabled";
|
|
2064
|
+
for (const p of (S.providers || []).filter((p) => (!kw ||
|
|
1998
2065
|
(p.name || "").toLowerCase().includes(kw) ||
|
|
1999
2066
|
(p.protocol || "").toLowerCase().includes(kw) ||
|
|
2000
2067
|
(p.source || "").toLowerCase().includes(kw) ||
|
|
2001
|
-
(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;
|
|
2002
2070
|
}
|
|
2003
2071
|
S.modelsSig = null;
|
|
2004
2072
|
renderProvList();
|
|
@@ -2105,6 +2173,18 @@ function renderProvDetail() {
|
|
|
2105
2173
|
' value="' + esc(kw) + '" oninput="filterModels(\'' + esc(p.id) + '\', this.value)">' +
|
|
2106
2174
|
'<span class="pm-count" id="pm-count-' + esc(p.id) + '"></span>' +
|
|
2107
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
|
+
}
|
|
2108
2188
|
html += '<div id="pm-groups" class="pm-groups' + (selN ? " has-sel" : "") + '">' +
|
|
2109
2189
|
provModelGroupsHtml(p) + "</div>";
|
|
2110
2190
|
if (hidden.length) {
|
|
@@ -2258,7 +2338,9 @@ function wireCapTags(p, excludeProto) {
|
|
|
2258
2338
|
/* 模型分组区 HTML(全量重绘与过滤重绘共用;过滤时暂停拖拽排序) */
|
|
2259
2339
|
function provModelGroupsHtml(p) {
|
|
2260
2340
|
const kw = ((S.modelFilter || {})[p.id] || "").trim().toLowerCase();
|
|
2341
|
+
const mview = S.mlistStatus || "enabled";
|
|
2261
2342
|
const models = (p.models || []).filter((m) => !m.hidden)
|
|
2343
|
+
.filter((m) => mview === "all" || (mview === "off" ? m.enabled === false : m.enabled !== false))
|
|
2262
2344
|
.filter((m) => !kw || (m.name || "").toLowerCase().includes(kw))
|
|
2263
2345
|
.sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
2264
2346
|
const sel = modelSel(p.id);
|
|
@@ -2269,9 +2351,15 @@ function provModelGroupsHtml(p) {
|
|
|
2269
2351
|
}
|
|
2270
2352
|
const gnames = Object.keys(groups).sort();
|
|
2271
2353
|
if (!gnames.length) {
|
|
2272
|
-
|
|
2354
|
+
const offAll = (p.models || []).filter((m) => !m.hidden && m.enabled === false).length;
|
|
2355
|
+
const msg = kw
|
|
2273
2356
|
? t("没有匹配「") + esc(kw) + t("」的模型。")
|
|
2274
|
-
:
|
|
2357
|
+
: mview === "off"
|
|
2358
|
+
? t("没有已停用的模型。")
|
|
2359
|
+
: (mview === "enabled" && offAll
|
|
2360
|
+
? t("%1 个模型已停用——点上方「已停用」查看。").replace("%1", offAll)
|
|
2361
|
+
: t("该供应商没有可用模型(或全部被停用)。"));
|
|
2362
|
+
return '<div class="empty">' + msg + "</div>";
|
|
2275
2363
|
}
|
|
2276
2364
|
let html = "";
|
|
2277
2365
|
for (const g of gnames) {
|
|
@@ -2307,9 +2395,12 @@ function updateModelCount(pid) {
|
|
|
2307
2395
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2308
2396
|
if (!el || !p || !p.models) return;
|
|
2309
2397
|
const kw = (S.modelFilter || {})[pid] || "";
|
|
2310
|
-
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;
|
|
2311
2402
|
const shown = total && kw.trim()
|
|
2312
|
-
? p.models.filter((m) =>
|
|
2403
|
+
? p.models.filter((m) => inView(m) &&
|
|
2313
2404
|
(m.name || "").toLowerCase().includes(kw.trim().toLowerCase())).length
|
|
2314
2405
|
: total;
|
|
2315
2406
|
el.textContent = kw.trim() ? (shown + " / " + total + t(" 个模型")) : (total + t(" 个模型"));
|
|
@@ -2508,9 +2599,11 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2508
2599
|
const p = (S.providers || []).find((x) => x.id === pid);
|
|
2509
2600
|
if (!p) return;
|
|
2510
2601
|
const kw = ((S.modelFilter || {})[pid] || "").trim().toLowerCase();
|
|
2602
|
+
const mview = S.mlistStatus || "enabled";
|
|
2511
2603
|
const s = modelSelSet(pid);
|
|
2512
2604
|
for (const m of p.models || []) {
|
|
2513
2605
|
if (m.hidden) continue;
|
|
2606
|
+
if (mview !== "all" && (mview === "off" ? m.enabled !== false : m.enabled === false)) continue;
|
|
2514
2607
|
if ((m.protocol || p.protocol || t("其他")) !== group) continue;
|
|
2515
2608
|
if (kw && !(m.name || "").toLowerCase().includes(kw)) continue; // 过滤时只作用于可见行
|
|
2516
2609
|
if (on) s[m.name] = true; else delete s[m.name];
|
|
@@ -2519,6 +2612,13 @@ function toggleGroupSel(pid, group, on) {
|
|
|
2519
2612
|
renderProvDetail();
|
|
2520
2613
|
}
|
|
2521
2614
|
|
|
2615
|
+
/* 模型列表状态过滤 chip:默认只看启用 */
|
|
2616
|
+
function setModelStatus(v) {
|
|
2617
|
+
S.mlistStatus = v;
|
|
2618
|
+
S.modelsSig = null;
|
|
2619
|
+
renderProvDetail();
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2522
2622
|
function clearModelSel(pid) {
|
|
2523
2623
|
if (S.selModels) delete S.selModels[pid];
|
|
2524
2624
|
S.modelsSig = null;
|
|
@@ -4417,6 +4517,7 @@ window.sideOpenTask = function (key) {
|
|
|
4417
4517
|
S.detailRunId = null;
|
|
4418
4518
|
S.focusStep = 0;
|
|
4419
4519
|
S.taskSig = "";
|
|
4520
|
+
S.runDetailSig = ""; // 详情在任务态/运行态间共用同一套 DOM:切走必须作废运行态签名
|
|
4420
4521
|
rdTabReset();
|
|
4421
4522
|
if (!S.histJump && typeof histPush === "function") histPush({ m: "main", tab: "run-detail" });
|
|
4422
4523
|
showDetailInMain();
|
|
@@ -4662,11 +4763,23 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4662
4763
|
const lr0 = runs[0] || {};
|
|
4663
4764
|
const resumePending = (lr0.resume_enqueue_at &&
|
|
4664
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; };
|
|
4665
4779
|
const sig = JSON.stringify(runs.map((r) => [r.id, r.status, (r.steps || []).length,
|
|
4666
4780
|
(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 || {})])
|
|
4781
|
+
msgN(r), msgP(r),
|
|
4782
|
+
r.summary, r.error, vcmp(r)])
|
|
4670
4783
|
.concat([JSON.stringify([(bmRoot || {}).book_meta || null,
|
|
4671
4784
|
(bmTask || {}).cover_gen || null]), resumePending]));
|
|
4672
4785
|
if (sig === S.taskSig) return;
|
|
@@ -4915,6 +5028,7 @@ async function openRun(id, pinTab) {
|
|
|
4915
5028
|
_taskStepRenderToken++;
|
|
4916
5029
|
S.detailRunId = id;
|
|
4917
5030
|
S.detailTaskKey = null;
|
|
5031
|
+
S.runDetailSig = ""; // 新目标首帧必画;也兜住「任务态改过 DOM 后回到本运行」的串台
|
|
4918
5032
|
renderPageCrumb(); // 标题「运行详情」;必须在赋值后画——showDetailInMain 先于本函数跑,那边画不到
|
|
4919
5033
|
rdTabReset(pinTab || null); // sideOpenRun 带步骤号时钉住步骤分区
|
|
4920
5034
|
document.querySelector("#sub-runs .panel:first-child").classList.add("hidden");
|
|
@@ -4929,6 +5043,7 @@ function closeRun() {
|
|
|
4929
5043
|
S.detailRunId = null;
|
|
4930
5044
|
S.detailTaskKey = null;
|
|
4931
5045
|
S.taskSig = "";
|
|
5046
|
+
S.runDetailSig = "";
|
|
4932
5047
|
S.focusStep = 0;
|
|
4933
5048
|
rdTabReset();
|
|
4934
5049
|
stopLogLive();
|
|
@@ -5276,6 +5391,15 @@ async function renderRunDetail() {
|
|
|
5276
5391
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5277
5392
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
5278
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;
|
|
5279
5403
|
// 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
|
|
5280
5404
|
// 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
|
|
5281
5405
|
syncArchBtn(rcTask, active);
|
|
@@ -5499,6 +5623,17 @@ function renderBookMetaPanel(task) {
|
|
|
5499
5623
|
if (box) box.classList.add("hidden");
|
|
5500
5624
|
return;
|
|
5501
5625
|
}
|
|
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
|
+
}
|
|
5502
5637
|
const src = bmRootTask(task); // 都是用第一个:开书资料读链上根任务
|
|
5503
5638
|
const inherited = src.id !== task.id;
|
|
5504
5639
|
const bm = src.book_meta || {};
|
|
@@ -5644,8 +5779,8 @@ window.coverGen = async function (taskId) {
|
|
|
5644
5779
|
};
|
|
5645
5780
|
|
|
5646
5781
|
window.bmCopyBtn = function (btn) {
|
|
5647
|
-
|
|
5648
|
-
toast(t("已复制:") + (btn.dataset.label || ""));
|
|
5782
|
+
// 只在真实复制成功后报「已复制」——剪贴板不可用时 fallbackCopy 会弹失败提示
|
|
5783
|
+
copyText(btn.dataset.text || "", () => toast(t("已复制:") + (btn.dataset.label || "")));
|
|
5649
5784
|
};
|
|
5650
5785
|
|
|
5651
5786
|
/* ---------------- 一键发布(bookmeta 分区内每平台卡的发布行) ----------------
|
|
@@ -5751,7 +5886,7 @@ function pbBlock(task, platform) {
|
|
|
5751
5886
|
}
|
|
5752
5887
|
btns += '<button class="primary" ' + (busy ? "disabled" : "") +
|
|
5753
5888
|
' onclick="pbCreateBook(\'' + esc(task.id) + "', '" + platform + '\')">' + t("创建作品") + "</button>";
|
|
5754
|
-
btns += '<button class="ghost" onclick="pbRegToggle(\'' + platform + '\')" title="' +
|
|
5889
|
+
btns += '<button class="ghost" onclick="pbRegToggle(\'' + esc(task.id) + "','" + platform + '\')" title="' +
|
|
5755
5890
|
esc(t("作品已在平台建好(建书流程没走通或手工建的)?补登记后直接发章,不会在平台新建")) + '">' + t("登记已有作品") + "</button>";
|
|
5756
5891
|
}
|
|
5757
5892
|
let extra = "";
|
|
@@ -5759,9 +5894,16 @@ function pbBlock(task, platform) {
|
|
|
5759
5894
|
extra += '<div class="pb-hint">' + esc(t("若作品其实已在平台建好,点「登记已有作品」补登记,勿重复创建")) + "</div>";
|
|
5760
5895
|
}
|
|
5761
5896
|
if (!bookReady) {
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
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)">' +
|
|
5765
5907
|
'<button class="ghost" onclick="pbRegister(\'' + esc(task.id) + "', '" + platform + '\')">' + t("确认登记") + "</button>" +
|
|
5766
5908
|
"</div>";
|
|
5767
5909
|
}
|
|
@@ -5777,18 +5919,27 @@ function pbBlock(task, platform) {
|
|
|
5777
5919
|
let _pbTimer = 0;
|
|
5778
5920
|
function pbSyncState(task) {
|
|
5779
5921
|
clearTimeout(_pbTimer);
|
|
5922
|
+
// 5s 节流折进调度间隔:距上一拉未满 5s 就等满再拉(pbKick 置 0 可立即拉)
|
|
5923
|
+
const delay = Math.max(120, 5000 - (Date.now() - (S.pubFetchAt || 0)));
|
|
5780
5924
|
_pbTimer = setTimeout(async () => {
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
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();
|
|
5784
5935
|
let sig = "";
|
|
5785
5936
|
try {
|
|
5786
5937
|
const v = await api("/api/publish");
|
|
5787
5938
|
S.pubState = v;
|
|
5788
5939
|
sig += JSON.stringify(v.platforms || {});
|
|
5789
|
-
} catch (e) { return; }
|
|
5940
|
+
} catch (e) { pbSyncState(tk); return; }
|
|
5790
5941
|
try {
|
|
5791
|
-
const ti = await api("/api/publish/task/" + encodeURIComponent(
|
|
5942
|
+
const ti = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/history");
|
|
5792
5943
|
S.pubTaskInfo = ti;
|
|
5793
5944
|
sig += "#" + JSON.stringify(ti.books || {}) + "#" + (ti.history || []).length;
|
|
5794
5945
|
// 已发章数自动校准:平台在线且登记在案、超 10 分钟没对过账就静默对一次
|
|
@@ -5803,22 +5954,23 @@ function pbSyncState(task) {
|
|
|
5803
5954
|
if (ps2.status === "connected" && nowMs - at > 600000 &&
|
|
5804
5955
|
nowMs - (S["_pbSyncAt_" + p] || 0) > 600000) {
|
|
5805
5956
|
S["_pbSyncAt_" + p] = nowMs;
|
|
5806
|
-
api("/api/publish/task/" + encodeURIComponent(
|
|
5957
|
+
api("/api/publish/task/" + encodeURIComponent(tk.id) + "/sync-published",
|
|
5807
5958
|
{ method: "POST", body: JSON.stringify({ platform: p }) }).catch(() => {});
|
|
5808
5959
|
}
|
|
5809
5960
|
});
|
|
5810
5961
|
} catch (e) { /* 任务级失败不阻塞平台状态 */ }
|
|
5811
5962
|
try {
|
|
5812
5963
|
// 批量发布视图(待发数/护栏/进度)——进行中时靠本节流轮询自然刷新
|
|
5813
|
-
const au = await api("/api/publish/task/" + encodeURIComponent(
|
|
5964
|
+
const au = await api("/api/publish/task/" + encodeURIComponent(tk.id) + "/pending");
|
|
5814
5965
|
S.pubAuto = au;
|
|
5815
5966
|
sig += "@" + JSON.stringify(au.books || {}) + "@" + JSON.stringify(au.running || {});
|
|
5816
5967
|
} catch (e) { /* 视图缺失(老服务)不阻塞 */ }
|
|
5817
|
-
if (sig !== (S._pbSig || "")
|
|
5968
|
+
if (sig !== (S._pbSig || "")) {
|
|
5818
5969
|
S._pbSig = sig;
|
|
5819
|
-
renderBookMetaPanel(
|
|
5970
|
+
renderBookMetaPanel(tk);
|
|
5820
5971
|
} else S._pbSig = sig;
|
|
5821
|
-
|
|
5972
|
+
pbSyncState(tk); // 续链:面板还开着就下一轮 5s 后再来
|
|
5973
|
+
}, delay);
|
|
5822
5974
|
}
|
|
5823
5975
|
|
|
5824
5976
|
window.pbSyncPublished = async function (taskId, platform) {
|
|
@@ -5856,19 +6008,34 @@ window.pbProbe = async function (platform) {
|
|
|
5856
6008
|
};
|
|
5857
6009
|
|
|
5858
6010
|
window.pbCreateBook = async function (taskId, platform) {
|
|
5859
|
-
if (!
|
|
6011
|
+
if (!(await uiConfirm(t("将用生成的作品信息在平台自动填建书表单。填好后会停在最后一步,由你在浏览器里人工点提交。继续?"),
|
|
6012
|
+
{ ok: t("开始建书") }))) return;
|
|
5860
6013
|
try {
|
|
5861
|
-
await pbApiWithQualityOverride(
|
|
6014
|
+
const r = await pbApiWithQualityOverride(
|
|
5862
6015
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/create-book",
|
|
5863
|
-
{ platform }, "创建作品前的质量门禁未通过");
|
|
5864
|
-
|
|
6016
|
+
{ platform }, "创建作品前的质量门禁未通过", taskId);
|
|
6017
|
+
if (!(r && r.gate_reviewed))
|
|
6018
|
+
toast(t("正在自动填写建书表单…(完成后请在浏览器里确认提交)"));
|
|
5865
6019
|
} catch (e) { toast(t("建书失败:") + e.message, true); }
|
|
5866
6020
|
S._pbSig = ""; pbKick();
|
|
5867
6021
|
};
|
|
5868
6022
|
|
|
5869
|
-
|
|
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) {
|
|
5870
6032
|
const box = $("pb-reg-" + platform);
|
|
5871
|
-
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"); // 展开态也外置:重绘不再把表单折回去
|
|
5872
6039
|
};
|
|
5873
6040
|
|
|
5874
6041
|
window.pbRegister = async function (taskId, platform) {
|
|
@@ -5881,6 +6048,7 @@ window.pbRegister = async function (taskId, platform) {
|
|
|
5881
6048
|
toast(t("已登记,可直接发章"));
|
|
5882
6049
|
const box = $("pb-reg-" + platform);
|
|
5883
6050
|
if (box) box.classList.add("hidden");
|
|
6051
|
+
if (S.pbRegDraft) delete S.pbRegDraft[taskId + ":" + platform]; // 登记落定,草稿使命完成
|
|
5884
6052
|
} catch (e) { toast(t("登记失败:") + e.message, true); }
|
|
5885
6053
|
S._pbSig = ""; pbKick();
|
|
5886
6054
|
};
|
|
@@ -5938,20 +6106,23 @@ window.pbUploadChapter = async function (taskId, platform) {
|
|
|
5938
6106
|
};
|
|
5939
6107
|
|
|
5940
6108
|
window.pbSendChapter = async function (taskId, platform, relName) {
|
|
5941
|
-
if (!
|
|
6109
|
+
if (!(await uiConfirm(t("将把《{0}》填进平台章节编辑器,填好后由你人工提交。继续?", relName),
|
|
6110
|
+
{ ok: t("开始填稿") }))) return;
|
|
5942
6111
|
try {
|
|
5943
|
-
await pbApiWithQualityOverride(
|
|
6112
|
+
const r = await pbApiWithQualityOverride(
|
|
5944
6113
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/chapter",
|
|
5945
|
-
{ platform, file: relName }, "发章前的质量门禁未通过");
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
'
|
|
5953
|
-
|
|
5954
|
-
|
|
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
|
+
}
|
|
5955
6126
|
}
|
|
5956
6127
|
} catch (e) { toast(t("发章失败:") + e.message, true); }
|
|
5957
6128
|
S._pbSig = ""; pbKick();
|
|
@@ -5970,12 +6141,14 @@ window.pbPublishAll = async function (taskId, platform) {
|
|
|
5970
6141
|
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
5971
6142
|
.find((b) => b.platform === platform)) || {};
|
|
5972
6143
|
if (!au.pending) return;
|
|
5973
|
-
if (!
|
|
6144
|
+
if (!(await uiConfirm(t("将从最靠前的待发章节开始填稿(共 {0} 章待发)。本轮只填一章并停在表单页,由你在浏览器里确认提交;提交后点击“我已在平台提交”,再选择下一章。护栏(每日上限/连续失败暂停)生效。继续?", au.pending),
|
|
6145
|
+
{ ok: t("开始发布") }))) return;
|
|
5974
6146
|
try {
|
|
5975
|
-
await pbApiWithQualityOverride(
|
|
6147
|
+
const r = await pbApiWithQualityOverride(
|
|
5976
6148
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
5977
|
-
{ platform }, "批量发布前的质量门禁未通过");
|
|
5978
|
-
|
|
6149
|
+
{ platform }, "批量发布前的质量门禁未通过", taskId);
|
|
6150
|
+
if (!(r && r.gate_reviewed))
|
|
6151
|
+
toast(t("正在填第一章稿——填好后请在浏览器窗口里确认提交,再重新校准"));
|
|
5979
6152
|
} catch (e) { toast(t("自动发布失败:") + e.message, true); }
|
|
5980
6153
|
S._pbSig = ""; pbKick();
|
|
5981
6154
|
};
|
|
@@ -7471,13 +7644,20 @@ async function renderPreview(runId) {
|
|
|
7471
7644
|
rdTabsSync(); // 预览可用性(rdTabAvail 认 #rd-preview 的显隐)随之更新
|
|
7472
7645
|
}
|
|
7473
7646
|
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
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(); };
|
|
7477
7653
|
try {
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
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);
|
|
7481
7661
|
};
|
|
7482
7662
|
|
|
7483
7663
|
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
@@ -9187,8 +9367,7 @@ function bindChat() {
|
|
|
9187
9367
|
if (!it) return;
|
|
9188
9368
|
const txt = it.kind === "user" ? String(it.text || "") : chatCleanText(it.text);
|
|
9189
9369
|
if (act.dataset.cact === "copy") {
|
|
9190
|
-
copyText(txt);
|
|
9191
|
-
toast(t("已复制"));
|
|
9370
|
+
copyText(txt, () => toast(t("已复制")));
|
|
9192
9371
|
} else if (act.dataset.cact === "edit") {
|
|
9193
9372
|
const ta = $("rd-chat-input");
|
|
9194
9373
|
if (ta && txt) {
|
|
@@ -13328,7 +13507,8 @@ async function scanPorts() {
|
|
|
13328
13507
|
}
|
|
13329
13508
|
|
|
13330
13509
|
async function closePort(port) {
|
|
13331
|
-
if (!
|
|
13510
|
+
if (!(await uiConfirm(t("结束占用端口 %1 的进程?先温和关闭,无效会自动强制结束(系统进程会被拒绝)").replace("%1", port),
|
|
13511
|
+
{ ok: t("结束进程") }))) return;
|
|
13332
13512
|
try {
|
|
13333
13513
|
const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
|
|
13334
13514
|
toast(r.message || (r.ok ? t("已关闭") : t("未能关闭")), !r.ok);
|
|
@@ -13643,7 +13823,7 @@ function setLangBtn(lang) {
|
|
|
13643
13823
|
if (getLang() === lang) return;
|
|
13644
13824
|
setLang(lang);
|
|
13645
13825
|
// 失效全部渲染签名,让下一次 render() 把所有动态内容重画一遍
|
|
13646
|
-
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 = {};
|
|
13647
13827
|
syncLangMode();
|
|
13648
13828
|
// 浏览器标签标题
|
|
13649
13829
|
document.title = codebeeDocumentTitle(lang);
|
|
@@ -14488,9 +14668,14 @@ function copyConnUrl() {
|
|
|
14488
14668
|
function fallbackCopy(text, done) {
|
|
14489
14669
|
const ta = document.createElement("textarea");
|
|
14490
14670
|
ta.value = text;
|
|
14671
|
+
ta.style.position = "fixed";
|
|
14672
|
+
ta.style.opacity = "0"; // 藏起来但不 display:none(后者 select 不到)
|
|
14491
14673
|
document.body.appendChild(ta);
|
|
14492
14674
|
ta.select();
|
|
14493
|
-
try {
|
|
14675
|
+
try {
|
|
14676
|
+
if (!document.execCommand("copy")) throw new Error("copy rejected");
|
|
14677
|
+
if (done) done();
|
|
14678
|
+
} catch (e) { toast(t("复制失败,请手动长按/右键复制"), true); }
|
|
14494
14679
|
ta.remove();
|
|
14495
14680
|
}
|
|
14496
14681
|
|
|
@@ -14548,7 +14733,7 @@ window.exportBackup = async function () {
|
|
|
14548
14733
|
: '<div><span class="bad">' + esc(t("☁ 远程推送失败:") + (remote.error || "")) + "</span></div>"))
|
|
14549
14734
|
+ (ext.length ? '<div><span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
|
|
14550
14735
|
+ esc(ext.join("、")) + "</span></div>" : "")
|
|
14551
|
-
+ '<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) + '">'
|
|
14552
14737
|
+ esc(t("复制路径")) + "</button>";
|
|
14553
14738
|
} catch (e) {
|
|
14554
14739
|
$("bk-result").innerHTML = '<span class="bad">' + esc(e.message || String(e)) + "</span>";
|
|
@@ -14623,12 +14808,10 @@ async function pullRemoteBackup(name) {
|
|
|
14623
14808
|
if (typeof inspectBackup === "function") inspectBackup(); // 直接进预览向导
|
|
14624
14809
|
}
|
|
14625
14810
|
|
|
14626
|
-
/*
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
catch (e) { fallbackCopy(text, () => toast(t("已复制"))); }
|
|
14631
|
-
};
|
|
14811
|
+
/* ⚠️ 不要在这里再定义 window.copyText——文件前部已有一个字符串版实现
|
|
14812
|
+
* (含 execCommand 兜底 + 成功回调);曾在文件尾再定义一次元素版(读
|
|
14813
|
+
* data-copy 属性),后定义覆盖前定义,导致所有传字符串的复制入口
|
|
14814
|
+
* (作品简介/对话/工作目录路径)一概 TypeError 静默失灵。 */
|
|
14632
14815
|
|
|
14633
14816
|
window.pickBackupFile = async function () {
|
|
14634
14817
|
const cur = (($("bi-path") || {}).value || "").trim();
|
|
@@ -14764,6 +14947,13 @@ function collapseDrawerIfMobile() {
|
|
|
14764
14947
|
function switchTab(name, shell) {
|
|
14765
14948
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
14766
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();
|
|
14767
14957
|
// 形态由目标页决定:快捷导航页(概览/运行记录/自动化)走 main——只换主栏内容、
|
|
14768
14958
|
// 左栏任务树原地不动;其余页进设置导航。调用方可用 shell 显式覆盖(如已在设置里
|
|
14769
14959
|
// 点运行详情,仍要留在设置侧栏)。用户主动进设置时左栏一定会换成设置导航。
|
|
@@ -14807,6 +14997,7 @@ function switchTab(name, shell) {
|
|
|
14807
14997
|
|
|
14808
14998
|
/* 退出设置/回到新建任务表单:左栏恢复任务树,主栏回到任务页 */
|
|
14809
14999
|
function exitSettings() {
|
|
15000
|
+
if (S.detailRunId || S.detailTaskKey) closeRun(); // 设置运行列表开着详情就回表单:一并关掉,防 chat-fill 锁滚动与标题残留
|
|
14810
15001
|
S.tab = "tasks";
|
|
14811
15002
|
S.mainPage = ""; // 主栏回到表单:不再是快捷导航页
|
|
14812
15003
|
stopAutoPoll(); // 离开设置视图:自动化页轮询一并停掉
|