codebee 0.1.3 → 0.1.5
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 +36 -0
- package/README.md +9 -0
- package/app/core/attachments.py +40 -0
- package/app/core/bookmeta.py +157 -52
- package/app/core/bookmeta_catalog.py +67 -90
- package/app/core/builtin_agent.py +564 -0
- package/app/core/flows.py +328 -328
- package/app/core/gitmod.py +82 -8
- package/app/core/history.py +8 -2
- package/app/core/jobs.py +448 -424
- package/app/core/manager.py +151 -1
- package/app/core/modelhub.py +86 -4
- package/app/core/pipeline.py +2326 -2164
- package/app/core/router.py +8 -3
- package/app/core/runner.py +185 -21
- package/app/core/selfupdate.py +54 -11
- package/app/core/store.py +4 -2
- package/app/main.py +117 -12
- package/app/ui/app.js +279 -47
- package/app/ui/i18n.js +21 -5
- package/app/ui/index.html +6 -2
- package/app/ui/style.css +3379 -2764
- package/package.json +2 -1
package/app/ui/app.js
CHANGED
|
@@ -488,10 +488,12 @@ function fmtModel(m) {
|
|
|
488
488
|
return typeof m === "object" ? JSON.stringify(m) : String(m);
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
/* 极简 Markdown
|
|
491
|
+
/* 极简 Markdown 渲染(标题/加粗/行内码/列表/表格/代码块)。
|
|
492
|
+
* 围栏代码块走 codeBlockHTML(行号表格+高亮+代码主题):裸 <pre> 只有
|
|
493
|
+
* --log-bg 底色没配浅字,暖色皮肤里深底配深字根本读不了。 */
|
|
492
494
|
function md2html(md) {
|
|
493
495
|
const lines = String(md || "").split(/\r?\n/);
|
|
494
|
-
let html = [], inCode = false, inTable = false, listOpen = false;
|
|
496
|
+
let html = [], inCode = false, codeBuf = null, inTable = false, listOpen = false;
|
|
495
497
|
const inline = (t) => esc(t)
|
|
496
498
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
497
499
|
.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
|
|
@@ -501,10 +503,11 @@ function md2html(md) {
|
|
|
501
503
|
const line = raw.replace(/\s+$/, "");
|
|
502
504
|
if (line.startsWith("```")) {
|
|
503
505
|
closeList(); closeTable();
|
|
504
|
-
html.push(
|
|
506
|
+
if (inCode) { html.push(codeBlockHTML(codeBuf.join("\n"))); codeBuf = null; }
|
|
507
|
+
else codeBuf = [];
|
|
505
508
|
inCode = !inCode; continue;
|
|
506
509
|
}
|
|
507
|
-
if (inCode) {
|
|
510
|
+
if (inCode) { codeBuf.push(raw); continue; }
|
|
508
511
|
if (/^\|(.+)\|\s*$/.test(line)) {
|
|
509
512
|
const cells = line.slice(1, -1).split("|").map((s) => s.trim());
|
|
510
513
|
if (/^[-: ]+$/.test(cells.join(""))) continue; // 分隔行
|
|
@@ -524,7 +527,7 @@ function md2html(md) {
|
|
|
524
527
|
html.push("<p>" + inline(line) + "</p>");
|
|
525
528
|
}
|
|
526
529
|
closeList(); closeTable();
|
|
527
|
-
if (inCode) html.push("
|
|
530
|
+
if (inCode && codeBuf) html.push(codeBlockHTML(codeBuf.join("\n"))); // 未闭合围栏也按代码块收尾
|
|
528
531
|
return html.join("\n");
|
|
529
532
|
}
|
|
530
533
|
|
|
@@ -1263,6 +1266,10 @@ function provModelRow(pid, m, group) {
|
|
|
1263
1266
|
esc((tm.error || "").slice(0, 24)) + "</span>") : "";
|
|
1264
1267
|
const price = (m.price_in != null && m.price_out != null)
|
|
1265
1268
|
? '<span class="hint">¥' + esc(m.price_in) + "/¥" + esc(m.price_out) + "</span>" : "";
|
|
1269
|
+
const imgcap = '<span class="tag imgcap' + (m.image_in ? " ok" : "") + '" title="' +
|
|
1270
|
+
(m.image_in ? t("支持图片输入,点击关闭")
|
|
1271
|
+
: t("纯文本模型,点击开启图片输入(内置智能体传图以此为准)")) + '"' +
|
|
1272
|
+
' onclick="toggleModelImage(\'' + esc(pid) + '\', \'' + esc(m.name) + '\')">' + t("图") + "</span>";
|
|
1266
1273
|
const sel = modelSel(pid);
|
|
1267
1274
|
return '<div class="prow' + (m.enabled ? "" : " off") + '" draggable="true" data-group="' +
|
|
1268
1275
|
esc(group) + '" data-name="' + esc(m.name) + '">' +
|
|
@@ -1272,7 +1279,7 @@ function provModelRow(pid, m, group) {
|
|
|
1272
1279
|
'<span class="drag" title="' + t("拖动调整优先级") + '"><svg class="ico" aria-hidden="true"><use href="#i-grip"></use></svg></span>' +
|
|
1273
1280
|
'<span class="pprio">#' + m.priority + "</span>" +
|
|
1274
1281
|
'<span class="pname" title="' + esc(m.name) + '">' + esc(m.name) + "</span>" +
|
|
1275
|
-
price + tmHtml +
|
|
1282
|
+
price + imgcap + tmHtml +
|
|
1276
1283
|
'<span class="row-ops">' +
|
|
1277
1284
|
'<button class="ghost small row-op" onclick="testModelBtn(\'' + esc(pid) + '\', \'' + esc(m.name) + '\')">' + t("测试") + '</button>' +
|
|
1278
1285
|
'<button class="danger small row-op" title="' + t("从列表删除:刷新/重新导入不会再带回,可在分组底部恢复") + '"' +
|
|
@@ -1359,6 +1366,21 @@ async function modelOp(pid, name, op) {
|
|
|
1359
1366
|
poll();
|
|
1360
1367
|
}
|
|
1361
1368
|
|
|
1369
|
+
function toggleModelImage(pid, name) {
|
|
1370
|
+
const p = (S.providers || []).find((x) => x.id === pid);
|
|
1371
|
+
const m = p && (p.models || []).find((x) => x.name === name);
|
|
1372
|
+
api("/api/models/model-caps", { method: "POST",
|
|
1373
|
+
body: JSON.stringify({ provider_id: pid, name, image_in: !(m && m.image_in) }) })
|
|
1374
|
+
.then(() => { S.modelsSig = null; poll(); })
|
|
1375
|
+
.catch((e) => toast(t("保存失败:") + e.message, true));
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/* 反查模型是否声明图片输入(绑定页 chip/列表只读徽标用) */
|
|
1379
|
+
function modelHasImage(pid, name) {
|
|
1380
|
+
const p = (S.providers || []).find((x) => x.id === pid);
|
|
1381
|
+
return !!(p && (p.models || []).some((x) => x.name === name && x.image_in));
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1362
1384
|
/* ---- 模型批量选择 ---- */
|
|
1363
1385
|
function toggleModelSel(pid, name, on) {
|
|
1364
1386
|
const s = modelSelSet(pid);
|
|
@@ -1786,6 +1808,9 @@ async function createTask() {
|
|
|
1786
1808
|
try {
|
|
1787
1809
|
const r = await api("/api/tasks", { method: "POST", body: JSON.stringify(payload) });
|
|
1788
1810
|
msg.textContent = t("已创建,跳转运行页…");
|
|
1811
|
+
// 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
|
|
1812
|
+
// 不刷的话对话页签不会就绪,自动选卡落不到「对话」
|
|
1813
|
+
try { await refreshState(); } catch (e) { /* 刷失败等轮询兜底 */ }
|
|
1789
1814
|
jumpToRun(r.run_id);
|
|
1790
1815
|
$("f-goal").value = "";
|
|
1791
1816
|
S.atts = []; renderAttachChips(); // 附件已移交任务待提交区,清空本地列表
|
|
@@ -2083,7 +2108,7 @@ function bindCtxMenus() {
|
|
|
2083
2108
|
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2084
2109
|
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2085
2110
|
const st = det.dataset.status || "";
|
|
2086
|
-
if (st === "failed" || st === "cancelled") items.push({ label: t("↻
|
|
2111
|
+
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2087
2112
|
// 与详情页同一套说法:失败/取消叫「编辑重试」,其余叫「基于此任务新建」
|
|
2088
2113
|
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2089
2114
|
fn: () => newFromTask(taskId) });
|
|
@@ -3093,6 +3118,7 @@ function closeRun() {
|
|
|
3093
3118
|
stopHiveTick();
|
|
3094
3119
|
detailSideReset();
|
|
3095
3120
|
$("run-detail").classList.add("hidden");
|
|
3121
|
+
renderChatNav(); // 解除 main.chat-fill(对话为主的固定高度),恢复外层滚动
|
|
3096
3122
|
if (document.body.classList.contains("settings-mode")) {
|
|
3097
3123
|
document.querySelector("#sub-runs .panel:first-child").classList.remove("hidden");
|
|
3098
3124
|
} else {
|
|
@@ -3152,7 +3178,55 @@ function applyRdTabs() {
|
|
|
3152
3178
|
});
|
|
3153
3179
|
document.querySelectorAll("#run-detail .rd-pane").forEach((p) =>
|
|
3154
3180
|
p.classList.toggle("hidden", p.dataset.pane !== S.rdTab));
|
|
3155
|
-
|
|
3181
|
+
// 对话页签不放日志抽屉:会盖住贴底输入条(手动切来/自动选卡都覆盖)
|
|
3182
|
+
if (S.rdTab === "chat" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
|
|
3183
|
+
renderChatNav();
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
/* 直连任务「对话为主」布局:收起常规页签条,右上角一排小胶囊按需打开
|
|
3187
|
+
* 蜂巢/步骤/成果等分区;离开对话时给「返回对话」入口(用户 2026-09-17 拍板:
|
|
3188
|
+
* 对话场景其它页签用处不大,默认关掉、需要再点开)。 */
|
|
3189
|
+
function renderChatNav() {
|
|
3190
|
+
const nav = $("rd-chat-nav"), detail = $("run-detail");
|
|
3191
|
+
if (!nav || !detail) return;
|
|
3192
|
+
const direct = !detail.classList.contains("hidden") && !!S.lastRun &&
|
|
3193
|
+
chatEngineIsDirect(S.lastRun);
|
|
3194
|
+
detail.classList.toggle("chat-mode", direct);
|
|
3195
|
+
// 对话为主时锁死外层滚动(main 是滚动根);离开详情必须解锚,否则任务列表滚不动
|
|
3196
|
+
const mainEl = document.querySelector("main");
|
|
3197
|
+
if (mainEl) mainEl.classList.toggle("chat-fill", direct);
|
|
3198
|
+
if (!direct) { nav.classList.add("hidden"); nav.innerHTML = ""; return; }
|
|
3199
|
+
const labels = { hive: t("蜂巢"), steps: t("步骤"), result: t("成果"),
|
|
3200
|
+
git: "Git", bible: t("圣经"), bookmeta: t("作品信息") };
|
|
3201
|
+
const avail = rdTabAvail();
|
|
3202
|
+
let pills = "";
|
|
3203
|
+
for (const k of ["hive", "steps", "result", "git", "bible", "bookmeta"]) {
|
|
3204
|
+
if (!avail[k]) continue;
|
|
3205
|
+
// 徽章直接镜像隐藏页签上的(蜂巢在岗数/步骤数/待裁决/成果数),不另设状态源
|
|
3206
|
+
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="' + k + '"] .rd-badge');
|
|
3207
|
+
pills += '<button class="cn-pill' + (S.rdTab === k ? " on" : "") +
|
|
3208
|
+
'" onclick="rdChatNavGo(\'' + k + '\')">' + labels[k] +
|
|
3209
|
+
(badge && badge.textContent ? "<b>" + esc(badge.textContent) + "</b>" : "") + "</button>";
|
|
3210
|
+
}
|
|
3211
|
+
nav.innerHTML = (S.rdTab !== "chat"
|
|
3212
|
+
? '<button class="cn-back" onclick="rdChatNavBack()">' +
|
|
3213
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-arrow-left"></use></svg>' +
|
|
3214
|
+
t("返回对话") + "</button>"
|
|
3215
|
+
: "") +
|
|
3216
|
+
'<span class="cn-pills">' + pills + "</span>";
|
|
3217
|
+
nav.classList.remove("hidden");
|
|
3218
|
+
}
|
|
3219
|
+
window.rdChatNavGo = function (tab) {
|
|
3220
|
+
S.rdTab = tab;
|
|
3221
|
+
S.rdTabPin = true; // 用户主动去看其它分区:别被自动选卡拽回对话
|
|
3222
|
+
applyRdTabs();
|
|
3223
|
+
};
|
|
3224
|
+
window.rdChatNavBack = function () {
|
|
3225
|
+
S.rdTab = "chat";
|
|
3226
|
+
S.rdTabPin = false;
|
|
3227
|
+
S.rdTabSig = "";
|
|
3228
|
+
applyRdTabs();
|
|
3229
|
+
};
|
|
3156
3230
|
|
|
3157
3231
|
/* 徽章:蜂巢=在岗数(运行中)、步骤=总步数、版本=待裁决、成果=文件数(loadArtifacts 里刷) */
|
|
3158
3232
|
function rdTabBadges() {
|
|
@@ -3173,8 +3247,11 @@ function rdTabBadges() {
|
|
|
3173
3247
|
function rdTabsSync(ctx) {
|
|
3174
3248
|
if (ctx) S._rdCtx = ctx;
|
|
3175
3249
|
if (ctx && !S.rdTabPin) {
|
|
3250
|
+
// 对话页签可用性入签名:刚建的任务要等 state 刷进来 direct 才判定成立,
|
|
3251
|
+
// chat 从不可用变可用时必须触发一次重新选卡(否则落在步骤/蜂巢不跳对话)
|
|
3252
|
+
const chatAvail = $("rd-chat") && !$("rd-chat").classList.contains("hidden");
|
|
3176
3253
|
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (ctx.status || "") +
|
|
3177
|
-
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0);
|
|
3254
|
+
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0);
|
|
3178
3255
|
if (S.rdTabSig !== sig) {
|
|
3179
3256
|
S.rdTabSig = sig;
|
|
3180
3257
|
const avail = rdTabAvail();
|
|
@@ -3370,11 +3447,13 @@ const BOOKMETA_PLATFORMS = [
|
|
|
3370
3447
|
];
|
|
3371
3448
|
const BOOKMETA_FIELDS = {
|
|
3372
3449
|
fanqie: [["book_name", "作品名"], ["signing_mode", "签约模式"], ["target_reader", "目标读者"],
|
|
3373
|
-
["category", "
|
|
3374
|
-
["tags_plot", "情节标签"], ["
|
|
3375
|
-
["
|
|
3450
|
+
["category", "主分类"], ["tags_theme", "主题标签"], ["tags_role", "角色标签"],
|
|
3451
|
+
["tags_plot", "情节标签"], ["content_plot", "内容·情节"], ["content_emotion", "内容·情感"],
|
|
3452
|
+
["content_character", "内容·人设"], ["content_world", "内容·世界观"],
|
|
3453
|
+
["protagonist_1", "主角名1"], ["protagonist_2", "主角名2"], ["summary", "作品简介"]],
|
|
3376
3454
|
qimao: [["book_name", "作品名称"], ["target_reader", "目标读者"], ["category_main", "一级分类"],
|
|
3377
|
-
["category_sub", "二级分类"], ["
|
|
3455
|
+
["category_sub", "二级分类"], ["tags_style", "风格标签"], ["tags_role", "角色标签"],
|
|
3456
|
+
["tags_plot", "情节标签"], ["tags_bg", "背景标签"], ["protagonist_1", "主角名1"],
|
|
3378
3457
|
["protagonist_2", "主角名2"], ["status", "作品状态"], ["summary", "作品简介"]],
|
|
3379
3458
|
};
|
|
3380
3459
|
|
|
@@ -3486,6 +3565,7 @@ window.bmGen = async function (taskId, platform) {
|
|
|
3486
3565
|
await api("/api/tasks/" + encodeURIComponent(taskId) + "/book-meta",
|
|
3487
3566
|
{ method: "POST", body: JSON.stringify({ platform }) });
|
|
3488
3567
|
toast(t("已开始生成,完成后这里会自动更新"));
|
|
3568
|
+
await refreshState(); render(); // 立即翻到「生成中」,不等 SSE 推送/下一轮轮询
|
|
3489
3569
|
} catch (e) { toast(t("生成失败:") + e.message, true); }
|
|
3490
3570
|
};
|
|
3491
3571
|
|
|
@@ -3604,7 +3684,7 @@ function _renderGitSnapshot(run, task) {
|
|
|
3604
3684
|
'<span class="chip failed">' + t("检出失败") + "</span>" +
|
|
3605
3685
|
'<code class="git-branch">codebee/' + esc(tid || t("(无主运行)")) + t("(") + esc(t("未创建")) + t(")") + "</code></div>" +
|
|
3606
3686
|
(lastErr ? '<div class="hint warn">' + esc(lastErr) + "</div>" : "") +
|
|
3607
|
-
'<div class="hint">' + esc(t("
|
|
3687
|
+
'<div class="hint">' + esc(t("处理后点「继续任务」,运行会重新检出任务分支。")) + "</div>";
|
|
3608
3688
|
rdTabsSync();
|
|
3609
3689
|
return;
|
|
3610
3690
|
}
|
|
@@ -4021,7 +4101,7 @@ function drawInspector() {
|
|
|
4021
4101
|
main.innerHTML = '<div class="insp-branch"><code>codebee/' + esc(d.task.id || "") +
|
|
4022
4102
|
t("(") + esc(t("未创建")) + t(")") + '</code></div>' +
|
|
4023
4103
|
(lastErr ? '<div class="hint warn">' + esc(lastErr) + "</div>" : "") +
|
|
4024
|
-
'<span class="insp-hint">' + esc(t("
|
|
4104
|
+
'<span class="insp-hint">' + esc(t("处理后点「继续任务」,运行会重新检出任务分支。")) + "</span>";
|
|
4025
4105
|
} else {
|
|
4026
4106
|
// 没指定代码版本:没有任务分支,卡里只留一句说明
|
|
4027
4107
|
chipEl.classList.add("hidden");
|
|
@@ -4650,7 +4730,8 @@ const hiveTails = {}; // "runid|rel" -> 最近一行输出缓
|
|
|
4650
4730
|
function stopHiveTick() { if (hiveTimer) { clearInterval(hiveTimer); hiveTimer = null; } }
|
|
4651
4731
|
|
|
4652
4732
|
/* run.steps -> 阶段泳道流水线:规划→起草→评审→修订→打磨→合成,先后关系一眼可见;
|
|
4653
|
-
*
|
|
4733
|
+
* 运行中蜜蜂摆动+秒表走动+蜜光呼吸;彗尾光点流向下一阶段;点格即看实时输出;
|
|
4734
|
+
* 入场瀑布只在步骤结构真变时重播(签名闸门,轮询空转不重建 DOM)。 */
|
|
4654
4735
|
function hiveStage(role) {
|
|
4655
4736
|
const r = String(role || "");
|
|
4656
4737
|
if (/^(plan|outline)/.test(r)) return t("规划");
|
|
@@ -4711,7 +4792,7 @@ window.renderHive = function (run) {
|
|
|
4711
4792
|
if (sub) sub.textContent = running.length
|
|
4712
4793
|
? t("在岗") + " " + running.length + " / " + steps.length : t("全部空闲");
|
|
4713
4794
|
const activeIdx = lanes.map((l) => byStage[l].some((s) => s.status === "running")).lastIndexOf(true);
|
|
4714
|
-
const cell = (s) => {
|
|
4795
|
+
const cell = (s, laneIdx, cellIdx) => {
|
|
4715
4796
|
// 五归一:取消/超时的格子不再冒充「完成」——与步骤芯片同三色体系
|
|
4716
4797
|
const st = s.status === "running" || s.status === "queued" ? "running"
|
|
4717
4798
|
: s.status === "failed" ? "failed"
|
|
@@ -4727,7 +4808,10 @@ window.renderHive = function (run) {
|
|
|
4727
4808
|
const title = [s.role, who, s.started_at ? t("开始于 ") + s.started_at : "",
|
|
4728
4809
|
(s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
|
|
4729
4810
|
+ (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
|
|
4730
|
-
|
|
4811
|
+
// --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
|
|
4812
|
+
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '"' +
|
|
4813
|
+
' style="--d:' + (laneIdx * 110 + cellIdx * 45) + 'ms"' +
|
|
4814
|
+
' title="' + esc(title) + '" ' +
|
|
4731
4815
|
'onclick="hiveOpenLog(\'' + esc(run.id) + "', '" + esc(s.log || "") + '\')">' +
|
|
4732
4816
|
'<div class="hc-head">' +
|
|
4733
4817
|
'<svg class="ico hc-bee" aria-hidden="true"><use href="#i-bee"></use></svg>' +
|
|
@@ -4737,17 +4821,19 @@ window.renderHive = function (run) {
|
|
|
4737
4821
|
esc(tailText) + "</div>" +
|
|
4738
4822
|
'<div class="hc-meta"><span class="hc-elapsed" data-started="' + esc(s.started_at || "") + '">' +
|
|
4739
4823
|
(s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at)) + "</span>" +
|
|
4740
|
-
(st === "running" ? '<span class="hc-live"
|
|
4824
|
+
(st === "running" ? '<span class="hc-live"><i class="live-dot"></i>' + t("工作中") + "</span>" : "") +
|
|
4741
4825
|
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4742
4826
|
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4743
4827
|
"</div></div>";
|
|
4744
4828
|
};
|
|
4745
|
-
const dots = (list) => list.slice(-12).map((s) => {
|
|
4829
|
+
const dots = (list, laneIdx) => list.slice(-12).map((s, di) => {
|
|
4746
4830
|
const dc = s.status === "running" ? "run" : s.status === "failed" ? "fail"
|
|
4747
4831
|
: s.status === "cancelled" ? "cancel" : s.status === "timeout" ? "time" : "done";
|
|
4748
4832
|
const stx = { running: t("运行中"), failed: t("失败"), cancelled: t("已取消"),
|
|
4749
4833
|
timeout: t("超时"), done: t("完成") }[dc] || s.status;
|
|
4750
|
-
|
|
4834
|
+
// --d:与格子同款级联延迟——入场时整条轨道按执行顺序"哗"地点亮(进度回放感)
|
|
4835
|
+
return '<i class="ld ld-' + dc + '" style="--d:' + (laneIdx * 110 + di * 45) + 'ms"' +
|
|
4836
|
+
' title="' + esc((s.role || "") + " · " + stx) + '"></i>';
|
|
4751
4837
|
}).join("");
|
|
4752
4838
|
$("rd-hive-cells").innerHTML = lanes.map((stage, i) => {
|
|
4753
4839
|
const list = byStage[stage];
|
|
@@ -4755,13 +4841,38 @@ window.renderHive = function (run) {
|
|
|
4755
4841
|
// 无 running(终态回看):全部视为已完成泳道;有 running 时 activeIdx 之前算完成
|
|
4756
4842
|
const cls = hasRun ? "lane-active"
|
|
4757
4843
|
: (activeIdx === -1 || i < activeIdx ? "lane-done" : "lane-idle");
|
|
4844
|
+
// 泳道头视觉 v2:阶段序号徽章(01/02…流水线站序)+ 已完成进度(settled/total)
|
|
4845
|
+
const settled = list.filter((s) => !["running", "queued"].includes(s.status)).length;
|
|
4758
4846
|
return '<div class="hive-lane ' + cls + '">' +
|
|
4759
|
-
'<div class="lane-head"
|
|
4847
|
+
'<div class="lane-head">' +
|
|
4848
|
+
'<span class="lane-idx">' + String(i + 1).padStart(2, "0") + "</span>" +
|
|
4849
|
+
'<span class="lane-name">' + esc(stage) + '</span>' +
|
|
4760
4850
|
'<span class="lane-n">×' + list.length + "</span>" +
|
|
4761
|
-
(
|
|
4762
|
-
'<
|
|
4763
|
-
'<div class="lane-
|
|
4851
|
+
(settled ? '<span class="lane-prog">' + settled + "/" + list.length + "</span>" : "") +
|
|
4852
|
+
(hasRun ? '<span class="lane-live"><i class="live-dot"></i>' + t("进行中") + "</span>" : "") + "</div>" +
|
|
4853
|
+
'<div class="lane-track">' + dots(list, i) + "</div>" +
|
|
4854
|
+
'<div class="lane-cells">' + list.slice(-8).map((s, ci) => cell(s, i, ci)).join("") + "</div></div>";
|
|
4764
4855
|
}).join('<div class="lane-flow" aria-hidden="true"></div>');
|
|
4856
|
+
// 入场瀑布闸门:只按"结构"(泳道数/格数/状态构成)签名;运行中尾巴/秒表每 2s
|
|
4857
|
+
// 变化不能触重播。签名没变就不动 DOM——格子不闪、动画不重启、悬停不弹手。
|
|
4858
|
+
const sig = lanes.map((l) => l + ":" + byStage[l].length + "(" +
|
|
4859
|
+
byStage[l].map((s) => s.status[0] || "?").join("") + ")").join("|");
|
|
4860
|
+
const cellsBox = $("rd-hive-cells");
|
|
4861
|
+
if (cellsBox.dataset.hiveSig !== sig) {
|
|
4862
|
+
const fresh = cellsBox.dataset.hiveSig == null; // 首次展开详情也照播,先落框架再逐格亮起
|
|
4863
|
+
cellsBox.dataset.hiveSig = sig;
|
|
4864
|
+
// hive-enter 只在下一帧就摘(Rune/Hermes 式一闪而过的瀑布),全程保留会锁死
|
|
4865
|
+
// 入场帧;1400ms 定时是 RAF 被后台标签页冻结时的兜底。
|
|
4866
|
+
if (!fresh) cellsBox.classList.remove("hive-enter");
|
|
4867
|
+
cellsBox.classList.add("hive-enter");
|
|
4868
|
+
if (cellsBox._hiveEnterRAF) cancelAnimationFrame(cellsBox._hiveEnterRAF);
|
|
4869
|
+
cellsBox._hiveEnterRAF = requestAnimationFrame(() => {
|
|
4870
|
+
requestAnimationFrame(() => cellsBox.classList.remove("hive-enter"));
|
|
4871
|
+
cellsBox._hiveEnterRAF = null;
|
|
4872
|
+
});
|
|
4873
|
+
if (cellsBox._hiveEnterT) clearTimeout(cellsBox._hiveEnterT);
|
|
4874
|
+
cellsBox._hiveEnterT = setTimeout(() => cellsBox.classList.remove("hive-enter"), 1400);
|
|
4875
|
+
}
|
|
4765
4876
|
const live = running.length && (run.status === "running" || run.status === "queued");
|
|
4766
4877
|
if (hiveClock) { clearInterval(hiveClock); hiveClock = null; }
|
|
4767
4878
|
if (live) {
|
|
@@ -4773,12 +4884,8 @@ window.renderHive = function (run) {
|
|
|
4773
4884
|
el.textContent = hiveElapsed(el.dataset.started);
|
|
4774
4885
|
});
|
|
4775
4886
|
}, 1000);
|
|
4776
|
-
//
|
|
4777
|
-
//
|
|
4778
|
-
if (S.hiveAutoLog !== run.id && !currentLog) {
|
|
4779
|
-
const first = running.find((s) => s.log);
|
|
4780
|
-
if (first) { S.hiveAutoLog = run.id; toggleLog(run.id, first.log); }
|
|
4781
|
-
}
|
|
4887
|
+
// 日志抽屉一律手动打开(点蜂巢格/步骤行):自动弹开会盖住对话输入条,
|
|
4888
|
+
// 用户要求所有页面默认不展示日志(2026-09-17)
|
|
4782
4889
|
} else stopHiveTick();
|
|
4783
4890
|
rdTabsSync(); // 蜂巢显隐直接决定「蜂巢」标签可用性
|
|
4784
4891
|
};
|
|
@@ -4993,6 +5100,38 @@ function chatEngineIsDirect(run) {
|
|
|
4993
5100
|
return !!(t && t.engine === "direct");
|
|
4994
5101
|
}
|
|
4995
5102
|
|
|
5103
|
+
/* 对话正文:``` 围栏渲染成真代码块(复用 codeBlockHTML:行号+高亮+代码主题),
|
|
5104
|
+
* 其余文本保持 pre-wrap 原样(行内 `code` 与 **加粗** 轻量翻译)。模型输出先
|
|
5105
|
+
* 全量 esc 再插标记;未闭合围栏按代码块收尾,流式输出中途也不撒裸反引号。 */
|
|
5106
|
+
function chatBodyHTML(text) {
|
|
5107
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
5108
|
+
const out = [];
|
|
5109
|
+
let txt = [], code = null;
|
|
5110
|
+
const flushTxt = () => {
|
|
5111
|
+
if (!txt.length) return;
|
|
5112
|
+
out.push('<div class="chat-body">' + txt.map((l) => {
|
|
5113
|
+
let h = esc(l);
|
|
5114
|
+
h = h.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
5115
|
+
h = h.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
|
|
5116
|
+
return h;
|
|
5117
|
+
}).join("\n") + "</div>");
|
|
5118
|
+
txt = [];
|
|
5119
|
+
};
|
|
5120
|
+
for (const raw of lines) {
|
|
5121
|
+
const line = raw.replace(/\s+$/, "");
|
|
5122
|
+
if (line.startsWith("```")) {
|
|
5123
|
+
if (code) { out.push(codeBlockHTML(code.join("\n"))); code = null; }
|
|
5124
|
+
else { flushTxt(); code = []; }
|
|
5125
|
+
continue;
|
|
5126
|
+
}
|
|
5127
|
+
if (code) code.push(raw);
|
|
5128
|
+
else txt.push(raw);
|
|
5129
|
+
}
|
|
5130
|
+
if (code) out.push(codeBlockHTML(code.join("\n")));
|
|
5131
|
+
flushTxt();
|
|
5132
|
+
return out.join("");
|
|
5133
|
+
}
|
|
5134
|
+
|
|
4996
5135
|
async function renderChat(run, active) {
|
|
4997
5136
|
const box = $("rd-chat");
|
|
4998
5137
|
if (!box) return;
|
|
@@ -5013,34 +5152,59 @@ async function renderChat(run, active) {
|
|
|
5013
5152
|
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
5014
5153
|
if (!S.lastRun || S.lastRun.id !== runForFetch) return;
|
|
5015
5154
|
const flow = $("rd-chat-flow");
|
|
5155
|
+
// 时间显示统一收敛到 HH:MM(日期在 meta 条里,全量戳塞气泡就是噪音);
|
|
5156
|
+
// 附件兼容 字符串路径 / {name|path} 对象 两种形态(对象直接拼会成 [object Object])
|
|
5157
|
+
const chatTime = (v) => { const s = String(v || "");
|
|
5158
|
+
return /^\d{4}-\d{2}-\d{2}[ T]/.test(s) ? s.slice(11, 16) : s; };
|
|
5159
|
+
const attName = (a) => { const p = typeof a === "string" ? a
|
|
5160
|
+
: ((a && (a.name || a.path)) || "");
|
|
5161
|
+
return String(p).split(/[\\/]/).pop() || ""; };
|
|
5016
5162
|
flow.innerHTML = items.map((it) => {
|
|
5017
5163
|
if (it.kind === "user") {
|
|
5018
|
-
//
|
|
5164
|
+
// 用户消息:右侧浅灰圆角块,元信息在块内顶部。
|
|
5165
|
+
// 附件胶囊可点开预览:data-att 带 _attachments/ 相对路径(老数据裸文件名
|
|
5166
|
+
// 在前端钉回 _attachments/,与服务端 norm_rel 同口径),事件委托绑在
|
|
5167
|
+
// rd-chat-flow 上——时间线整块 innerHTML 重建也不丢监听
|
|
5168
|
+
const atts = (it.attachments || []).map((a) => {
|
|
5169
|
+
const nm = attName(a);
|
|
5170
|
+
if (!nm) return null;
|
|
5171
|
+
let rel = typeof a === "string" ? a : ((a && (a.path || a.name)) || nm);
|
|
5172
|
+
rel = String(rel).replace(/\\/g, "/");
|
|
5173
|
+
if (!rel.startsWith("_attachments/")) rel = "_attachments/" + rel.split("/").pop();
|
|
5174
|
+
return { nm, rel };
|
|
5175
|
+
}).filter(Boolean);
|
|
5019
5176
|
return '<div class="chat-row me">' +
|
|
5020
5177
|
'<div class="chat-bubble me">' +
|
|
5021
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at
|
|
5178
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5022
5179
|
(it.consumed ? "" : " · " + t("待送达")) + "</div>" +
|
|
5023
5180
|
esc(it.text || t("(仅附件)")) +
|
|
5024
|
-
(
|
|
5025
|
-
? '<div class="chat-atts">' +
|
|
5026
|
-
'<span class="att-chip2"
|
|
5181
|
+
(atts.length
|
|
5182
|
+
? '<div class="chat-atts">' + atts.map((a) =>
|
|
5183
|
+
'<span class="att-chip2 att-open" data-att="' + esc(a.rel) +
|
|
5184
|
+
'" title="' + esc(t("点击查看")) + '">' + esc(a.nm) + "</span>").join("") + "</div>"
|
|
5027
5185
|
: "") +
|
|
5028
5186
|
"</div></div>";
|
|
5029
5187
|
}
|
|
5030
5188
|
// 协议尾行是给编排器判收的机器标记,不该出现在聊天正文里(真源在提示词约定)
|
|
5031
5189
|
const body = String(it.text || "").replace(/\n*DIRECT_DONE:.*\s*$/s, "").trim();
|
|
5190
|
+
// 运行中还没有正文:三点打字动画(终态无正文才落「无文本输出」占位)
|
|
5191
|
+
const bodyHtml = body ? chatBodyHTML(body)
|
|
5192
|
+
: (it.status === "running"
|
|
5193
|
+
? '<span class="chat-typing" aria-label="' + esc(t("正在执行…")) + '"><i></i><i></i><i></i></span>'
|
|
5194
|
+
: esc(t("(本轮无文本输出)")));
|
|
5195
|
+
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
5196
|
+
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
5032
5197
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
5033
5198
|
return '<div class="chat-row">' +
|
|
5034
5199
|
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5035
5200
|
'<div class="chat-bubble agent">' +
|
|
5036
|
-
'<
|
|
5037
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at
|
|
5038
|
-
(
|
|
5039
|
-
(it.status && it.status !== "done" ? " · " + esc(it.status) : "") + "</div></div></div>";
|
|
5201
|
+
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5202
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5203
|
+
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div></div></div>";
|
|
5040
5204
|
}).join("") || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
5041
5205
|
const hint = $("rd-chat-hint");
|
|
5042
5206
|
if (hint) hint.textContent = active
|
|
5043
|
-
? t("
|
|
5207
|
+
? t("运行中:新消息会排队,本轮回答完后依次送达")
|
|
5044
5208
|
: t("已结束:发送后将自动开新一轮接着做");
|
|
5045
5209
|
flow.scrollTop = flow.scrollHeight;
|
|
5046
5210
|
}
|
|
@@ -5048,12 +5212,28 @@ async function renderChat(run, active) {
|
|
|
5048
5212
|
function drawChatAtts() {
|
|
5049
5213
|
const box = $("rd-chat-att-list");
|
|
5050
5214
|
if (!box) return;
|
|
5215
|
+
// 点名字开预览(待提交区原文走 /api/attachments/<id>),× 才是移除
|
|
5051
5216
|
box.innerHTML = chatAtts.map((a, i) =>
|
|
5052
|
-
'<span class="att-chip2"
|
|
5053
|
-
esc(t("
|
|
5217
|
+
'<span class="att-chip2 att-view" data-att-id="' + esc(a.id || "") +
|
|
5218
|
+
'" data-att-name="' + esc(a.name) + '" title="' + esc(t("点击查看")) + '">' + esc(a.name) +
|
|
5219
|
+
'<b onclick="chatRemoveAtt(' + i + ')" title="' + esc(t("移除")) + '">×' +
|
|
5220
|
+
'</b></span>').join("");
|
|
5054
5221
|
}
|
|
5055
5222
|
window.chatRemoveAtt = function (i) { chatAtts.splice(i, 1); drawChatAtts(); };
|
|
5056
5223
|
|
|
5224
|
+
/* 对话附件点击预览:已落盘的走 run 工作目录 /api/runs/<id>/file(rel 为
|
|
5225
|
+
* _attachments/ 相对路径),待提交的走 /api/attachments/<id>;
|
|
5226
|
+
* 弹窗与成品预览同款——图片直接看图、文本看码、二进制给下载。 */
|
|
5227
|
+
window.chatAttPopup = function (rel) {
|
|
5228
|
+
if (!rel || !chatRunId) return;
|
|
5229
|
+
return _fpPreviewUrl("/api/runs/" + encodeURIComponent(chatRunId) +
|
|
5230
|
+
"/file?name=" + encodeURIComponent(rel), rel);
|
|
5231
|
+
};
|
|
5232
|
+
window.chatPendingPopup = function (id, name) {
|
|
5233
|
+
if (!id) return;
|
|
5234
|
+
return _fpPreviewUrl("/api/attachments/" + encodeURIComponent(id), name || "attachment");
|
|
5235
|
+
};
|
|
5236
|
+
|
|
5057
5237
|
async function chatUploadFiles(files) {
|
|
5058
5238
|
for (const f of files) {
|
|
5059
5239
|
try {
|
|
@@ -5081,24 +5261,33 @@ window.chatSend = async function () {
|
|
|
5081
5261
|
btn.disabled = true;
|
|
5082
5262
|
try {
|
|
5083
5263
|
if (active) {
|
|
5264
|
+
// 运行中:消息进信箱排队(时间线上带「待送达」标记),本轮跑完自动续轮送达
|
|
5084
5265
|
await api("/api/runs/" + encodeURIComponent(chatRunId) + "/messages", {
|
|
5085
5266
|
method: "POST",
|
|
5086
5267
|
body: JSON.stringify({ text, attachments: chatAtts.map((a) => a.id) }),
|
|
5087
5268
|
});
|
|
5088
|
-
toast(t("
|
|
5269
|
+
toast(t("已排队:随下一步一起送达"));
|
|
5089
5270
|
} else {
|
|
5090
5271
|
const r = await api("/api/runs/" + encodeURIComponent(chatRunId) + "/chat", {
|
|
5091
5272
|
method: "POST",
|
|
5092
5273
|
body: JSON.stringify({ text, attachments: chatAtts.map((a) => a.id) }),
|
|
5093
5274
|
});
|
|
5094
5275
|
toast(t("已开新一轮,接着做…"));
|
|
5095
|
-
if (r && r.run_id) {
|
|
5276
|
+
if (r && r.run_id) {
|
|
5277
|
+
if (S.detailTaskKey) { S.taskSig = ""; renderTaskDetail(); }
|
|
5278
|
+
else jumpToRun(r.run_id);
|
|
5279
|
+
}
|
|
5096
5280
|
}
|
|
5281
|
+
// 发送成功一律清空输入框(只清输入,时间线里的历史回复不动);
|
|
5282
|
+
// 此前 /chat 分支提前 return 跳过了清空,发出去的字一直留在框里
|
|
5097
5283
|
ta.value = "";
|
|
5284
|
+
ta.style.height = "";
|
|
5098
5285
|
chatAtts = []; drawChatAtts();
|
|
5099
5286
|
chatSig = ""; // 强制重画时间线
|
|
5100
|
-
|
|
5101
|
-
|
|
5287
|
+
if (active) {
|
|
5288
|
+
const d = await api("/api/runs/" + encodeURIComponent(chatRunId));
|
|
5289
|
+
if (d.run) renderChat(d.run, d.run.status === "running" || d.run.status === "queued");
|
|
5290
|
+
}
|
|
5102
5291
|
} catch (e) { toast(t("发送失败:") + e.message, true); }
|
|
5103
5292
|
finally { btn.disabled = false; }
|
|
5104
5293
|
};
|
|
@@ -5113,6 +5302,12 @@ function bindChat() {
|
|
|
5113
5302
|
e.target.value = "";
|
|
5114
5303
|
});
|
|
5115
5304
|
send.addEventListener("click", window.chatSend);
|
|
5305
|
+
// 自动伸高:跟内容走、有上限(160px 后内部滚动);清空由 chatSend 归零。
|
|
5306
|
+
// 高度只由用户输入驱动,轮询重画不碰它——框框不再忽大忽小
|
|
5307
|
+
ta.addEventListener("input", () => {
|
|
5308
|
+
ta.style.height = "auto";
|
|
5309
|
+
ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
|
|
5310
|
+
});
|
|
5116
5311
|
ta.addEventListener("keydown", (e) => {
|
|
5117
5312
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); window.chatSend(); }
|
|
5118
5313
|
});
|
|
@@ -5126,6 +5321,19 @@ function bindChat() {
|
|
|
5126
5321
|
return f && !f.name ? new File([f], "paste-" + Date.now() + ".png", { type: f.type }) : f;
|
|
5127
5322
|
}).filter(Boolean));
|
|
5128
5323
|
});
|
|
5324
|
+
// 附件胶囊点击预览:委托绑在静态容器上——时间线轮询整块重建 innerHTML、
|
|
5325
|
+
// 输入条胶囊增删重画,都不丢监听。时间线胶囊走已落盘文件,输入条胶囊
|
|
5326
|
+
// (× 移除按钮除外)走待提交区
|
|
5327
|
+
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5328
|
+
const chip = e.target.closest(".att-open");
|
|
5329
|
+
if (chip) window.chatAttPopup(chip.dataset.att);
|
|
5330
|
+
});
|
|
5331
|
+
const attBox = $("rd-chat-att-list");
|
|
5332
|
+
if (attBox) attBox.addEventListener("click", (e) => {
|
|
5333
|
+
if (e.target.closest("b")) return; // × 自己的 onclick 负责移除
|
|
5334
|
+
const chip = e.target.closest("[data-att-id]");
|
|
5335
|
+
if (chip) window.chatPendingPopup(chip.dataset.attId, chip.dataset.attName);
|
|
5336
|
+
});
|
|
5129
5337
|
}
|
|
5130
5338
|
|
|
5131
5339
|
/* ---------------------------------------------------------- 外观:皮肤 + 明暗(换肤) */
|
|
@@ -5134,6 +5342,7 @@ function bindChat() {
|
|
|
5134
5342
|
* 皮肤改色只需要动 style.css,预览与真实界面不会各自漂移。 */
|
|
5135
5343
|
const SKINS = [
|
|
5136
5344
|
{ id: "ocean", name: "深海", desc: "藏青底色 + 天蓝强调,夜间长时间盯任务更沉静(默认)" },
|
|
5345
|
+
{ id: "hermes", name: "墨金", desc: "暖墨底色 + 金色发丝线,Hermes 式古典优雅" },
|
|
5137
5346
|
{ id: "classic", name: "经典", desc: "黑白灰 + 蓝色强调,ChatGPT 式清爽配色" },
|
|
5138
5347
|
{ id: "forest", name: "森野", desc: "墨绿底色 + 青翠强调,偏自然的护眼配色" },
|
|
5139
5348
|
{ id: "amber", name: "暖阳", desc: "暖棕底色 + 琥珀强调,纸感暖调" },
|
|
@@ -5696,6 +5905,8 @@ function bindModelBox(c, provId) {
|
|
|
5696
5905
|
: ""));
|
|
5697
5906
|
return '<span class="ochip' + (i === 0 ? " primary" : "") + '">' +
|
|
5698
5907
|
"<b>" + (i === 0 ? t("主") : t("备")) + "</b>" + esc(pname) + " · " + esc(c2.m) +
|
|
5908
|
+
(modelHasImage(c2.p, c2.m)
|
|
5909
|
+
? ' <span class="tag ok" title="' + esc(t("支持图片输入")) + '">' + t("图") + "</span>" : "") +
|
|
5699
5910
|
(dead ? ' <span class="hint warn">⚠ ' + esc(t("协议不匹配,解析时跳过")) + "</span>" : badge) +
|
|
5700
5911
|
(i > 0 ? '<button class="mini" data-m="' + esc(c2.m) + '" data-p="' + esc(c2.p) +
|
|
5701
5912
|
'" title="' + t("设为主模型") + '" onclick="bindPromote(\'' + esc(c.id) + '\', this)">' +
|
|
@@ -5748,6 +5959,8 @@ function bindPanel(c) {
|
|
|
5748
5959
|
return '<label class="oitem' + (dead ? " dim" : "") + '"><input type="checkbox" value="' + esc(m) + '" data-p="' + esc(g.id) + '"' +
|
|
5749
5960
|
(has ? " checked" : "") + (dead ? " disabled" : "") +
|
|
5750
5961
|
" onchange=\"bindPick('" + esc(c.id) + "', this, this.checked)\">" + esc(m) +
|
|
5962
|
+
(modelHasImage(g.id, m)
|
|
5963
|
+
? ' <span class="tag ok" title="' + esc(t("支持图片输入")) + '">' + t("图") + "</span>" : "") +
|
|
5751
5964
|
(dead ? ' <span class="hint warn">' + esc(t("无可用 wire,解析时跳过")) + "</span>" : "") +
|
|
5752
5965
|
"</label>";
|
|
5753
5966
|
}).join("") +
|
|
@@ -6955,9 +7168,24 @@ async function loadSelfupdate(force) {
|
|
|
6955
7168
|
SU = await api("/api/selfupdate" + (force ? "?force=1" : ""));
|
|
6956
7169
|
} catch (e) { SU = null; }
|
|
6957
7170
|
renderSu();
|
|
7171
|
+
maybeWhatsnew();
|
|
6958
7172
|
return SU;
|
|
6959
7173
|
}
|
|
6960
7174
|
|
|
7175
|
+
/* 升级重启后的一次性「本次更新内容」:localStorage 记住已展示的版本,
|
|
7176
|
+
* 版本变化且有本地 changelog 小节时弹窗(首次使用只记账不弹)。 */
|
|
7177
|
+
function maybeWhatsnew() {
|
|
7178
|
+
if (!SU || !SU.current) return;
|
|
7179
|
+
const prev = localStorage.getItem("su.myver");
|
|
7180
|
+
localStorage.setItem("su.myver", SU.current);
|
|
7181
|
+
if (prev && prev !== SU.current && SU.whatsnew) {
|
|
7182
|
+
openModal(t("本次更新内容"),
|
|
7183
|
+
"<p class=\"hint\">" + t("已更新到") + " <b>v" + esc(SU.current) + "</b></p>" +
|
|
7184
|
+
"<pre class=\"su-notes\">" + esc(SU.whatsnew) + "</pre>",
|
|
7185
|
+
"<button class=\"small\" onclick=\"closeModal()\">" + t("知道了") + "</button>");
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
|
|
6961
7189
|
function renderSu() {
|
|
6962
7190
|
const info = $("su-info");
|
|
6963
7191
|
if (!info) return;
|
|
@@ -6968,6 +7196,10 @@ function renderSu() {
|
|
|
6968
7196
|
if (SU.has_update) {
|
|
6969
7197
|
html += "<p class=\"hint\"><b>" + t("发现新版本") + " v" + SU.latest +
|
|
6970
7198
|
t(" ") + "<a href=\"#\" onclick=\"event.preventDefault();suApply()\">" + t("立即升级") + "</a></b></p>";
|
|
7199
|
+
if (SU.notes) {
|
|
7200
|
+
html += "<div class=\"hint\"><b>" + t("新版本更新内容") + t(":") + "</b>" +
|
|
7201
|
+
"<pre class=\"su-notes\">" + esc(SU.notes) + "</pre></div>";
|
|
7202
|
+
}
|
|
6971
7203
|
} else if (SU.mode === "npm" && !SU.note) {
|
|
6972
7204
|
html += "<p class=\"hint\">" + t("已是最新版。") + "</p>";
|
|
6973
7205
|
}
|