codebee 0.1.2 → 0.1.4
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/app/core/bookmeta.py +161 -27
- package/app/core/bookmeta_catalog.py +187 -0
- package/app/core/builtin_agent.py +382 -0
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +441 -424
- package/app/core/manager.py +1523 -1511
- package/app/core/market_remote.py +959 -896
- package/app/core/modelhub.py +7 -0
- package/app/core/pipeline.py +138 -32
- package/app/core/store.py +3 -1
- package/app/main.py +1478 -1448
- package/app/ui/app.js +212 -20
- package/app/ui/i18n.js +1732 -1712
- package/app/ui/index.html +6 -2
- package/app/ui/style.css +87 -59
- package/bin/tutti.js +147 -147
- package/package.json +39 -39
package/app/ui/app.js
CHANGED
|
@@ -123,7 +123,12 @@ function renderQuickChips() {
|
|
|
123
123
|
const box = $("cmp-quick");
|
|
124
124
|
if (!box || !S.flows) return;
|
|
125
125
|
const cur = ($("f-type") || {}).value;
|
|
126
|
-
|
|
126
|
+
// 直连是默认档位:固定首枚 chip;其余内置流程跟后面(去重,共 5 枚)
|
|
127
|
+
const builtins = (S.flows || []).filter((f) => f.builtin);
|
|
128
|
+
const d = builtins.find((f) => f.id === "direct");
|
|
129
|
+
const list = d ? [d].concat(builtins.filter((f) => f.id !== "direct").slice(0, 4))
|
|
130
|
+
: builtins.slice(0, 5);
|
|
131
|
+
box.innerHTML = list.map((f) =>
|
|
127
132
|
'<button type="button" class="cmp-chip' + (f.id === cur ? " on" : "") +
|
|
128
133
|
'" onclick="pickQuickType(\'' + esc(f.id) + '\')">' + flowIconHtml(f) +
|
|
129
134
|
"<span>" + esc(t(f.name)) + "</span></button>").join("");
|
|
@@ -829,6 +834,7 @@ function renderBindings() {
|
|
|
829
834
|
targets.map((c) => [c.id, c.orch_kind]), S.bindSel]);
|
|
830
835
|
if (sig === S.bindSig) return;
|
|
831
836
|
S.bindSig = sig;
|
|
837
|
+
syncSaveAllBtn();
|
|
832
838
|
bbox.innerHTML = targets.map((c) => {
|
|
833
839
|
const b = (S.bindings || {})[c.id] || {};
|
|
834
840
|
const opts = '<option value="">' + t("不绑定(用 CLI 自身的凭据与配置)") + '</option>' + bindable.map((p) => {
|
|
@@ -1674,9 +1680,14 @@ async function doImport() {
|
|
|
1674
1680
|
|
|
1675
1681
|
async function saveBinding(id) {
|
|
1676
1682
|
const st = bindSelById(id);
|
|
1683
|
+
// 主供应商跟链首走:模型链是唯一真源,链首非空时供应商下拉必须与之一致
|
|
1684
|
+
// (下拉是旧状态时把旧值发上去,后端「显式指定」就会盖回停用的旧供应商)。
|
|
1685
|
+
const headP = st.chain.length ? st.chain[0].p : "";
|
|
1686
|
+
const provSel = $("bindprov-" + id);
|
|
1687
|
+
if (provSel && provSel.value !== (headP || "")) provSel.value = headP || "";
|
|
1677
1688
|
try {
|
|
1678
1689
|
await api("/api/models/binding", { method: "POST", body: JSON.stringify({
|
|
1679
|
-
agent_id: id, provider_id:
|
|
1690
|
+
agent_id: id, provider_id: provSel ? provSel.value : "",
|
|
1680
1691
|
chain: st.chain.map((c) => ({ provider_id: c.p, model: c.m })),
|
|
1681
1692
|
difficulty_routing: $("binddiff-" + id).checked }) });
|
|
1682
1693
|
st.dirty = false;
|
|
@@ -1972,6 +1983,13 @@ async function archiveTask(id, archived) {
|
|
|
1972
1983
|
try {
|
|
1973
1984
|
await api("/api/tasks/" + encodeURIComponent(id) + "/archive",
|
|
1974
1985
|
{ method: "POST", body: JSON.stringify({ archived: !!archived }) });
|
|
1986
|
+
// 取消归档时把任务工作目录从「已移除目录」里捞回来:用户点时钟开关找回
|
|
1987
|
+
// 归档任务再取消归档,直觉上文件夹就该回侧栏(否则只能靠在该目录新建任务)。
|
|
1988
|
+
if (!archived) {
|
|
1989
|
+
const t0 = ((S.state || {}).tasks || []).concat(((S.state || {}).archived_tasks) || [])
|
|
1990
|
+
.find((x) => x.id === id);
|
|
1991
|
+
if (t0 && t0.workdir) unhideSideDir(t0.workdir);
|
|
1992
|
+
}
|
|
1975
1993
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1976
1994
|
poll();
|
|
1977
1995
|
}
|
|
@@ -2934,6 +2952,7 @@ function drawTaskDetail(key, runs) {
|
|
|
2934
2952
|
}
|
|
2935
2953
|
renderDirector(activeRun || null, !!activeRun);
|
|
2936
2954
|
renderHive(activeRun || latest); // 终态回看最近一次运行的蜂巢
|
|
2955
|
+
renderChat(latest, active); // 直连任务:对话时间线在任务级详情同样渲染(S.lastRun 已指向 latest)
|
|
2937
2956
|
const sum = (f) => runs.reduce((a, r) => a + (Number(r[f]) || 0), 0);
|
|
2938
2957
|
$("rd-meta").innerHTML =
|
|
2939
2958
|
'<span class="stat">' + t("运行 ") + '<b>' + runs.length + "</b>" + t(" 次") + "</span>" +
|
|
@@ -2973,8 +2992,9 @@ function drawTaskDetail(key, runs) {
|
|
|
2973
2992
|
try {
|
|
2974
2993
|
const md = await fetch("/api/runs/" + encodeURIComponent(latest.id) + "/report").then((r) => r.text());
|
|
2975
2994
|
const html2 = md2html(md);
|
|
2995
|
+
if (S.detailTaskKey !== key) return; // 拉取期间切了详情:过期报告不落缓存/不落盘
|
|
2976
2996
|
S.taskReport = { key, runId: latest.id, html: html2 };
|
|
2977
|
-
|
|
2997
|
+
$("rd-report").innerHTML = html2;
|
|
2978
2998
|
} catch (e) { /* 报告拉取失败静默,保留旧内容 */ }
|
|
2979
2999
|
} else {
|
|
2980
3000
|
$("rd-report").innerHTML = '<div class="hint">' + t("运行结束后生成") + '</div>';
|
|
@@ -3158,11 +3178,14 @@ function rdTabsSync(ctx) {
|
|
|
3158
3178
|
if (S.rdTabSig !== sig) {
|
|
3159
3179
|
S.rdTabSig = sig;
|
|
3160
3180
|
const avail = rdTabAvail();
|
|
3161
|
-
//
|
|
3181
|
+
// 直连任务:对话就是全部内容,默认落「对话」;其余流程保持既有落位。
|
|
3182
|
+
// 没跑到终态时成果分区是空的,落进去只见「运行结束后生成」白板——
|
|
3183
|
+
// 运行中先落蜂巢/步骤,收尾后自动选卡会再切到成果
|
|
3184
|
+
const finishing = !ctx.running && ["done", "failed", "cancelled", "timeout"].includes(ctx.status);
|
|
3162
3185
|
S.rdTab = (avail.chat ? "chat" : null)
|
|
3163
3186
|
|| (ctx.running ? (avail.hive ? "hive" : "steps")
|
|
3164
3187
|
: (ctx.gitState === "isolated" && avail.git ? "git"
|
|
3165
|
-
: (ctx.hasResult ? "result" : "steps")));
|
|
3188
|
+
: (ctx.hasResult && finishing ? "result" : "steps")));
|
|
3166
3189
|
}
|
|
3167
3190
|
}
|
|
3168
3191
|
applyRdTabs();
|
|
@@ -3185,6 +3208,7 @@ async function renderRunDetail() {
|
|
|
3185
3208
|
try { run = (await api("/api/runs/" + encodeURIComponent(id))).run; }
|
|
3186
3209
|
catch (e) { return; }
|
|
3187
3210
|
if (!run) return;
|
|
3211
|
+
if (S.detailRunId !== id || S.detailTaskKey) return; // 拉取期间已切走:过期响应不落盘
|
|
3188
3212
|
$("rd-title").textContent = run.title;
|
|
3189
3213
|
const chip = $("rd-status");
|
|
3190
3214
|
chip.className = "chip " + run.status;
|
|
@@ -3228,6 +3252,8 @@ async function renderRunDetail() {
|
|
|
3228
3252
|
// 报告
|
|
3229
3253
|
if (run.status === "done" || run.report) {
|
|
3230
3254
|
const md = await fetch("/api/runs/" + encodeURIComponent(id) + "/report").then((r) => r.text());
|
|
3255
|
+
// 报告拉取期间切了详情:过期报告/成品不落盘(A 的报告写进 B 详情的报告区)
|
|
3256
|
+
if (S.detailRunId !== id || S.detailTaskKey) return;
|
|
3231
3257
|
$("rd-report").innerHTML = md2html(md);
|
|
3232
3258
|
loadArtifacts(id);
|
|
3233
3259
|
} else {
|
|
@@ -3276,10 +3302,15 @@ async function renderBiblePanel(task) {
|
|
|
3276
3302
|
const box = $("rd-bible");
|
|
3277
3303
|
if (!box || !(task || {}).id) { if (box) box.classList.add("hidden"); rdTabsSync(); return; }
|
|
3278
3304
|
if (!task.serial) { box.classList.add("hidden"); rdTabsSync(); return; }
|
|
3305
|
+
// 拉取期间可能切详情:过期响应不落盘(把 A 任务的圣经写进 B 详情的串台面)
|
|
3306
|
+
const detailKey = S.detailTaskKey || S.detailRunId || "";
|
|
3279
3307
|
let d;
|
|
3280
3308
|
try { d = await api("/api/tasks/" + encodeURIComponent(task.id) + "/bible"); }
|
|
3281
3309
|
catch (e) { box.classList.add("hidden"); rdTabsSync(); return; }
|
|
3282
|
-
if ((S.detailTaskKey || S.detailRunId
|
|
3310
|
+
if ((S.detailTaskKey || S.detailRunId || "") !== detailKey ||
|
|
3311
|
+
((S.lastRunTask || {}).id || "") !== (task.id || "")) {
|
|
3312
|
+
box.classList.add("hidden"); rdTabsSync(); return;
|
|
3313
|
+
}
|
|
3283
3314
|
const running = task.status === "running" || task.status === "queued";
|
|
3284
3315
|
const text = d.text || "";
|
|
3285
3316
|
const editing = S._bibleEdit === task.id;
|
|
@@ -3339,7 +3370,8 @@ const BOOKMETA_PLATFORMS = [
|
|
|
3339
3370
|
];
|
|
3340
3371
|
const BOOKMETA_FIELDS = {
|
|
3341
3372
|
fanqie: [["book_name", "作品名"], ["signing_mode", "签约模式"], ["target_reader", "目标读者"],
|
|
3342
|
-
["
|
|
3373
|
+
["category", "分类"], ["tags_theme", "主题标签"], ["tags_role", "角色标签"],
|
|
3374
|
+
["tags_plot", "情节标签"], ["protagonist_1", "主角名1"],
|
|
3343
3375
|
["protagonist_2", "主角名2"], ["summary", "作品简介"]],
|
|
3344
3376
|
qimao: [["book_name", "作品名称"], ["target_reader", "目标读者"], ["category_main", "一级分类"],
|
|
3345
3377
|
["category_sub", "二级分类"], ["tags", "作品标签"], ["protagonist_1", "主角名1"],
|
|
@@ -3955,6 +3987,9 @@ async function refreshInspector(force) {
|
|
|
3955
3987
|
function drawInspector() {
|
|
3956
3988
|
const d = S.inspData;
|
|
3957
3989
|
if (!d || !d.task) return;
|
|
3990
|
+
// 已收起(详情页让位/用户关闭)就整帧不落盘:在途响应回来时用户多半已切到
|
|
3991
|
+
// 别的详情,落盘只会让 loadArtifacts 等渲染面拿到别人任务的 run(串台源头)
|
|
3992
|
+
if (!document.body.classList.contains("inspector-open")) return;
|
|
3958
3993
|
const ta = $("insp-msg-input");
|
|
3959
3994
|
if (ta && document.activeElement === ta) return;
|
|
3960
3995
|
const sig = JSON.stringify([
|
|
@@ -4456,15 +4491,22 @@ async function loadArtifacts(runId) {
|
|
|
4456
4491
|
let d;
|
|
4457
4492
|
try { d = await api("/api/runs/" + encodeURIComponent(runId) + "/files"); }
|
|
4458
4493
|
catch (e) { return; }
|
|
4459
|
-
//
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4494
|
+
// 成果分区/徽章只吃当前详情目标的 run:详情开着但 runId 是别的任务(检查器
|
|
4495
|
+
// 后台跟刷旧选中)时,绝不能把别家的文件写进主栏——那是肉眼可见的串台
|
|
4496
|
+
const detailOpen = !$("run-detail").classList.contains("hidden");
|
|
4497
|
+
const detailOwns = detailOpen &&
|
|
4498
|
+
(S.detailRunId === runId ||
|
|
4499
|
+
(S.detailTaskKey && S.lastRun && S.lastRun.id === runId));
|
|
4500
|
+
// 检查器入口只在「开着且 runId 仍是检查器当前 run」时认领;两边都不认 → 过期响应丢弃
|
|
4501
|
+
const inspOpen = document.body.classList.contains("inspector-open") && S.inspKey !== null;
|
|
4502
|
+
const inspOwns = inspOpen &&
|
|
4503
|
+
runId === ((S.inspData && S.inspData.run && S.inspData.run.id) || "");
|
|
4504
|
+
if (!detailOwns && !inspOwns) return;
|
|
4463
4505
|
const files = d.files || [];
|
|
4464
|
-
const box = $("insp-artifacts");
|
|
4465
|
-
const mainBox = $("rd-arts");
|
|
4466
|
-
//
|
|
4467
|
-
const fb = document.querySelector('#rd-tabs .rd-tab[data-tab="result"] .rd-badge');
|
|
4506
|
+
const box = inspOwns ? $("insp-artifacts") : null;
|
|
4507
|
+
const mainBox = detailOwns ? $("rd-arts") : null;
|
|
4508
|
+
// 成果分区徽章:文件数(终态才有产出,运行中不计);只随 detailOwns 一起刷
|
|
4509
|
+
const fb = detailOwns ? document.querySelector('#rd-tabs .rd-tab[data-tab="result"] .rd-badge') : null;
|
|
4468
4510
|
if (fb) { fb.textContent = files.length ? String(files.length) : "";
|
|
4469
4511
|
fb.className = "rd-badge" + (files.length ? "" : " hidden"); }
|
|
4470
4512
|
if (!files.length) {
|
|
@@ -4562,6 +4604,12 @@ async function toggleLog(runId, rel) {
|
|
|
4562
4604
|
}
|
|
4563
4605
|
try {
|
|
4564
4606
|
const r = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4607
|
+
// 拉取期间可能切详情:过期日志不落盘(A 的步骤输出写进 B 详情的抽屉)。
|
|
4608
|
+
// 检查器蜂巢/步骤点开日志同款:run 属检查器当前 run 也算在场
|
|
4609
|
+
const owned = S.detailRunId === runId ||
|
|
4610
|
+
(S.detailTaskKey && S.lastRun && S.lastRun.id === runId) ||
|
|
4611
|
+
((S.inspData || {}).run || {}).id === runId;
|
|
4612
|
+
if (!owned) return;
|
|
4565
4613
|
pre.textContent = r.log || t("(等待输出…)");
|
|
4566
4614
|
box.classList.remove("hidden");
|
|
4567
4615
|
currentLog = rel;
|
|
@@ -4739,6 +4787,10 @@ window.renderHive = function (run) {
|
|
|
4739
4787
|
* 只刷运行中格子——终态格子的结论一旦定格,不再被命令回显覆盖。 */
|
|
4740
4788
|
async function hiveTick(rid) {
|
|
4741
4789
|
if (document.hidden || !rid) return;
|
|
4790
|
+
// 换详情目标后旧蜂巢定时器必须停:先把 run.id 记下来,拉回来不是它就停表,
|
|
4791
|
+
// 否则 A 任务的步骤尾巴会持续写进 B 详情的蜂巢格
|
|
4792
|
+
if (!((S.inspData || {}).run || {}).id && S.detailRunId !== rid &&
|
|
4793
|
+
!(S.detailTaskKey && S.lastRun && S.lastRun.id === rid)) { stopHiveTick(); return; }
|
|
4742
4794
|
const box = $("rd-hive-cells");
|
|
4743
4795
|
if (!box) return;
|
|
4744
4796
|
const rels = Array.from(box.querySelectorAll(".hive-cell.st-running .hc-tail[data-log]"))
|
|
@@ -4951,28 +5003,42 @@ async function renderChat(run, active) {
|
|
|
4951
5003
|
const sig = run.id + "|" + run.status + "|" + (run.steps || []).length +
|
|
4952
5004
|
"|" + ((run.messages || []).length);
|
|
4953
5005
|
if (sig === chatSig) return; // 轮询重画去抖:内容没变不重建 DOM(保住输入焦点)
|
|
5006
|
+
const runForFetch = run.id; // 拉取期间可能切详情:过期响应不落盘(同 renderRunDetail 闸门)
|
|
4954
5007
|
chatSig = sig;
|
|
4955
5008
|
let items = [];
|
|
4956
5009
|
try { items = ((await api("/api/runs/" + encodeURIComponent(run.id) + "/timeline")).items) || []; }
|
|
4957
5010
|
catch (e) { items = []; }
|
|
5011
|
+
if (!$("run-detail") || $("run-detail").classList.contains("hidden")) return;
|
|
5012
|
+
// 过期闸门:run 级(renderRunDetail)与任务级(drawTaskDetail)都经 S.lastRun
|
|
5013
|
+
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
5014
|
+
if (!S.lastRun || S.lastRun.id !== runForFetch) return;
|
|
4958
5015
|
const flow = $("rd-chat-flow");
|
|
4959
5016
|
flow.innerHTML = items.map((it) => {
|
|
4960
5017
|
if (it.kind === "user") {
|
|
5018
|
+
// 用户消息:右侧浅灰圆角块,元信息在块内顶部
|
|
4961
5019
|
return '<div class="chat-row me">' +
|
|
4962
|
-
'<div class="chat-bubble me">' +
|
|
5020
|
+
'<div class="chat-bubble me">' +
|
|
5021
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at || "") +
|
|
5022
|
+
(it.consumed ? "" : " · " + t("待送达")) + "</div>" +
|
|
5023
|
+
esc(it.text || t("(仅附件)")) +
|
|
4963
5024
|
((it.attachments || []).length
|
|
4964
5025
|
? '<div class="chat-atts">' + it.attachments.map((a) =>
|
|
4965
5026
|
'<span class="att-chip2">' + esc(String(a).split(/[\\/]/).pop()) + "</span>").join("") + "</div>"
|
|
4966
5027
|
: "") +
|
|
4967
|
-
|
|
4968
|
-
(it.consumed ? "" : " · " + t("待送达")) + "</div></div></div>";
|
|
5028
|
+
"</div></div>";
|
|
4969
5029
|
}
|
|
5030
|
+
// 协议尾行是给编排器判收的机器标记,不该出现在聊天正文里(真源在提示词约定)
|
|
5031
|
+
const body = String(it.text || "").replace(/\n*DIRECT_DONE:.*\s*$/s, "").trim();
|
|
5032
|
+
// 运行中的步骤还没有正文(output/summary 都空):给占位,不渲染空气泡
|
|
5033
|
+
const shown = body || (it.status === "running" ? t("正在执行…") : t("(本轮无文本输出)"));
|
|
5034
|
+
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
4970
5035
|
return '<div class="chat-row">' +
|
|
5036
|
+
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
4971
5037
|
'<div class="chat-bubble agent">' +
|
|
5038
|
+
'<pre class="chat-body">' + esc(shown) + "</pre>" +
|
|
4972
5039
|
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at || "") +
|
|
4973
5040
|
(it.note ? " · " + esc(it.note) : "") +
|
|
4974
|
-
(it.status && it.status !== "done" ? " · " + esc(it.status) : "") + "</div>"
|
|
4975
|
-
'<pre class="chat-body">' + esc(it.text || "") + "</pre></div></div>";
|
|
5041
|
+
(it.status && it.status !== "done" ? " · " + esc(it.status) : "") + "</div></div></div>";
|
|
4976
5042
|
}).join("") || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
4977
5043
|
const hint = $("rd-chat-hint");
|
|
4978
5044
|
if (hint) hint.textContent = active
|
|
@@ -5487,6 +5553,132 @@ function bindSelById(id) {
|
|
|
5487
5553
|
|
|
5488
5554
|
function bindRepaint() { S.bindSig = null; renderBindings(); }
|
|
5489
5555
|
|
|
5556
|
+
/* 一键推荐绑定:给每个链为空的 CLI 按协议适配规则预填一条主模型。
|
|
5557
|
+
* 推荐顺序:显式协议原生匹配 > auto 且 wire_caps 命中;默认模型停用的厂商
|
|
5558
|
+
* 降权(当前键位是坏的,除非只剩它,否则不当首选——失效链重推荐实测);
|
|
5559
|
+
* 同分按 priority / 数组序稳定。只预填草稿态(dirty),逐条看清后各自保存
|
|
5560
|
+
* 或「全部保存」,健康链绝不被覆盖。 */
|
|
5561
|
+
function recommendFor(c) {
|
|
5562
|
+
const allow = bindAllowedProtocols(c.orch_kind);
|
|
5563
|
+
const provs = (S.providers || []).filter((p) => chainProvUsable(p, allow));
|
|
5564
|
+
if (!provs.length) return null;
|
|
5565
|
+
const score = (p) => {
|
|
5566
|
+
const native = allow.indexOf(p.protocol) >= 0;
|
|
5567
|
+
const badDefault = (p.models || []).some((m) => m.name === p.model &&
|
|
5568
|
+
(m.hidden || m.enabled === false)) ? 500 : 0;
|
|
5569
|
+
return (native ? 0 : 1) * 1000 + badDefault + (p.priority || 0);
|
|
5570
|
+
};
|
|
5571
|
+
provs.sort((a, b) => score(a) - score(b));
|
|
5572
|
+
const p = provs[0];
|
|
5573
|
+
const models = (p.models || []).filter((m) => !m.hidden && m.enabled !== false);
|
|
5574
|
+
if (!models.length) return null;
|
|
5575
|
+
return { p: p.id, m: models[0].name };
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5578
|
+
/* 供应商对某 CLI 是否「推荐可用」:启用、协议适配,且至少有一把不在冷却期的
|
|
5579
|
+
* KEY(全冷却=欠费失效,推荐了也白推荐——解析层照样失败)。 */
|
|
5580
|
+
function chainProvUsable(p, allow) {
|
|
5581
|
+
if (!p || p.enabled === false) return false;
|
|
5582
|
+
if (!provAdaptedProto(p, allow)) return false;
|
|
5583
|
+
const ks = p.keys || [];
|
|
5584
|
+
if (!ks.length) return true; // 老数据单 KEY(api_key 镜像):无 keys 结构视为可用
|
|
5585
|
+
return ks.some((k) => k.enabled !== false && !k.cooling);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
/* 绑定链里的死条目:厂商已删/停用/协议不再适配/KEY 全冷却,或该模型已被
|
|
5589
|
+
* 隐藏停用。这些条目解析时会被跳过,留在链首等于配了个寂寞。 */
|
|
5590
|
+
function chainDeadReasons(c, chain) {
|
|
5591
|
+
const allow = bindAllowedProtocols(c.orch_kind);
|
|
5592
|
+
const dead = (chain || []).map((x) => {
|
|
5593
|
+
if (!x.p) return ""; // 空 p = CLI 默认凭据,不算死
|
|
5594
|
+
const pv = (S.providers || []).find((p) => p.id === x.p);
|
|
5595
|
+
if (!pv) return t("供应商已删除");
|
|
5596
|
+
if (pv.enabled === false) return t("供应商已停用");
|
|
5597
|
+
if (!provAdaptedProto(pv, allow)) return t("协议不匹配");
|
|
5598
|
+
const ks = pv.keys || [];
|
|
5599
|
+
if (ks.length && !ks.some((k) => k.enabled !== false && !k.cooling))
|
|
5600
|
+
return t("密钥全部冷却中");
|
|
5601
|
+
const mm = (pv.models || []).find((m) => m.name === x.m);
|
|
5602
|
+
if (mm && (mm.hidden || mm.enabled === false)) return t("模型已停用");
|
|
5603
|
+
return "";
|
|
5604
|
+
});
|
|
5605
|
+
return dead;
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
function autoBindAll() {
|
|
5609
|
+
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5610
|
+
let filled = 0, skipped = 0, noProv = 0, refilled = 0;
|
|
5611
|
+
for (const c of targets) {
|
|
5612
|
+
const st = bindSelById(c.id);
|
|
5613
|
+
if (!st.chain.length) {
|
|
5614
|
+
const rec = recommendFor(c);
|
|
5615
|
+
if (!rec) { noProv++; continue; }
|
|
5616
|
+
st.chain = [rec];
|
|
5617
|
+
st.dirty = true;
|
|
5618
|
+
filled++;
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5621
|
+
// 链非空:只处理死链——整条链全部死透(或只有链首且已死)才重推荐,
|
|
5622
|
+
// 部分存活的长链保留(还降级得动,不擅自覆盖用户手工配的结构)
|
|
5623
|
+
const dead = chainDeadReasons(c, st.chain);
|
|
5624
|
+
const aliveN = dead.filter((d) => !d).length;
|
|
5625
|
+
const allDead = aliveN === 0;
|
|
5626
|
+
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
5627
|
+
const rec = recommendFor(c);
|
|
5628
|
+
if (allDead && rec) {
|
|
5629
|
+
st.chain = [rec];
|
|
5630
|
+
st.dirty = true;
|
|
5631
|
+
refilled++;
|
|
5632
|
+
} else if (allDead) {
|
|
5633
|
+
noProv++;
|
|
5634
|
+
} else if (headDead && rec && chainKey(st.chain) !== chainKey([rec])) {
|
|
5635
|
+
// 链首死但链内还有活的备选:只把推荐的新链首插到最前面(原降级序保留)
|
|
5636
|
+
const hasRec = st.chain.some((x) => x.p === rec.p && x.m === rec.m);
|
|
5637
|
+
if (!hasRec) {
|
|
5638
|
+
st.chain = [rec].concat(st.chain);
|
|
5639
|
+
st.dirty = true;
|
|
5640
|
+
refilled++;
|
|
5641
|
+
} else {
|
|
5642
|
+
// 推荐项已在链内:把它升到链首
|
|
5643
|
+
st.chain = [rec].concat(st.chain.filter((x) => x !== rec));
|
|
5644
|
+
st.dirty = true;
|
|
5645
|
+
refilled++;
|
|
5646
|
+
}
|
|
5647
|
+
} else {
|
|
5648
|
+
skipped++;
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
bindRepaint();
|
|
5652
|
+
if (filled && refilled) {
|
|
5653
|
+
toast(t("已为 %1 个空链 CLI 预填推荐,并修复 %2 条失效链——确认后保存。")
|
|
5654
|
+
.replace("%1", filled).replace("%2", refilled));
|
|
5655
|
+
} else if (filled) {
|
|
5656
|
+
toast(t("已为 %1 个 CLI 预填推荐模型,确认无误后点各卡片「保存」,或点右上「全部保存」。").replace("%1", filled));
|
|
5657
|
+
} else if (refilled) {
|
|
5658
|
+
toast(t("已为 %1 条失效链重新推荐(原厂商失效/停用/模型停用)——确认后点「全部保存」生效。")
|
|
5659
|
+
.replace("%1", refilled));
|
|
5660
|
+
} else if (noProv && !skipped) {
|
|
5661
|
+
toast(t("没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。"), true);
|
|
5662
|
+
} else if (skipped && !filled && !refilled) {
|
|
5663
|
+
toast(t("所有 CLI 都已配置模型链,无需推荐。"));
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5667
|
+
async function saveAllBindings() {
|
|
5668
|
+
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5669
|
+
const dirty = targets.filter((c) => (S.bindSel[c.id] || {}).dirty);
|
|
5670
|
+
for (const c of dirty) await saveBinding(c.id);
|
|
5671
|
+
if (dirty.length) toast(t("已保存 %1 个 CLI 的绑定。").replace("%1", dirty.length));
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
/* 有未保存草稿时显示「全部保存」按钮(在 poll 重绘里顺带刷新) */
|
|
5675
|
+
function syncSaveAllBtn() {
|
|
5676
|
+
const btn = $("btn-saveall-bind");
|
|
5677
|
+
if (!btn) return;
|
|
5678
|
+
const anyDirty = Object.values(S.bindSel || {}).some((st) => st.dirty);
|
|
5679
|
+
btn.classList.toggle("hidden", !anyDirty);
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5490
5682
|
function bindModelBox(c, provId) {
|
|
5491
5683
|
const st = bindSelById(c.id);
|
|
5492
5684
|
const allow = bindAllowedProtocols(c.orch_kind);
|