codebee 0.1.9 → 0.1.11
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 +20 -0
- package/README.md +8 -6
- package/app/core/aiflavor.py +51 -0
- package/app/core/automation.py +9 -0
- package/app/core/gitmod.py +4 -0
- package/app/core/jobs.py +115 -13
- package/app/core/manager.py +49 -0
- package/app/core/modelhub.py +10 -1
- package/app/core/paths.py +1 -0
- package/app/core/pipeline.py +282 -1
- package/app/core/publish/__init__.py +7 -0
- package/app/core/publish/auto.py +367 -0
- package/app/core/publish/browser.py +410 -0
- package/app/core/publish/fanqie.py +98 -0
- package/app/core/publish/flow.py +281 -0
- package/app/core/publish/ledger.py +198 -0
- package/app/core/publish/manager.py +427 -0
- package/app/core/publish/qimao.py +97 -0
- package/app/core/publish/ws.py +139 -0
- package/app/core/settings.py +18 -3
- package/app/core/store.py +24 -2
- package/app/main.py +174 -0
- package/app/ui/app.js +332 -28
- package/app/ui/i18n.js +5 -1
- package/app/ui/index.html +5 -3
- package/app/ui/style.css +64 -0
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -271,6 +271,14 @@ function qsAuth() {
|
|
|
271
271
|
return s ? "?" + s : "";
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/* 下载/预览等导航类 URL 同样带不了请求头:令牌拼进 query(已有 query 用 &,
|
|
275
|
+
* 没有用 ?;已带 token 的 URL 原样返回)。本机会话令牌为空则原样返回。 */
|
|
276
|
+
function urlAuth(u) {
|
|
277
|
+
const t = localStorage.getItem("orch.token");
|
|
278
|
+
if (!t || String(u).includes("token=")) return u;
|
|
279
|
+
return u + (String(u).includes("?") ? "&" : "?") + "token=" + encodeURIComponent(t);
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
/* ---------------------------------------------------------- 工具 */
|
|
275
283
|
async function api(path, opts) {
|
|
276
284
|
opts = opts || {};
|
|
@@ -395,6 +403,20 @@ function statusChip(st) {
|
|
|
395
403
|
return '<span class="chip ' + esc(st) + '">' + (zh[st] || esc(st)) + "</span>";
|
|
396
404
|
}
|
|
397
405
|
|
|
406
|
+
/* 运行状态文案(传 run 对象):退避窗口内的续跑副本写明「将于 HH:MM 自动
|
|
407
|
+
* 续跑」,别让 5 分钟等待看起来像卡死/资源排队(2026-09-18 重写任务误判案)。
|
|
408
|
+
* 到点后翻回「排队中」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
|
|
409
|
+
function runStatusText(run) {
|
|
410
|
+
const st = String((run && run.status) || "");
|
|
411
|
+
if (st === "queued" && run && run.resume_enqueue_at) {
|
|
412
|
+
const at = Date.parse(String(run.resume_enqueue_at).replace(" ", "T"));
|
|
413
|
+
if (!isNaN(at) && Date.now() < at)
|
|
414
|
+
return t("将于 ") + String(run.resume_enqueue_at).slice(11, 16) + t(" 自动续跑");
|
|
415
|
+
}
|
|
416
|
+
return { queued: t("排队中"), running: t("运行中"), done: t("完成"),
|
|
417
|
+
failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") }[st] || st;
|
|
418
|
+
}
|
|
419
|
+
|
|
398
420
|
/* 运行错误来源徽标:错误文案以「超时」打头(runner 统一格式)时标 TIMEOUT,
|
|
399
421
|
* 和普通失败一眼区分;运行状态仍是 failed,不动状态机 */
|
|
400
422
|
function errTag(err) {
|
|
@@ -1758,6 +1780,40 @@ function renderImplSelects() {
|
|
|
1758
1780
|
}
|
|
1759
1781
|
}
|
|
1760
1782
|
|
|
1783
|
+
/* 需求拷问采访卡(借鉴 grill-me 一次一问的折中:一卡多问、芯片点选):
|
|
1784
|
+
* 点选项把「问题+所选」追加进目标框,凑齐后用户补一句即可发送;「跳过」直接创建 */
|
|
1785
|
+
function renderClarify(questions, goal, resetSubmit) {
|
|
1786
|
+
const box = $("clarify-box");
|
|
1787
|
+
if (!box) { resetSubmit(); return; }
|
|
1788
|
+
box.classList.remove("hidden");
|
|
1789
|
+
box.innerHTML =
|
|
1790
|
+
'<div class="cl-head"><svg class="ico" aria-hidden="true"><use href="#i-chat"></use></svg>' +
|
|
1791
|
+
'<b>' + esc(t("先对齐几个点,再做更准")) + "</b>" +
|
|
1792
|
+
'<button type="button" class="ghost small" id="clarify-skip">' + esc(t("跳过,直接做")) + "</button></div>" +
|
|
1793
|
+
questions.map((it, i) =>
|
|
1794
|
+
'<div class="cl-q"><div class="cl-qtext">' + (i + 1) + ". " + esc(it.q) + "</div>" +
|
|
1795
|
+
'<div class="cl-opts">' + (it.options || []).map((o) =>
|
|
1796
|
+
'<button type="button" class="cl-opt" data-q="' + esc(it.q) + '" data-o="' + esc(o) + '">' +
|
|
1797
|
+
esc(o) + "</button>").join("") + "</div></div>").join("");
|
|
1798
|
+
box.querySelectorAll(".cl-opt").forEach((b) => b.addEventListener("click", () => {
|
|
1799
|
+
const ta = $("f-goal");
|
|
1800
|
+
if (!ta) return;
|
|
1801
|
+
const line = it0safe(b.dataset.q, b.dataset.o);
|
|
1802
|
+
ta.value = ta.value.trim() + (ta.value.includes(line) ? "" : (ta.value ? "\n" : "") + line);
|
|
1803
|
+
b.classList.add("picked");
|
|
1804
|
+
b.disabled = true;
|
|
1805
|
+
}));
|
|
1806
|
+
function it0safe(q, o) { return q + ":" + o; }
|
|
1807
|
+
const skip = $("clarify-skip");
|
|
1808
|
+
if (skip) skip.addEventListener("click", () => {
|
|
1809
|
+
box.classList.add("hidden");
|
|
1810
|
+
box.innerHTML = "";
|
|
1811
|
+
S.clarifyDone = true; // 跳过 = 本轮不再采访
|
|
1812
|
+
$("btn-create").click();
|
|
1813
|
+
});
|
|
1814
|
+
$("f-goal").focus();
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1761
1817
|
async function createTask() {
|
|
1762
1818
|
if (S.creatingTask) return;
|
|
1763
1819
|
S.creatingTask = true;
|
|
@@ -1790,6 +1846,22 @@ async function createTask() {
|
|
|
1790
1846
|
resetSubmit();
|
|
1791
1847
|
return;
|
|
1792
1848
|
}
|
|
1849
|
+
// 需求拷问闸(借鉴 grill-me):goal 很短且没写背景时先问 1-3 个澄清问题;
|
|
1850
|
+
// 采访失败/无问题照常创建——是增强不是闸门
|
|
1851
|
+
if (payload.goal.length < 12 && !payload.context && !S.clarifyDone) {
|
|
1852
|
+
msg.textContent = t("目标有点简短,先问几个问题…");
|
|
1853
|
+
try {
|
|
1854
|
+
const cq = await api("/api/tasks/clarify", {
|
|
1855
|
+
method: "POST", body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
|
|
1856
|
+
const qs = (cq && cq.questions) || [];
|
|
1857
|
+
if (qs.length) {
|
|
1858
|
+
S.clarifyDone = true; // 本轮已采访;再点发送直接创建
|
|
1859
|
+
renderClarify(qs, payload.goal, resetSubmit);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
} catch (e) { /* 澄清失败照常创建 */ }
|
|
1863
|
+
}
|
|
1864
|
+
S.clarifyDone = false;
|
|
1793
1865
|
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
1794
1866
|
else unhideSideDir(payload.workdir); // 在已移除的目录新建任务 → 自动恢复显示
|
|
1795
1867
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
@@ -2684,7 +2756,8 @@ function renderRunList() {
|
|
|
2684
2756
|
' title="' + (can ? t("勾选以批量删除") : t("运行中的记录不可删除,请先取消")) + '"' +
|
|
2685
2757
|
' onclick="event.stopPropagation()" onchange="toggleRunSel(\'' + esc(r.id) + '\', this.checked)">' +
|
|
2686
2758
|
runKindTag(r.kind) +
|
|
2687
|
-
'<span class="name">' + esc(r.title) + "</span>" +
|
|
2759
|
+
'<span class="name">' + esc(r.title) + "</span>" +
|
|
2760
|
+
'<span class="chip ' + esc(r.status || "") + '">' + esc(runStatusText(r)) + "</span>" +
|
|
2688
2761
|
'<span class="time">' + esc(r.created_at) + "</span>" +
|
|
2689
2762
|
'<button class="danger small" title="' + t("删除该记录") + '" onclick="event.stopPropagation(); deleteRun(\'' + esc(r.id) + '\')">' + t("删除") + '</button></div>' +
|
|
2690
2763
|
'<div class="desc">' + esc(t(r.summary || r.error || (r.steps ? r.steps.length + t(" 个步骤") : ""))) + "</div></div>";
|
|
@@ -3073,10 +3146,9 @@ function renderTaskDetail() {
|
|
|
3073
3146
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
3074
3147
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
3075
3148
|
if (isTask) {
|
|
3076
|
-
|
|
3077
|
-
.then((r) => r.json())
|
|
3149
|
+
api("/api/tasks/" + encodeURIComponent(key) + "/runs")
|
|
3078
3150
|
.then((d) => { if (S.detailTaskKey === key) drawTaskDetail(key, d.runs || []); })
|
|
3079
|
-
.catch((e) => { /*
|
|
3151
|
+
.catch((e) => { /* 拉取失败静默,等下一轮轮询重试;401 已由 api() 弹令牌门 */ });
|
|
3080
3152
|
return;
|
|
3081
3153
|
}
|
|
3082
3154
|
drawTaskDetail(key, ((S.state || {}).runs || []).filter((r) => (r.task_id || r.id) === key));
|
|
@@ -3146,13 +3218,12 @@ function drawTaskDetail(key, runs) {
|
|
|
3146
3218
|
'<span class="stat">tokens <b>' + sum("tokens") + "</b></span>" +
|
|
3147
3219
|
(latest.error ? '<span class="stat err">' + errTag(latest.error) + esc(latest.error.slice(0, 200)) + "</span>" : "");
|
|
3148
3220
|
$("rd-plan").classList.add("hidden");
|
|
3149
|
-
const statusTxt = { queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") };
|
|
3150
3221
|
let html = "";
|
|
3151
3222
|
ordered.forEach((r, i) => {
|
|
3152
3223
|
const runNo = runs.length - i;
|
|
3153
3224
|
html += '<div class="run-sep"><span class="rs-i">' + t("第 ") + runNo + "/" + runs.length + t(" 次运行") + '</span>' +
|
|
3154
3225
|
'<span class="rs-t">' + esc(String(r.created_at || "").slice(5, 16)) + "</span>" +
|
|
3155
|
-
'<span class="chip ' + esc(r.status || "") + '">' + (
|
|
3226
|
+
'<span class="chip ' + esc(r.status || "") + '">' + esc(runStatusText(r)) + "</span>" +
|
|
3156
3227
|
(r.error ? '<span class="rs-err" title="' + esc(r.error.slice(0, 200)) + '">' + esc(r.error.slice(0, 60)) + "</span>" : "") +
|
|
3157
3228
|
"</div>";
|
|
3158
3229
|
html += (r.steps || []).slice().reverse().map((s) =>
|
|
@@ -3173,19 +3244,35 @@ function drawTaskDetail(key, runs) {
|
|
|
3173
3244
|
if (currentLog && !stepsMatch(runs, currentLog)) window.rdLogClose();
|
|
3174
3245
|
// 报告:最新一次 run 的(缓存,轮询重画不重复拉取)
|
|
3175
3246
|
const drawReport = async () => {
|
|
3247
|
+
const hint = (msg) => {
|
|
3248
|
+
if (S.detailTaskKey !== key) return; // 拉取期间切了详情:别糊到别的任务上
|
|
3249
|
+
$("rd-report").innerHTML = '<div class="hint">' + esc(msg) + "</div>";
|
|
3250
|
+
};
|
|
3176
3251
|
if (S.taskReport && S.taskReport.key === key && S.taskReport.runId === latest.id) {
|
|
3177
3252
|
$("rd-report").innerHTML = S.taskReport.html; return;
|
|
3178
3253
|
}
|
|
3179
3254
|
if (latest.status === "done" || latest.report) {
|
|
3180
3255
|
try {
|
|
3181
|
-
const
|
|
3256
|
+
const r = await fetch("/api/runs/" + encodeURIComponent(latest.id) + "/report",
|
|
3257
|
+
{ headers: authHeaders() });
|
|
3258
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
3259
|
+
const md = await r.text();
|
|
3260
|
+
if (!md.trim()) throw new Error("empty");
|
|
3182
3261
|
const html2 = md2html(md);
|
|
3183
3262
|
if (S.detailTaskKey !== key) return; // 拉取期间切了详情:过期报告不落缓存/不落盘
|
|
3184
3263
|
S.taskReport = { key, runId: latest.id, html: html2 };
|
|
3185
3264
|
$("rd-report").innerHTML = html2;
|
|
3186
|
-
} catch (e) {
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
// 拉取失败不再静默留白(手机端「报告空空」的来源之一):给可见提示,
|
|
3267
|
+
// 不落缓存,下一轮轮询签名变化时自动重试
|
|
3268
|
+
hint(t("报告读取失败(") + ((e || {}).message || t("网络异常")) + t("),稍后自动重试"));
|
|
3269
|
+
}
|
|
3270
|
+
} else if (latest.status === "running" || latest.status === "queued") {
|
|
3271
|
+
hint(t("本次运行进行中:报告结束后在这里生成,实时进度看「步骤」页签"));
|
|
3187
3272
|
} else {
|
|
3188
|
-
|
|
3273
|
+
const st = { failed: "失败", cancelled: "已取消", timeout: "超时" }[latest.status] || latest.status || "";
|
|
3274
|
+
hint(t("本次运行没有生成报告") + (st ? t("(状态:") + st + t(")") : "") +
|
|
3275
|
+
t(";各步骤日志在「步骤」页签"));
|
|
3189
3276
|
}
|
|
3190
3277
|
};
|
|
3191
3278
|
drawReport();
|
|
@@ -3481,7 +3568,7 @@ async function renderRunDetail() {
|
|
|
3481
3568
|
$("rd-title").textContent = run.title;
|
|
3482
3569
|
const chip = $("rd-status");
|
|
3483
3570
|
chip.className = "chip " + run.status;
|
|
3484
|
-
chip.textContent =
|
|
3571
|
+
chip.textContent = runStatusText(run);
|
|
3485
3572
|
const active = run.status === "queued" || run.status === "running";
|
|
3486
3573
|
$("btn-cancel").classList.toggle("hidden", !active);
|
|
3487
3574
|
S.cancelTargetRunId = active ? run.id : null;
|
|
@@ -3528,13 +3615,29 @@ async function renderRunDetail() {
|
|
|
3528
3615
|
applyStepFocus();
|
|
3529
3616
|
// 报告
|
|
3530
3617
|
if (run.status === "done" || run.report) {
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3618
|
+
try {
|
|
3619
|
+
const r = await fetch("/api/runs/" + encodeURIComponent(id) + "/report",
|
|
3620
|
+
{ headers: authHeaders() });
|
|
3621
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
3622
|
+
const md = await r.text();
|
|
3623
|
+
if (!md.trim()) throw new Error("empty");
|
|
3624
|
+
// 报告拉取期间切了详情:过期报告/成品不落盘(A 的报告写进 B 详情的报告区)
|
|
3625
|
+
if (S.detailRunId !== id || S.detailTaskKey) return;
|
|
3626
|
+
$("rd-report").innerHTML = md2html(md);
|
|
3627
|
+
} catch (e) {
|
|
3628
|
+
// 拉取失败不再静默留白:给可见提示(下轮轮询重画会重试)
|
|
3629
|
+
if (S.detailRunId === id && !S.detailTaskKey)
|
|
3630
|
+
$("rd-report").innerHTML = '<div class="hint">' +
|
|
3631
|
+
esc(t("报告读取失败(") + ((e || {}).message || t("网络异常")) + t("),稍后自动重试")) + "</div>";
|
|
3632
|
+
}
|
|
3535
3633
|
loadArtifacts(id);
|
|
3536
3634
|
} else {
|
|
3537
|
-
|
|
3635
|
+
const st = { failed: "失败", cancelled: "已取消", timeout: "超时" }[run.status] || run.status || "";
|
|
3636
|
+
$("rd-report").innerHTML = '<div class="hint">' +
|
|
3637
|
+
(run.status === "running" || run.status === "queued"
|
|
3638
|
+
? esc(t("本次运行进行中:报告结束后在这里生成,实时进度看「步骤」页签"))
|
|
3639
|
+
: esc(t("本次运行没有生成报告") + (st ? t("(状态:") + st + t(")") : "") + t(";各步骤日志在「步骤」页签"))) +
|
|
3640
|
+
"</div>";
|
|
3538
3641
|
}
|
|
3539
3642
|
// 成品文件双入口同源:主栏「成果」分区 + 检查器成品 TAB(列表上下文),
|
|
3540
3643
|
// 详情上下文检查器让位后主栏是唯一可见面
|
|
@@ -3746,6 +3849,7 @@ function renderBookMetaPanel(task) {
|
|
|
3746
3849
|
bmStatusChip(st) + '<span class="flex1"></span>' + action + "</div>";
|
|
3747
3850
|
return '<div class="bm-card st-' + esc(st || "new") + '">' + head + body +
|
|
3748
3851
|
(entry.source && st === "done" ? '<div class="bm-src">' + esc(t("来源:") + entry.source) + "</div>" : "") +
|
|
3852
|
+
pbBlock(task, p.id) +
|
|
3749
3853
|
"</div>";
|
|
3750
3854
|
}).join("") + "</div>";
|
|
3751
3855
|
box.classList.remove("hidden");
|
|
@@ -3758,6 +3862,7 @@ function renderBookMetaPanel(task) {
|
|
|
3758
3862
|
badge.textContent = anyRun ? "●" : (n ? n + "/" + BOOKMETA_PLATFORMS.length : "");
|
|
3759
3863
|
badge.className = "rd-badge" + (badge.textContent ? (anyRun ? " live" : "") : " hidden");
|
|
3760
3864
|
}
|
|
3865
|
+
pbSyncState(task); // 发布状态独立于 SSE(/api/publish),节流拉取+变化重渲染
|
|
3761
3866
|
}
|
|
3762
3867
|
|
|
3763
3868
|
window.bmGen = async function (taskId, platform) {
|
|
@@ -3774,6 +3879,202 @@ window.bmCopyBtn = function (btn) {
|
|
|
3774
3879
|
toast(t("已复制:") + (btn.dataset.label || ""));
|
|
3775
3880
|
};
|
|
3776
3881
|
|
|
3882
|
+
/* ---------------- 一键发布(bookmeta 分区内每平台卡的发布行) ----------------
|
|
3883
|
+
* 状态独立于 SSE(/api/publish 轮询,5s 节流;waiting_login/busy 靠主轮询周期
|
|
3884
|
+
* 自然刷新)。Phase 1 纪律:所有提交动作 auto_submit=false——表单填好后停,
|
|
3885
|
+
* 提交权留给用户在浏览器窗口里人工确认(建书/发章皆是)。 */
|
|
3886
|
+
const PB_ST = {
|
|
3887
|
+
none: ["未连接", "pb-st-none"], waiting_login: ["等扫码…", "pb-st-wait"],
|
|
3888
|
+
connected: ["已连接", "pb-st-ok"], busy: ["操作中…", "pb-st-busy"],
|
|
3889
|
+
error: ["出错", "pb-st-err"],
|
|
3890
|
+
};
|
|
3891
|
+
|
|
3892
|
+
function pbBlock(task, platform) {
|
|
3893
|
+
const ps = (S.pubState && S.pubState.platforms && S.pubState.platforms[platform]) || {};
|
|
3894
|
+
const st = ps.status || "none";
|
|
3895
|
+
const [label, cls] = PB_ST[st] || PB_ST.none;
|
|
3896
|
+
const books = (S.pubTaskInfo && S.pubTaskInfo.books) || {};
|
|
3897
|
+
const book = books[platform];
|
|
3898
|
+
const hist = (S.pubTaskInfo && S.pubTaskInfo.history) || [];
|
|
3899
|
+
const nCh = hist.filter((r) => r.platform === platform && r.action === "upload_chapter" && r.ok).length;
|
|
3900
|
+
const busy = st === "busy";
|
|
3901
|
+
let btns = "";
|
|
3902
|
+
if (st === "none" || st === "error" || st === "waiting_login") {
|
|
3903
|
+
btns += '<button class="ghost" onclick="pbConnect(\'' + platform + '\')">' +
|
|
3904
|
+
(st === "error" ? t("重连") : t("连接平台")) + "</button>";
|
|
3905
|
+
} else if (st === "connected") {
|
|
3906
|
+
btns += '<button class="ghost" onclick="pbDisconnect(\'' + platform + '\')" title="' + esc(t("关掉该平台的浏览器窗口(登录态保留)")) + '">' + t("断开") + "</button>";
|
|
3907
|
+
}
|
|
3908
|
+
btns += '<button class="ghost pb-tool" onclick="pbProbe(\'' + platform + '\')" title="' +
|
|
3909
|
+
esc(t("dump 平台表单结构(校准自动填表用)")) + '">' + t("探测") + "</button>";
|
|
3910
|
+
if (book) {
|
|
3911
|
+
btns += '<span class="pb-book" title="' + esc(t("已在此平台创建的作品")) + '">' +
|
|
3912
|
+
esc(t("已建书:") + (book.title || "")) + "</span>";
|
|
3913
|
+
btns += ' <button class="primary" ' + (busy ? "disabled" : "") +
|
|
3914
|
+
' onclick="pbUploadChapter(\'' + esc(task.id) + "', '" + platform + '\')">' +
|
|
3915
|
+
t("发一章") + (nCh ? "(已发 " + nCh + ")" : "") + "</button>";
|
|
3916
|
+
// 批量发布(publish/auto.py):待发清单 + 护栏 + 进度都来自 /pending 视图
|
|
3917
|
+
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
3918
|
+
.find((b) => b.platform === platform)) || {};
|
|
3919
|
+
const run = (S.pubAuto && S.pubAuto.running &&
|
|
3920
|
+
S.pubAuto.running.platform === platform) ? S.pubAuto.running : null;
|
|
3921
|
+
if (run && run.status === "running") {
|
|
3922
|
+
btns += '<span class="pb-book">' + esc(t("自动发布中 ") + run.done + "/" + run.total) + "</span>";
|
|
3923
|
+
} else if (au.pending > 0) {
|
|
3924
|
+
btns += ' <button class="ghost" ' + (busy || !au.guard_ok ? "disabled" : "") +
|
|
3925
|
+
' title="' + esc(au.guard_ok ? t("按章号顺序逐章填稿(人工模式每点一次填一章,浏览器里提交后再点发下一章)") : au.guard_reason || "") + '"' +
|
|
3926
|
+
' onclick="pbPublishAll(\'' + esc(task.id) + "', '" + platform + '\')">' +
|
|
3927
|
+
t("发布全部待发") + "(" + au.pending + ")</button>";
|
|
3928
|
+
}
|
|
3929
|
+
if (au.pending > 0 && !au.guard_ok) {
|
|
3930
|
+
btns += '<div class="pb-err">' + esc(au.guard_reason || t("护栏拦截")) + "</div>";
|
|
3931
|
+
}
|
|
3932
|
+
if (run && run.status === "manual_pause") {
|
|
3933
|
+
// 人工模式一轮只填一章:等用户在浏览器提交后再发起(连发会导航离开
|
|
3934
|
+
// 未提交的编辑器丢稿)。message 是下一步指引,不是错误。
|
|
3935
|
+
btns += '<div class="pb-hint">' + esc(run.message || t("已填好一章,请在浏览器确认提交")) + "</div>";
|
|
3936
|
+
} else if (run && run.status !== "running") {
|
|
3937
|
+
const doneLine = run.status === "done"
|
|
3938
|
+
? t("自动发布完成:") + run.done + "/" + run.total + (run.message ? "。" + run.message : "")
|
|
3939
|
+
: t("自动发布中断:") + (run.error || "");
|
|
3940
|
+
btns += '<div class="' + (run.status === "done" ? "pb-hint" : "pb-err") + '">' +
|
|
3941
|
+
esc(doneLine + "(" + (run.at || "") + ")") + "</div>";
|
|
3942
|
+
}
|
|
3943
|
+
} else {
|
|
3944
|
+
btns += '<button class="primary" ' + (busy ? "disabled" : "") +
|
|
3945
|
+
' onclick="pbCreateBook(\'' + esc(task.id) + "', '" + platform + '\')">' + t("创建作品") + "</button>";
|
|
3946
|
+
}
|
|
3947
|
+
return '<div class="bm-pub">' +
|
|
3948
|
+
'<span class="pb-badge ' + cls + '">' + esc(t(label)) + "</span>" +
|
|
3949
|
+
btns +
|
|
3950
|
+
(st === "error" && ps.error ? '<div class="pb-err">' + esc(ps.error) + "</div>" : "") +
|
|
3951
|
+
'<div class="pb-chapters hidden" id="pb-ch-' + platform + '"></div>' +
|
|
3952
|
+
"</div>";
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
let _pbTimer = 0;
|
|
3956
|
+
function pbSyncState(task) {
|
|
3957
|
+
clearTimeout(_pbTimer);
|
|
3958
|
+
_pbTimer = setTimeout(async () => {
|
|
3959
|
+
const now = Date.now();
|
|
3960
|
+
if (now - (S.pubFetchAt || 0) < 5000) return; // 节流:主轮询不每次都打 /api/publish
|
|
3961
|
+
S.pubFetchAt = now;
|
|
3962
|
+
let sig = "";
|
|
3963
|
+
try {
|
|
3964
|
+
const v = await api("/api/publish");
|
|
3965
|
+
S.pubState = v;
|
|
3966
|
+
sig += JSON.stringify(v.platforms || {});
|
|
3967
|
+
} catch (e) { return; }
|
|
3968
|
+
try {
|
|
3969
|
+
const ti = await api("/api/publish/task/" + encodeURIComponent(task.id) + "/history");
|
|
3970
|
+
S.pubTaskInfo = ti;
|
|
3971
|
+
sig += "#" + JSON.stringify(ti.books || {}) + "#" + (ti.history || []).length;
|
|
3972
|
+
} catch (e) { /* 任务级失败不阻塞平台状态 */ }
|
|
3973
|
+
try {
|
|
3974
|
+
// 批量发布视图(待发数/护栏/进度)——进行中时靠本节流轮询自然刷新
|
|
3975
|
+
const au = await api("/api/publish/task/" + encodeURIComponent(task.id) + "/pending");
|
|
3976
|
+
S.pubAuto = au;
|
|
3977
|
+
sig += "@" + JSON.stringify(au.books || {}) + "@" + JSON.stringify(au.running || {});
|
|
3978
|
+
} catch (e) { /* 视图缺失(老服务)不阻塞 */ }
|
|
3979
|
+
if (sig !== (S._pbSig || "") && !$("rd-bookmeta").classList.contains("hidden")) {
|
|
3980
|
+
S._pbSig = sig;
|
|
3981
|
+
renderBookMetaPanel(task);
|
|
3982
|
+
} else S._pbSig = sig;
|
|
3983
|
+
}, 120);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
window.pbConnect = async function (platform) {
|
|
3987
|
+
try {
|
|
3988
|
+
await api("/api/publish/" + platform + "/connect", { method: "POST", body: "{}" });
|
|
3989
|
+
toast(t("已打开浏览器——请在窗口里登录平台,登录后这里自动变为「已连接」"));
|
|
3990
|
+
} catch (e) { toast(t("连接失败:") + e.message, true); }
|
|
3991
|
+
S._pbSig = ""; pbKick();
|
|
3992
|
+
};
|
|
3993
|
+
|
|
3994
|
+
window.pbDisconnect = async function (platform) {
|
|
3995
|
+
try { await api("/api/publish/" + platform + "/disconnect", { method: "POST", body: "{}" }); }
|
|
3996
|
+
catch (e) { toast(t("断开失败:") + e.message, true); }
|
|
3997
|
+
S._pbSig = ""; pbKick();
|
|
3998
|
+
};
|
|
3999
|
+
|
|
4000
|
+
window.pbProbe = async function (platform) {
|
|
4001
|
+
try {
|
|
4002
|
+
await api("/api/publish/" + platform + "/probe", { method: "POST", body: "{}" });
|
|
4003
|
+
toast(t("已开始探测,结果记录在发布台账(data/publish)里"));
|
|
4004
|
+
} catch (e) { toast(t("探测失败:") + e.message, true); }
|
|
4005
|
+
S._pbSig = ""; pbKick();
|
|
4006
|
+
};
|
|
4007
|
+
|
|
4008
|
+
window.pbCreateBook = async function (taskId, platform) {
|
|
4009
|
+
if (!confirm(t("将用生成的作品信息在平台自动填建书表单。填好后会停在最后一步,由你在浏览器里人工点提交。继续?"))) return;
|
|
4010
|
+
try {
|
|
4011
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/create-book",
|
|
4012
|
+
{ method: "POST", body: JSON.stringify({ platform }) });
|
|
4013
|
+
toast(t("正在自动填写建书表单…(完成后请在浏览器里确认提交)"));
|
|
4014
|
+
} catch (e) { toast(t("建书失败:") + e.message, true); }
|
|
4015
|
+
S._pbSig = ""; pbKick();
|
|
4016
|
+
};
|
|
4017
|
+
|
|
4018
|
+
window.pbUploadChapter = async function (taskId, platform) {
|
|
4019
|
+
const box = $("pb-ch-" + platform);
|
|
4020
|
+
if (!box) return;
|
|
4021
|
+
if (!box.classList.contains("hidden")) { box.classList.add("hidden"); return; }
|
|
4022
|
+
box.classList.remove("hidden");
|
|
4023
|
+
box.innerHTML = '<div class="pb-loading">' + esc(t("正在列出章节文件…")) + "</div>";
|
|
4024
|
+
let wd = "";
|
|
4025
|
+
try { const tk = ((S.state || {}).tasks || []).find((x) => x.id === taskId); wd = (tk && tk.workdir) || ""; } catch (e) { /* 兜底空 */ }
|
|
4026
|
+
if (!wd) { box.innerHTML = '<div class="pb-err">' + esc(t("找不到任务工作目录")) + "</div>"; return; }
|
|
4027
|
+
let files = [];
|
|
4028
|
+
try {
|
|
4029
|
+
const r = await api("/api/dir/scan?path=" + encodeURIComponent(wd));
|
|
4030
|
+
files = (r.files || []).filter((f) => /\.md$/i.test(f.name || "") &&
|
|
4031
|
+
!/^作品信息/.test((f.name || "").split("/").pop()));
|
|
4032
|
+
} catch (e) {
|
|
4033
|
+
box.innerHTML = '<div class="pb-err">' + esc(t("列文件失败:") + e.message) + "</div>";
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
if (!files.length) { box.innerHTML = '<div class="pb-err">' + esc(t("工作目录里没有 .md 章稿")) + "</div>"; return; }
|
|
4037
|
+
files.sort((a, b) => (a.name > b.name ? 1 : -1));
|
|
4038
|
+
box.innerHTML = '<div class="pb-hint">' + esc(t("选一章发送(表单填好后停,人工确认提交);已成功发过的章会被台账拦下:")) + "</div>" +
|
|
4039
|
+
files.map((f) =>
|
|
4040
|
+
'<button class="ghost pb-ch-item" onclick="pbSendChapter(\'' + esc(taskId) + "', '" + platform +
|
|
4041
|
+
'\',\'' + esc(f.name) + '\')" title="' + esc(f.name) + '">' + esc(f.name.split("/").pop()) + "</button>"
|
|
4042
|
+
).join("");
|
|
4043
|
+
};
|
|
4044
|
+
|
|
4045
|
+
window.pbSendChapter = async function (taskId, platform, relName) {
|
|
4046
|
+
if (!confirm(t("将把《" + relName + "》填进平台章节编辑器,填好后由你人工提交。继续?"))) return;
|
|
4047
|
+
try {
|
|
4048
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/chapter",
|
|
4049
|
+
{ method: "POST", body: JSON.stringify({ platform, file: relName }) });
|
|
4050
|
+
toast(t("正在填写章节…(完成后请在浏览器里确认提交)"));
|
|
4051
|
+
const box = $("pb-ch-" + platform);
|
|
4052
|
+
if (box) box.classList.add("hidden");
|
|
4053
|
+
} catch (e) { toast(t("发章失败:") + e.message, true); }
|
|
4054
|
+
S._pbSig = ""; pbKick();
|
|
4055
|
+
};
|
|
4056
|
+
|
|
4057
|
+
window.pbPublishAll = async function (taskId, platform) {
|
|
4058
|
+
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
4059
|
+
.find((b) => b.platform === platform)) || {};
|
|
4060
|
+
if (!au.pending) return;
|
|
4061
|
+
if (!confirm(t("将从最靠前的待发章节开始填稿(共 " + au.pending +
|
|
4062
|
+
" 章待发)。本轮只填一章并停在表单页,由你在浏览器里确认提交;提交后再点一次即发下一章。护栏(每日上限/连续失败暂停)生效。继续?"))) return;
|
|
4063
|
+
try {
|
|
4064
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
4065
|
+
{ method: "POST", body: JSON.stringify({ platform }) });
|
|
4066
|
+
toast(t("正在填第一章稿——填好后请在浏览器窗口里确认提交"));
|
|
4067
|
+
} catch (e) { toast(t("自动发布失败:") + e.message, true); }
|
|
4068
|
+
S._pbSig = ""; pbKick();
|
|
4069
|
+
};
|
|
4070
|
+
|
|
4071
|
+
function pbKick() {
|
|
4072
|
+
const key = detailSideTaskKey();
|
|
4073
|
+
if (!key) return;
|
|
4074
|
+
const tk = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
4075
|
+
if (tk) { S.pubFetchAt = 0; pbSyncState(tk); }
|
|
4076
|
+
}
|
|
4077
|
+
|
|
3777
4078
|
/* ---------------- 详情页任务级 side 数据(检查器让位后的主栏自给) ----------------
|
|
3778
4079
|
* 检查器收进列表上下文后,任务累计统计与 git 实时状态由详情页自己轮询
|
|
3779
4080
|
* /api/tasks/<id>/side(KB 级、2s 节流,与检查器同款)。拉不到(无主运行、
|
|
@@ -4099,8 +4400,13 @@ window.gitWbFetch = function () { return _gitWbPost("fetch", null, t("已抓取
|
|
|
4099
4400
|
window.gitWbPull = function () { return _gitWbPost("pull", null, t("已拉取远程更新")); };
|
|
4100
4401
|
window.gitWbPush = function () { return _gitWbPost("push", null, t("已推送到远程")); };
|
|
4101
4402
|
window.gitWbPrCreate = async function () {
|
|
4102
|
-
|
|
4103
|
-
const
|
|
4403
|
+
// 提交信息框有字时兼作 PR 标题(一处输入两用,不为 PR 单开输入框)
|
|
4404
|
+
const mt = ((($("gwb-msg") || {}).value || "").trim());
|
|
4405
|
+
const q = mt
|
|
4406
|
+
? t("把当前分支推送到远程并用 gh 创建 PR?(目标分支自动取 main/master;提交信息框内容将作为 PR 标题)")
|
|
4407
|
+
: t("把当前分支推送到远程并用 gh 创建 PR?(目标分支自动取 main/master)");
|
|
4408
|
+
if (!await uiConfirm(q, { ok: t("建 PR") })) return null;
|
|
4409
|
+
const res = await _gitWbPost("pr_create", mt ? { title: mt.slice(0, 120) } : null,
|
|
4104
4410
|
(r) => (r && r.url ? t("PR 已创建:") + r.url : t("PR 已创建")));
|
|
4105
4411
|
if (res && res.url) window.open(res.url, "_blank", "noopener");
|
|
4106
4412
|
return res;
|
|
@@ -4408,8 +4714,8 @@ function drawInspector() {
|
|
|
4408
4714
|
const artBox = $("insp-artifacts");
|
|
4409
4715
|
if (artBox) artBox.innerHTML = arts.length
|
|
4410
4716
|
? arts.map((f) =>
|
|
4411
|
-
'<a class="file-chip" href="/api/runs/
|
|
4412
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
4717
|
+
'<a class="file-chip" href="' + urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" +
|
|
4718
|
+
encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" title="' +
|
|
4413
4719
|
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) +
|
|
4414
4720
|
'" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4415
4721
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
@@ -4580,6 +4886,7 @@ function _fpExt(name) { return (String(name).split(".").pop() || "").toLowerCase
|
|
|
4580
4886
|
* 上下文 → 额外给「编辑」,改完保存回工作目录(POST /api/dir/save)。 */
|
|
4581
4887
|
async function _fpPreviewUrl(url, name, size, opts) {
|
|
4582
4888
|
opts = opts || {};
|
|
4889
|
+
url = urlAuth(url); // 预览 fetch 与下载链接都走这个 URL:远程会话在此统一补令牌
|
|
4583
4890
|
const ext = _fpExt(name);
|
|
4584
4891
|
_fpSaveCtx = opts.save || null;
|
|
4585
4892
|
_fpRawText = ""; _fpDirty = false; _fpCopyText = null;
|
|
@@ -4775,8 +5082,8 @@ function artifactsChips(runId, files) {
|
|
|
4775
5082
|
return files.map((f) => {
|
|
4776
5083
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
4777
5084
|
return '<span class="fc-row">' +
|
|
4778
|
-
'<a class="file-chip artifact-file-open" href="/api/runs/
|
|
4779
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
5085
|
+
'<a class="file-chip artifact-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" +
|
|
5086
|
+
encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" ' +
|
|
4780
5087
|
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4781
5088
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
4782
5089
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>" +
|
|
@@ -5574,8 +5881,8 @@ function chatResultHTML(run, res) {
|
|
|
5574
5881
|
if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
|
|
5575
5882
|
const files = res.files || [];
|
|
5576
5883
|
const chips = files.map((f) =>
|
|
5577
|
-
'<a class="file-chip chat-file-open" href="/api/runs/
|
|
5578
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
5884
|
+
'<a class="file-chip chat-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(run.id) + "/file?name=" +
|
|
5885
|
+
encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" title="' +
|
|
5579
5886
|
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(run.id) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
5580
5887
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
5581
5888
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>").join("");
|
|
@@ -5634,8 +5941,7 @@ function renderRunOutcome(run, runs) {
|
|
|
5634
5941
|
const status = String(run.status || "");
|
|
5635
5942
|
const active = status === "running" || status === "queued";
|
|
5636
5943
|
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5637
|
-
const label =
|
|
5638
|
-
: (status === "done" ? t("完成") : status === "failed" ? t("失败") : status === "cancelled" ? t("已取消") : status || t("未知状态"));
|
|
5944
|
+
const label = runStatusText(run);
|
|
5639
5945
|
const icon = status === "done" ? "#i-check" : bad ? "#i-x" : "#i-gauge";
|
|
5640
5946
|
const duration = runDurationSeconds(run);
|
|
5641
5947
|
const latest = steps.slice().reverse().find((s) => s.agent_label || s.agent || s.role);
|
|
@@ -5677,9 +5983,7 @@ function renderDetailOverview(run, runs, task) {
|
|
|
5677
5983
|
const status = String(run.status || "");
|
|
5678
5984
|
const active = status === "running" || status === "queued";
|
|
5679
5985
|
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5680
|
-
const statusText =
|
|
5681
|
-
: status === "done" ? t("完成") : status === "failed" ? t("失败")
|
|
5682
|
-
: status === "cancelled" ? t("已取消") : status || t("未知状态");
|
|
5986
|
+
const statusText = runStatusText(run);
|
|
5683
5987
|
const last = steps.slice().reverse().find((s) => s.summary || s.output || s.note) || steps[steps.length - 1];
|
|
5684
5988
|
const lastText = last ? String(last.summary || last.output || last.note || "").replace(/\s+/g, " ").trim().slice(0, 240) : "";
|
|
5685
5989
|
const executor = run.executor || run.implementer || (last && (last.agent_label || last.agent)) || "";
|
package/app/ui/i18n.js
CHANGED
|
@@ -681,6 +681,10 @@
|
|
|
681
681
|
"建 PR": "Create PR",
|
|
682
682
|
"推送当前分支并用 gh 创建 PR(目标分支自动取 main/master)": "Push current branch and create a PR with gh (base auto-detected: main/master)",
|
|
683
683
|
"把当前分支推送到远程并用 gh 创建 PR?(目标分支自动取 main/master)": "Push current branch and create a PR with gh? (base auto-detected: main/master)",
|
|
684
|
+
"把当前分支推送到远程并用 gh 创建 PR?(目标分支自动取 main/master;提交信息框内容将作为 PR 标题)": "Push current branch and create a PR with gh? (base auto-detected: main/master; the commit message box will be used as the PR title)",
|
|
685
|
+
"目标有点简短,先问几个问题…": "That's a bit brief — a few questions first…",
|
|
686
|
+
"先对齐几个点,再做更准": "Quick alignment before we build",
|
|
687
|
+
"跳过,直接做": "Skip, just do it",
|
|
684
688
|
"PR 已创建:": "PR created: ",
|
|
685
689
|
"PR 已创建": "PR created",
|
|
686
690
|
"背景信息、约束、相关文件(可空)": "Background, constraints, related files (optional)",
|
|
@@ -1032,7 +1036,7 @@
|
|
|
1032
1036
|
"当前配置不生效:请检查供应商密钥、启停状态与模型选择。": "Current config is not active: check the API key, enabled state, and model selection.",
|
|
1033
1037
|
"最大并发任务数(多任务同时跑、互不打扰)": "Max concurrent tasks (run in parallel, fully isolated)",
|
|
1034
1038
|
"1 · 串行": "1 · serial",
|
|
1035
|
-
"1 =
|
|
1039
|
+
"1 = 串行排队;任务多可开 6-12 并行。同一任务仍单飞(防双烧评审),跨任务互不打扰;排队超 2 分钟会自动补队自愈。": "1 = serial queue; open 6-12 when running many tasks. Same task stays single-flight (no double review); tasks don't block each other; anything stuck in queue over 2 minutes self-heals automatically.",
|
|
1036
1040
|
"默认保存路径(新建任务未指定工作目录时使用;支持 ~)": "Default save path (used when a new task doesn't specify one; ~ supported)",
|
|
1037
1041
|
"保存时把「旧默认路径下」的现有任务目录迁移到新路径(运行中的跳过;手动指定的目录不受影响)": "When saving, migrate existing task directories under the old default path to the new one (skip running tasks; manually specified paths are untouched)",
|
|
1038
1042
|
"不沿用(全新开始)": "Don't resume (fresh start)",
|
package/app/ui/index.html
CHANGED
|
@@ -290,6 +290,8 @@
|
|
|
290
290
|
<div id="cmp-quick" class="cmp-quick" role="group" aria-label="常用类型" data-i18n-aria="常用类型"></div>
|
|
291
291
|
<!-- 开跑前成本预估(refreshEstimate 填充;无同类样本时保持隐藏) -->
|
|
292
292
|
<p class="hint cmp-estimate hidden" id="cmp-estimate"></p>
|
|
293
|
+
<!-- 需求拷问采访卡(renderClarify 渲染;goal 模糊时出现,跳过=直接创建) -->
|
|
294
|
+
<div id="clarify-box" class="clarify-box hidden"></div>
|
|
293
295
|
<div class="field hidden" id="row-resume-session">
|
|
294
296
|
<label data-i18n="选择会话">选择会话</label>
|
|
295
297
|
<select id="f-resume-session"><option data-i18n="(加载中…)">(加载中…)</option></select>
|
|
@@ -324,7 +326,7 @@
|
|
|
324
326
|
<div id="bible-create-field" class="field hidden">
|
|
325
327
|
<label><svg class="ico" aria-hidden="true"><use href="#i-book"></use></svg><span data-i18n="初始故事圣经">初始故事圣经</span><span class="field-optional" data-i18n="可选">可选</span></label>
|
|
326
328
|
<textarea id="f-bible" rows="5" placeholder="人物、世界观、主线冲突、伏笔和不能违背的设定;会在开跑前写入 story-bible.md" data-i18n-ph="人物、世界观、主线冲突、伏笔和不能违背的设定;会在开跑前写入 story-bible.md"></textarea>
|
|
327
|
-
<p class="hint" data-i18n="连载任务会在每章起草与评审时自动注入;工作目录已有 story-bible.md 时请留空,避免覆盖已有设定。">连载任务会在每章起草与评审时自动注入;工作目录已有 story-bible.md
|
|
329
|
+
<p class="hint" data-i18n="连载任务会在每章起草与评审时自动注入;工作目录已有 story-bible.md 时请留空,避免覆盖已有设定。">连载任务会在每章起草与评审时自动注入;工作目录已有 story-bible.md 时请留空,避免覆盖已有设定。另可放 plot-modules.md 剧情模块库(拆文沉淀的桥段/爽点素材,每章自动注入)。</p>
|
|
328
330
|
</div>
|
|
329
331
|
<div class="grid-3">
|
|
330
332
|
<div class="field"><label data-i18n="稿件文件(合并稿)">稿件文件(合并稿)</label><input id="f-manuscript" value="manuscript.md" placeholder="工作目录内的文件名" data-i18n-ph="工作目录内的文件名"></div>
|
|
@@ -675,10 +677,10 @@
|
|
|
675
677
|
<h3 data-i18n="运行设置" class="sec-title">运行设置</h3>
|
|
676
678
|
<div class="field"><label data-i18n="最大并发任务数(多任务同时跑、互不打扰)">最大并发任务数(多任务同时跑、互不打扰)</label>
|
|
677
679
|
<div class="input-row">
|
|
678
|
-
<input id="set-workers" type="number" min="1" max="
|
|
680
|
+
<input id="set-workers" type="number" min="1" max="12" value="6">
|
|
679
681
|
<button data-i18n="保存" class="ghost small" onclick="saveSettings()">保存</button>
|
|
680
682
|
</div>
|
|
681
|
-
<p data-i18n="1 =
|
|
683
|
+
<p data-i18n="1 = 串行排队;任务多可开 6-12 并行。同一任务仍单飞(防双烧评审),跨任务互不打扰;排队超 2 分钟会自动补队自愈。" class="hint">1 = 串行排队;任务多可开 6-12 并行。同一任务仍单飞(防双烧评审),跨任务互不打扰;排队超 2 分钟会自动补队自愈。</p>
|
|
682
684
|
</div>
|
|
683
685
|
<div class="field"><label data-i18n="默认保存路径(新建任务未指定工作目录时使用;支持 ~)">默认保存路径(新建任务未指定工作目录时使用;支持 ~)</label>
|
|
684
686
|
<div class="input-row">
|