codebee 0.1.20 → 0.1.22
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 +171 -165
- package/README.md +36 -9
- package/app/core/automation.py +6 -6
- package/app/core/capability.py +16 -2
- package/app/core/catalog.py +1 -1
- package/app/core/covergen.py +40 -4
- package/app/core/dispatch.py +141 -0
- package/app/core/flows.py +5 -0
- package/app/core/jobs.py +281 -166
- package/app/core/knowledge.py +19 -7
- package/app/core/manager.py +30 -23
- package/app/core/market_remote.py +66 -22
- package/app/core/modelhub.py +145 -11
- package/app/core/pipeline.py +124 -31
- package/app/core/router.py +57 -12
- package/app/core/runner.py +2 -1
- package/app/core/selfupdate.py +24 -14
- package/app/core/settings.py +3 -3
- package/app/core/skills.py +15 -11
- package/app/core/store.py +20 -14
- package/app/core/task_compile.py +83 -0
- package/app/core/zentao.py +26 -6
- package/app/main.py +20 -12
- package/app/pet.py +127 -30
- package/app/ui/app.js +197 -76
- package/app/ui/i18n.js +22 -3
- package/app/ui/index.html +11 -11
- package/app/ui/style.css +32 -0
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -297,6 +297,93 @@ function urlAuth(u) {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
/* ---------------------------------------------------------- 工具 */
|
|
300
|
+
let _requestBusyCount = 0;
|
|
301
|
+
let _lastActionButton = null;
|
|
302
|
+
let _lastActionAt = 0;
|
|
303
|
+
const _requestBusyButtons = new WeakMap();
|
|
304
|
+
|
|
305
|
+
/* 记录发起请求的按钮。写请求统一在 api() 里挂忙碌态,避免每个业务入口各写一套;
|
|
306
|
+
* 确认框的按钮不覆盖原始操作按钮,但会续期,使“确认后执行”仍反馈在原按钮上。 */
|
|
307
|
+
document.addEventListener("click", (e) => {
|
|
308
|
+
const btn = e.target && e.target.closest ? e.target.closest("button") : null;
|
|
309
|
+
if (!btn) return;
|
|
310
|
+
if (btn.classList.contains("request-busy")) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
e.stopImmediatePropagation();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
_lastActionAt = performance.now();
|
|
316
|
+
if (!btn.closest("#ask")) _lastActionButton = btn;
|
|
317
|
+
}, true);
|
|
318
|
+
|
|
319
|
+
function requestBusyStart(opts) {
|
|
320
|
+
const method = String((opts && opts.method) || "GET").toUpperCase();
|
|
321
|
+
if (opts && opts.busy === false) return null;
|
|
322
|
+
if ((method === "GET" || method === "HEAD") && !(opts && opts.busy)) return null;
|
|
323
|
+
|
|
324
|
+
_requestBusyCount++;
|
|
325
|
+
let bar = $("request-progress");
|
|
326
|
+
if (!bar && document.body) {
|
|
327
|
+
bar = document.createElement("div");
|
|
328
|
+
bar.id = "request-progress";
|
|
329
|
+
bar.className = "request-progress";
|
|
330
|
+
bar.setAttribute("role", "status");
|
|
331
|
+
bar.setAttribute("aria-label", t("操作处理中"));
|
|
332
|
+
bar.setAttribute("aria-hidden", "true");
|
|
333
|
+
document.body.appendChild(bar);
|
|
334
|
+
}
|
|
335
|
+
if (bar) {
|
|
336
|
+
bar.classList.add("active");
|
|
337
|
+
bar.setAttribute("aria-hidden", "false");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let btn = opts && opts.busyElement;
|
|
341
|
+
if (typeof btn === "string") btn = document.querySelector(btn);
|
|
342
|
+
if (!btn && performance.now() - _lastActionAt < 2000) btn = _lastActionButton;
|
|
343
|
+
if (!btn || !document.documentElement.contains(btn)) btn = null;
|
|
344
|
+
if (btn) {
|
|
345
|
+
let state = _requestBusyButtons.get(btn);
|
|
346
|
+
if (!state) {
|
|
347
|
+
state = {
|
|
348
|
+
count: 0,
|
|
349
|
+
ariaBusy: btn.getAttribute("aria-busy"),
|
|
350
|
+
ariaDisabled: btn.getAttribute("aria-disabled"),
|
|
351
|
+
};
|
|
352
|
+
_requestBusyButtons.set(btn, state);
|
|
353
|
+
}
|
|
354
|
+
state.count++;
|
|
355
|
+
btn.classList.add("request-busy");
|
|
356
|
+
btn.setAttribute("aria-busy", "true");
|
|
357
|
+
btn.setAttribute("aria-disabled", "true");
|
|
358
|
+
}
|
|
359
|
+
return { button: btn };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function requestBusyEnd(token) {
|
|
363
|
+
if (!token) return;
|
|
364
|
+
_requestBusyCount = Math.max(0, _requestBusyCount - 1);
|
|
365
|
+
const btn = token.button;
|
|
366
|
+
const state = btn && _requestBusyButtons.get(btn);
|
|
367
|
+
if (state) {
|
|
368
|
+
state.count--;
|
|
369
|
+
if (state.count <= 0) {
|
|
370
|
+
btn.classList.remove("request-busy");
|
|
371
|
+
if (state.ariaBusy === null) btn.removeAttribute("aria-busy");
|
|
372
|
+
else btn.setAttribute("aria-busy", state.ariaBusy);
|
|
373
|
+
if (state.ariaDisabled === null) btn.removeAttribute("aria-disabled");
|
|
374
|
+
else btn.setAttribute("aria-disabled", state.ariaDisabled);
|
|
375
|
+
_requestBusyButtons.delete(btn);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (_requestBusyCount === 0) {
|
|
379
|
+
const bar = $("request-progress");
|
|
380
|
+
if (bar) {
|
|
381
|
+
bar.classList.remove("active");
|
|
382
|
+
bar.setAttribute("aria-hidden", "true");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
300
387
|
async function api(path, opts) {
|
|
301
388
|
opts = opts || {};
|
|
302
389
|
// 可选超时:长请求(如外部插件下载)传 opts.timeout,网络卡死时也能
|
|
@@ -306,21 +393,30 @@ async function api(path, opts) {
|
|
|
306
393
|
ctrl = new AbortController();
|
|
307
394
|
var timer = setTimeout(() => ctrl.abort(), opts.timeout);
|
|
308
395
|
}
|
|
309
|
-
|
|
396
|
+
const busyToken = requestBusyStart(opts);
|
|
397
|
+
const fetchOpts = Object.assign({}, opts);
|
|
398
|
+
delete fetchOpts.timeout;
|
|
399
|
+
delete fetchOpts.busy;
|
|
400
|
+
delete fetchOpts.busyElement;
|
|
310
401
|
try {
|
|
311
|
-
res
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
402
|
+
let res;
|
|
403
|
+
try {
|
|
404
|
+
res = await fetch(path, Object.assign({ headers: authHeaders() }, fetchOpts,
|
|
405
|
+
ctrl ? { signal: ctrl.signal } : {}));
|
|
406
|
+
} catch (e) {
|
|
407
|
+
if (ctrl && e.name === "AbortError") throw new Error(t("请求超时,请重试或检查网络"));
|
|
408
|
+
throw e;
|
|
409
|
+
}
|
|
410
|
+
if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
|
|
411
|
+
let data = null;
|
|
412
|
+
try { data = await res.json(); } catch (e) { /* ignore */ }
|
|
413
|
+
if (res.status === 423 && data && data.control) setControl(data.control);
|
|
414
|
+
if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
415
|
+
return data;
|
|
315
416
|
} finally {
|
|
316
417
|
if (timer) clearTimeout(timer);
|
|
418
|
+
requestBusyEnd(busyToken);
|
|
317
419
|
}
|
|
318
|
-
if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
|
|
319
|
-
let data = null;
|
|
320
|
-
try { data = await res.json(); } catch (e) { /* ignore */ }
|
|
321
|
-
if (res.status === 423 && data && data.control) setControl(data.control);
|
|
322
|
-
if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
323
|
-
return data;
|
|
324
420
|
}
|
|
325
421
|
|
|
326
422
|
/* 轻提示:3.5s 自动消失 */
|
|
@@ -426,13 +522,13 @@ function jsq(s) {
|
|
|
426
522
|
}
|
|
427
523
|
|
|
428
524
|
function statusChip(st) {
|
|
429
|
-
const zh = { queued: t("
|
|
525
|
+
const zh = { queued: t("正在启动"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") };
|
|
430
526
|
return '<span class="chip ' + esc(st) + '">' + (zh[st] || esc(st)) + "</span>";
|
|
431
527
|
}
|
|
432
528
|
|
|
433
529
|
/* 运行状态文案(传 run 对象):退避窗口内的续跑副本写明「将于 HH:MM 自动
|
|
434
530
|
* 续跑」,别让 5 分钟等待看起来像卡死/资源排队(2026-09-18 重写任务误判案)。
|
|
435
|
-
*
|
|
531
|
+
* 到点后翻回「正在启动」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
|
|
436
532
|
function runStatusText(run) {
|
|
437
533
|
const st = String((run && run.status) || "");
|
|
438
534
|
if (st === "queued" && run && run.resume_enqueue_at) {
|
|
@@ -440,7 +536,7 @@ function runStatusText(run) {
|
|
|
440
536
|
if (!isNaN(at) && Date.now() < at)
|
|
441
537
|
return t("将于 {0} 自动续跑", String(run.resume_enqueue_at).slice(11, 16));
|
|
442
538
|
}
|
|
443
|
-
return { queued: t("
|
|
539
|
+
return { queued: t("正在启动"), running: t("运行中"), done: t("完成"),
|
|
444
540
|
failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") }[st] || st;
|
|
445
541
|
}
|
|
446
542
|
|
|
@@ -632,7 +728,7 @@ async function healthOp(op) {
|
|
|
632
728
|
async function healthDisableModel(pid, model) {
|
|
633
729
|
if (!pid || !model) { closeModal(); return; }
|
|
634
730
|
const yes = await uiConfirm(
|
|
635
|
-
t("确认禁用模型 {0}
|
|
731
|
+
t("确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在模型调度页重新启用。").replace("{0}", pid + " · " + model),
|
|
636
732
|
{ title: t("禁用模型"), danger: true, ok: t("禁用") });
|
|
637
733
|
if (!yes) return;
|
|
638
734
|
try {
|
|
@@ -657,7 +753,7 @@ async function healthDisableModel(pid, model) {
|
|
|
657
753
|
async function healthDisableProvider(pid) {
|
|
658
754
|
if (!pid) { closeModal(); return; }
|
|
659
755
|
const yes = await uiConfirm(
|
|
660
|
-
t("
|
|
756
|
+
t("确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在模型调度页重新启用。"),
|
|
661
757
|
{ title: t("禁用厂商"), danger: true, ok: t("禁用") });
|
|
662
758
|
if (!yes) return;
|
|
663
759
|
try {
|
|
@@ -783,7 +879,7 @@ async function ctrlClick() {
|
|
|
783
879
|
function startCtrlHeartbeat() {
|
|
784
880
|
setInterval(() => {
|
|
785
881
|
if (S.control && S.control.mine) {
|
|
786
|
-
api("/api/control/heartbeat", { method: "POST", body: "{}" })
|
|
882
|
+
api("/api/control/heartbeat", { method: "POST", body: "{}", busy: false })
|
|
787
883
|
.then((d) => setControl(d.control)).catch(() => {});
|
|
788
884
|
}
|
|
789
885
|
}, 15000);
|
|
@@ -855,7 +951,7 @@ function renderBindings() {
|
|
|
855
951
|
}
|
|
856
952
|
bbox.innerHTML = targets.map((c) => {
|
|
857
953
|
const b = (S.bindings || {})[c.id] || {};
|
|
858
|
-
const opts = '<option value="">' + t("
|
|
954
|
+
const opts = '<option value="">' + t("自动推荐(推荐)") + '</option>' + bindable.map((p) => {
|
|
859
955
|
const adaptedOnly = p.protocol !== "anthropic" && p.protocol !== "openai";
|
|
860
956
|
return '<option value="' + esc(p.id) + '"' + (b.provider_id === p.id ? " selected" : "") + ">" +
|
|
861
957
|
esc(p.name) + t("(") + esc(protoLabel(p)) + (adaptedOnly ? t(" · 已适配") : "") +
|
|
@@ -879,13 +975,13 @@ function renderBindings() {
|
|
|
879
975
|
return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
|
|
880
976
|
'<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
|
|
881
977
|
'<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
|
|
882
|
-
'<p class="hint">' + t("
|
|
978
|
+
'<p class="hint">' + t("默认不需要绑定:系统会按任务类型、难度、成本与健康状态自动推荐可用模型;只有需要固定厂商、模型或降级顺序时才在这里绑定。绑定后编排调用会注入该供应商的 API key 与地址;完全未指定时保持自动调度,没有兼容可用供应商才使用 CLI 自身配置。") + '</p>' +
|
|
883
979
|
bindModelBox(c, b.provider_id) + offWarn + protoWarn + chainWarn +
|
|
884
980
|
'<div class="ops"><label class="toggle"><input type="checkbox" id="binddiff-' + esc(c.id) + '"' +
|
|
885
981
|
(b.difficulty_routing ? " checked" : "") + '>' + t(" 按难度自动选模型(简单/困难)") + '</label>' +
|
|
886
982
|
'<button class="ghost small" onclick="saveBinding(\'' + esc(c.id) + '\')">' + t("保存") + '</button></div></div>';
|
|
887
983
|
}).join("") +
|
|
888
|
-
(!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI
|
|
984
|
+
(!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI——先到「本机智能体」页安装并启用。") + '</p>' : "") +
|
|
889
985
|
(nonBindable ? '<p class="hint">' + t("另有 ") + nonBindable +
|
|
890
986
|
t(" 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。") + '</p>' : "");
|
|
891
987
|
}
|
|
@@ -974,7 +1070,7 @@ async function batchProvOp(op) {
|
|
|
974
1070
|
const tips = {
|
|
975
1071
|
enable: t("启用所选 ") + ids.length + t(" 个供应商?"),
|
|
976
1072
|
disable: t("停用所选 ") + ids.length + t(" 个供应商?\n停用后其绑定会回落为 CLI 默认;配置与模型列表都保留,可随时再启用。"),
|
|
977
|
-
delete: t("删除所选 ") + ids.length + t(" 个供应商?\n
|
|
1073
|
+
delete: t("删除所选 ") + ids.length + t(" 个供应商?\n相关显式模型调度会自动解除,此操作不可撤销。"),
|
|
978
1074
|
};
|
|
979
1075
|
if (!await uiConfirm(tips[op] || (t("执行「") + op + t("」?")))) return;
|
|
980
1076
|
try {
|
|
@@ -1687,7 +1783,7 @@ async function openImportDialog() {
|
|
|
1687
1783
|
'<button class="ghost" onclick="closeModal()">' + t("取消") + '</button>');
|
|
1688
1784
|
let data;
|
|
1689
1785
|
try {
|
|
1690
|
-
data = await api("/api/models/sources");
|
|
1786
|
+
data = await api("/api/models/sources", { busy: true });
|
|
1691
1787
|
} catch (e) {
|
|
1692
1788
|
$("modal-body").innerHTML = '<div class="msg bad">' + t("扫描失败:") + esc(e.message) + "</div>";
|
|
1693
1789
|
return;
|
|
@@ -1780,11 +1876,15 @@ async function doImport() {
|
|
|
1780
1876
|
|
|
1781
1877
|
async function saveBinding(id) {
|
|
1782
1878
|
const st = bindSelById(id);
|
|
1879
|
+
const provSel = $("bindprov-" + id);
|
|
1783
1880
|
// 主供应商跟链首走:模型链是唯一真源,链首非空时供应商下拉必须与之一致
|
|
1784
1881
|
// (下拉是旧状态时把旧值发上去,后端「显式指定」就会盖回停用的旧供应商)。
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1882
|
+
// 空链时保留下拉选择:这是“只锁定供应商,模型按该供应商默认/难度选”的
|
|
1883
|
+
// 合法显式覆盖,不能重置成自动模式。
|
|
1884
|
+
const headP = st.chain.length ? st.chain[0].p : (provSel ? provSel.value : "");
|
|
1885
|
+
if (st.chain.length && provSel && provSel.value !== (headP || "")) {
|
|
1886
|
+
provSel.value = headP || "";
|
|
1887
|
+
}
|
|
1788
1888
|
try {
|
|
1789
1889
|
await api("/api/models/binding", { method: "POST", body: JSON.stringify({
|
|
1790
1890
|
agent_id: id, provider_id: provSel ? provSel.value : "",
|
|
@@ -1926,7 +2026,8 @@ async function createTask() {
|
|
|
1926
2026
|
msg.textContent = t("目标有点简短,先问几个问题…");
|
|
1927
2027
|
try {
|
|
1928
2028
|
const cq = await api("/api/tasks/clarify", {
|
|
1929
|
-
method: "POST",
|
|
2029
|
+
method: "POST", timeout: 75000,
|
|
2030
|
+
body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
|
|
1930
2031
|
const qs = (cq && cq.questions) || [];
|
|
1931
2032
|
if (qs.length) {
|
|
1932
2033
|
S.clarifyDone = true; // 本轮已采访;再点发送直接创建
|
|
@@ -1984,7 +2085,8 @@ async function createTask() {
|
|
|
1984
2085
|
if (critics.length) payload.critics = critics;
|
|
1985
2086
|
}
|
|
1986
2087
|
try {
|
|
1987
|
-
const r = await api("/api/tasks", { method: "POST",
|
|
2088
|
+
const r = await api("/api/tasks", { method: "POST", timeout: 30000,
|
|
2089
|
+
body: JSON.stringify(payload) });
|
|
1988
2090
|
msg.textContent = t("已创建,跳转运行页…");
|
|
1989
2091
|
// 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
|
|
1990
2092
|
// 不刷的话对话页签不会就绪,自动选卡落不到「对话」
|
|
@@ -2580,7 +2682,7 @@ async function retryTask(id) {
|
|
|
2580
2682
|
async function continueSerial(id) {
|
|
2581
2683
|
let info;
|
|
2582
2684
|
try {
|
|
2583
|
-
info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info");
|
|
2685
|
+
info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info", { busy: true });
|
|
2584
2686
|
} catch (e) { toast(t("无法续写:") + e.message, true); return; }
|
|
2585
2687
|
if (!info || !info.can) {
|
|
2586
2688
|
toast(t("无法续写:") + ((info && info.reason) || t("当前状态不支持")), true);
|
|
@@ -3260,7 +3362,7 @@ function drawTaskDetail(key, runs) {
|
|
|
3260
3362
|
// 取消收尾把僵尸步骤落成「已取消」时要立即重画,不等条数变化;
|
|
3261
3363
|
// 作品信息状态入签名:后台一键生成 running→done 要立刻反映到成果区面板
|
|
3262
3364
|
const bmTask = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
3263
|
-
//
|
|
3365
|
+
// 自动续跑退避相位入签名:预定启动时刻过了之后 chip 翻为「正在启动」
|
|
3264
3366
|
const lr0 = runs[0] || {};
|
|
3265
3367
|
const resumePending = (lr0.resume_enqueue_at &&
|
|
3266
3368
|
Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
|
|
@@ -3274,8 +3376,8 @@ function drawTaskDetail(key, runs) {
|
|
|
3274
3376
|
const ordered = runs.slice(); // 详情按最近运行优先,方便排查
|
|
3275
3377
|
const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
|
|
3276
3378
|
const active = runs.some((r) => r.status === "running" || r.status === "queued");
|
|
3277
|
-
//
|
|
3278
|
-
//
|
|
3379
|
+
// 活跃态细分真实状态:queued 仅用于自动续跑退避或创建到起跑的瞬时状态;
|
|
3380
|
+
// 前者在 chip 上写明下一轮何时起跑,避免退避窗口看起来像卡死。
|
|
3279
3381
|
const activeRun0 = runs.find((r) => r.status === "running" || r.status === "queued");
|
|
3280
3382
|
const st = activeRun0 ? activeRun0.status : latest.status;
|
|
3281
3383
|
const resumeIn = (st === "queued" && resumePending)
|
|
@@ -3285,7 +3387,7 @@ function drawTaskDetail(key, runs) {
|
|
|
3285
3387
|
chip.className = "chip " + st;
|
|
3286
3388
|
chip.textContent = resumeIn
|
|
3287
3389
|
? t("将于 ") + resumeIn + t(" 自动续跑(第 ") + (Number(latest.auto_resumes) || 0) + t(" 次)")
|
|
3288
|
-
: ({ queued: t("
|
|
3390
|
+
: ({ queued: t("正在启动"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") }[st] || st);
|
|
3289
3391
|
const bpt = $("btn-pause");
|
|
3290
3392
|
if (bpt) bpt.classList.add("hidden");
|
|
3291
3393
|
$("btn-delete").classList.add("hidden");
|
|
@@ -6057,12 +6159,17 @@ function chatResultHTML(run, res) {
|
|
|
6057
6159
|
if (res.turns) meta.push(res.turns + " " + t("轮对话"));
|
|
6058
6160
|
if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
|
|
6059
6161
|
const files = res.files || [];
|
|
6060
|
-
const chips = files.map((f) =>
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6162
|
+
const chips = files.map((f) => {
|
|
6163
|
+
const fUrl = urlAuth("/api/runs/" + encodeURIComponent(run.id) + "/file?name=" + encodeURIComponent(f.name));
|
|
6164
|
+
return '<div class="file-chip has-actions">' +
|
|
6165
|
+
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
6166
|
+
'<span class="p" title="' + esc(f.name + " · " + fmtSize(f.size)) + '">' + esc(f.name) + "</span>" +
|
|
6167
|
+
'<span class="fsz">' + fmtSize(f.size) + "</span>" +
|
|
6168
|
+
'<span class="fbtns">' +
|
|
6169
|
+
'<button type="button" class="chip-btn" title="' + esc(t("预览")) + '" onclick="artPopup(\'' + esc(run.id) + "', '" + esc(f.name) + "', " + (Number(f.size) || 0) + ')"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg></button>' +
|
|
6170
|
+
'<a class="chip-btn" href="' + fUrl + '" download="' + esc(f.name) + '" title="' + esc(t("下载")) + '"><svg class="ico" aria-hidden="true"><use href="#i-arrow-left"/></svg></a>' +
|
|
6171
|
+
"</span></div>";
|
|
6172
|
+
}).join("");
|
|
6066
6173
|
return '<div class="chat-row">' +
|
|
6067
6174
|
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="' + icon + '"></use></svg></span>' +
|
|
6068
6175
|
'<div class="chat-result' + (ok ? "" : bad ? " bad" : " off") + '">' +
|
|
@@ -6367,6 +6474,7 @@ function bindVoiceInput() {
|
|
|
6367
6474
|
if (e.results[i].isFinal) text += e.results[i][0].transcript;
|
|
6368
6475
|
}
|
|
6369
6476
|
if (text) {
|
|
6477
|
+
text = voiceFixup(text);
|
|
6370
6478
|
ta.value = (ta.value ? ta.value.replace(/\s+$/, "") + " " : "") + text;
|
|
6371
6479
|
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6372
6480
|
}
|
|
@@ -6378,6 +6486,24 @@ function bindVoiceInput() {
|
|
|
6378
6486
|
});
|
|
6379
6487
|
}
|
|
6380
6488
|
|
|
6489
|
+
/* 语音识别结果纠偏(借鉴 lexicon 个人词典):设置里维护的专有名词表
|
|
6490
|
+
* (localStorage orch.voice_terms,一行一个「错误→正确」或直接「术语」),
|
|
6491
|
+
* 识别结果做逐词替换。人名/书名/项目名是识别高频错位,词表是最小可用解。 */
|
|
6492
|
+
function voiceFixup(text) {
|
|
6493
|
+
let terms = [];
|
|
6494
|
+
try {
|
|
6495
|
+
const raw = localStorage.getItem("orch.voice_terms") || "";
|
|
6496
|
+
terms = raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
6497
|
+
} catch (e) {}
|
|
6498
|
+
for (const t of terms) {
|
|
6499
|
+
const m = t.split("→");
|
|
6500
|
+
if (m.length === 2 && m[0].trim() && m[1].trim()) {
|
|
6501
|
+
text = text.split(m[0].trim()).join(m[1].trim());
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
return text;
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6381
6507
|
/* ---------------------------------------------------------- 外观:皮肤 + 明暗(换肤) */
|
|
6382
6508
|
/* 调色板全部在 style.css(html[data-skin="X"],每套含夜间/日间两版变量);这里只放顺序
|
|
6383
6509
|
* 与文案。卡片预览色块用 skinPalette 从 CSS 变量实时取值,不在 JS 里重复写色值——
|
|
@@ -6785,7 +6911,7 @@ function updateChip(c) {
|
|
|
6785
6911
|
async function autoCheckUpdates() {
|
|
6786
6912
|
if (Date.now() - (S.updateCheckAt || 0) < 5 * 60 * 1000) return;
|
|
6787
6913
|
S.updateCheckAt = Date.now();
|
|
6788
|
-
try { await api("/api/catalog/check-updates", { method: "POST" }); } catch (e) { /* 忽略 */ }
|
|
6914
|
+
try { await api("/api/catalog/check-updates", { method: "POST", busy: false }); } catch (e) { /* 忽略 */ }
|
|
6789
6915
|
poll();
|
|
6790
6916
|
}
|
|
6791
6917
|
|
|
@@ -6877,12 +7003,11 @@ function chainDeadReasons(c, chain) {
|
|
|
6877
7003
|
return dead;
|
|
6878
7004
|
}
|
|
6879
7005
|
|
|
6880
|
-
/*
|
|
6881
|
-
*
|
|
6882
|
-
|
|
6883
|
-
function bindRepairAction(c, st) {
|
|
7006
|
+
/* 单条显式绑定链的推荐修复动作:需要改返回新链,不动返回 null。
|
|
7007
|
+
* 空链代表默认自动调度,不能悄悄写成持久绑定;已有链失效时才推荐替代链。 */
|
|
7008
|
+
function bindRepairAction(c, st, includeEmpty) {
|
|
6884
7009
|
const rec = recommendFor(c);
|
|
6885
|
-
if (!st.chain.length) return rec ? [rec] : null;
|
|
7010
|
+
if (!st.chain.length) return includeEmpty && rec ? [rec] : null;
|
|
6886
7011
|
const dead = chainDeadReasons(c, st.chain);
|
|
6887
7012
|
const allDead = dead.every((d) => d);
|
|
6888
7013
|
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
@@ -6897,7 +7022,7 @@ function autoBindAll() {
|
|
|
6897
7022
|
let filled = 0, skipped = 0, noProv = 0, refilled = 0;
|
|
6898
7023
|
for (const c of targets) {
|
|
6899
7024
|
const st = bindSelById(c.id);
|
|
6900
|
-
const act = bindRepairAction(c, st);
|
|
7025
|
+
const act = bindRepairAction(c, st, true);
|
|
6901
7026
|
if (!act) {
|
|
6902
7027
|
// 没动它:分清「健康链无需推荐」和「没有可推荐的」两种落空
|
|
6903
7028
|
if (!st.chain.length) { noProv++; continue; }
|
|
@@ -6926,9 +7051,8 @@ function autoBindAll() {
|
|
|
6926
7051
|
}
|
|
6927
7052
|
}
|
|
6928
7053
|
|
|
6929
|
-
/*
|
|
6930
|
-
*
|
|
6931
|
-
* 但直接落盘(自动场景没有人工确认环节);目录页——空默认模型直填。
|
|
7054
|
+
/* 厂商/模型停用·启用·删除后自动修复显式绑定:
|
|
7055
|
+
* 空链保持自动调度,不写入绑定;已有链失效时才直接落盘替代链。
|
|
6932
7056
|
* 没有合适的推荐就保持原样,什么都不绑。绑定页上用户手改中的草稿(dirty)
|
|
6933
7057
|
* 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
|
|
6934
7058
|
let _autoRebindRunning = false, _autoRebindAgain = false;
|
|
@@ -6941,11 +7065,11 @@ async function autoRebindSoon() {
|
|
|
6941
7065
|
for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
|
|
6942
7066
|
const st = bindSelById(c.id);
|
|
6943
7067
|
if (st.dirty) continue; // 用户手改中,不覆盖草稿
|
|
6944
|
-
const act = bindRepairAction(c, st);
|
|
7068
|
+
const act = bindRepairAction(c, st, false);
|
|
6945
7069
|
if (!act) continue;
|
|
6946
7070
|
const b = (S.bindings || {})[c.id] || {};
|
|
6947
7071
|
try {
|
|
6948
|
-
await api("/api/models/binding", { method: "POST", body: JSON.stringify({
|
|
7072
|
+
await api("/api/models/binding", { method: "POST", busy: false, body: JSON.stringify({
|
|
6949
7073
|
agent_id: c.id, provider_id: act[0].p,
|
|
6950
7074
|
chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
|
|
6951
7075
|
difficulty_routing: !!b.difficulty_routing }) });
|
|
@@ -6953,17 +7077,6 @@ async function autoRebindSoon() {
|
|
|
6953
7077
|
fixed++;
|
|
6954
7078
|
} catch (e) { /* 单条失败不打断,等下次变更再补 */ }
|
|
6955
7079
|
}
|
|
6956
|
-
for (const c of (S.catalog || []).filter((x) =>
|
|
6957
|
-
x.installed && x.config_writable && !fmtModel(x.model))) {
|
|
6958
|
-
const rec = recommendFor(c);
|
|
6959
|
-
if (!rec) continue;
|
|
6960
|
-
try {
|
|
6961
|
-
const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
|
|
6962
|
-
{ method: "POST", body: JSON.stringify({ model: rec.m }) });
|
|
6963
|
-
c.model = r.model || rec.m;
|
|
6964
|
-
fixed++;
|
|
6965
|
-
} catch (e) { /* 同上 */ }
|
|
6966
|
-
}
|
|
6967
7080
|
if (fixed) {
|
|
6968
7081
|
S.catSig = null; S.bindSig = null;
|
|
6969
7082
|
render();
|
|
@@ -7024,7 +7137,7 @@ function bindModelBox(c, provId) {
|
|
|
7024
7137
|
}).join("")
|
|
7025
7138
|
: '<span class="hint">' + t("未设置") + (provId
|
|
7026
7139
|
? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
|
|
7027
|
-
: t("
|
|
7140
|
+
: t("(自动推荐厂商与模型;没有兼容可用供应商时才使用 CLI 自身配置)")) + "</span>";
|
|
7028
7141
|
return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
|
|
7029
7142
|
'<div class="orch-row">' + chips +
|
|
7030
7143
|
'<button class="ghost small" onclick="bindToggle(\'' + esc(c.id) + '\')">' +
|
|
@@ -7827,11 +7940,11 @@ function autoTplPace(tp) {
|
|
|
7827
7940
|
});
|
|
7828
7941
|
}
|
|
7829
7942
|
|
|
7830
|
-
/* last_status → 徽章:
|
|
7943
|
+
/* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed=黄「已错过」 */
|
|
7831
7944
|
function autoStatusTag(tsk) {
|
|
7832
7945
|
if (tsk.last_status === "error") return '<span class="tag auto-tag-err">' + t("拉起失败") + "</span>";
|
|
7833
7946
|
if (tsk.last_status === "missed") return '<span class="tag auto-tag-miss">' + t("已错过") + "</span>";
|
|
7834
|
-
if (tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
|
|
7947
|
+
if (tsk.last_status === "started" || tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
|
|
7835
7948
|
return "";
|
|
7836
7949
|
}
|
|
7837
7950
|
|
|
@@ -8490,7 +8603,11 @@ function mkrDebounce() {
|
|
|
8490
8603
|
|
|
8491
8604
|
/* 拉一页:reset=清空已累积列表(首进/搜索/换来源/刷新后)。 */
|
|
8492
8605
|
async function mkrLoadPage(reset) {
|
|
8493
|
-
|
|
8606
|
+
// 翻页保持单飞;搜索/换来源/刷新属于重置请求,可以抢占旧请求。
|
|
8607
|
+
// 旧响应回来时由 requestKey 丢弃,不能覆盖用户刚选的新条件。
|
|
8608
|
+
if (S.mkrLoading && !reset) return;
|
|
8609
|
+
const requestKey = (S.mkrRequestKey || 0) + 1;
|
|
8610
|
+
S.mkrRequestKey = requestKey;
|
|
8494
8611
|
if (reset) { S.mkrAll = []; S.mkrOffset = 0; }
|
|
8495
8612
|
const q = (($("mkr-search") || {}).value || "").trim();
|
|
8496
8613
|
const srcSel = $("mkr-source");
|
|
@@ -8500,6 +8617,7 @@ async function mkrLoadPage(reset) {
|
|
|
8500
8617
|
S.mkrLoading = true;
|
|
8501
8618
|
let data = null;
|
|
8502
8619
|
try { data = await api(url); } catch (e) { data = null; }
|
|
8620
|
+
if (requestKey !== S.mkrRequestKey) return;
|
|
8503
8621
|
S.mkrLoading = false;
|
|
8504
8622
|
if (!data) { S.marketRemote = null; S.mkrAll = null; renderMarketRemote(); return; }
|
|
8505
8623
|
S.marketRemote = data;
|
|
@@ -8587,7 +8705,8 @@ async function mkrRefresh() {
|
|
|
8587
8705
|
const old = btn.textContent;
|
|
8588
8706
|
btn.textContent = t("拉取中…");
|
|
8589
8707
|
try {
|
|
8590
|
-
const r = await api("/api/market/remote/refresh", {
|
|
8708
|
+
const r = await api("/api/market/remote/refresh", {
|
|
8709
|
+
method: "POST", timeout: 120000, body: "{}" });
|
|
8591
8710
|
const bad = (r.refresh || []).filter((x) => !x.ok);
|
|
8592
8711
|
if (bad.length) toast(t("部分来源拉取失败:") + bad.map((x) => x.error || x.id).join(t(";")), true);
|
|
8593
8712
|
else toast(t("拉取成功"));
|
|
@@ -8878,7 +8997,7 @@ function suLastUpgradeRun() {
|
|
|
8878
8997
|
|
|
8879
8998
|
async function loadSelfupdate(force) {
|
|
8880
8999
|
try {
|
|
8881
|
-
SU = await api("/api/selfupdate" + (force ? "?force=1" : ""));
|
|
9000
|
+
SU = await api("/api/selfupdate" + (force ? "?force=1" : ""), { busy: !!force });
|
|
8882
9001
|
} catch (e) { SU = null; }
|
|
8883
9002
|
renderSu();
|
|
8884
9003
|
maybeWhatsnew();
|
|
@@ -8920,13 +9039,13 @@ function helpChapterBody(id) {
|
|
|
8920
9039
|
if (id === "quickstart") {
|
|
8921
9040
|
return '<div class="welcome-steps">' +
|
|
8922
9041
|
'<div class="wstep"><b>1</b><span>' + t("添加模型供应商:设置 → 模型接入,填入 API Key") + '</span></div>' +
|
|
8923
|
-
'<div class="wstep"><b>2</b><span>' + t("
|
|
9042
|
+
'<div class="wstep"><b>2</b><span>' + t("启用本机智能体:运行时默认自动推荐模型") + '</span></div>' +
|
|
8924
9043
|
'<div class="wstep"><b>3</b><span>' + t("新建任务:选工作目录、写目标,蜂群开工") + '</span></div>' +
|
|
8925
9044
|
'</div>' +
|
|
8926
9045
|
'<p class="help-note">' + t("任务跑起来后,详情页能看到步骤、蜂巢、成果文件与 Git 版本;「直接执行」类任务还能像聊天一样边跑边追加消息。") + '</p>' +
|
|
8927
9046
|
'<div class="welcome-acts">' +
|
|
8928
9047
|
'<button class="wl-btn primary" onclick="welcomeGo(\'models\')"><span>' + t("开始配置模型") + '</span><svg class="ico" aria-hidden="true"><use href="#i-chevron-r"></use></svg></button>' +
|
|
8929
|
-
'<button class="wl-btn" onclick="welcomeGo(\'bindings\')"><span>' + t("
|
|
9048
|
+
'<button class="wl-btn" onclick="welcomeGo(\'bindings\')"><span>' + t("模型调度(可选)") + '</span><svg class="ico" aria-hidden="true"><use href="#i-chevron-r"></use></svg></button>' +
|
|
8930
9049
|
'<button class="wl-btn ghost" onclick="welcomeClose()"><span>' + t("先跳过,直接体验") + '</span></button>' +
|
|
8931
9050
|
'</div>';
|
|
8932
9051
|
}
|
|
@@ -8935,8 +9054,8 @@ function helpChapterBody(id) {
|
|
|
8935
9054
|
'<p class="help-p">' + t("「设置 → 模型接入 → 添加供应商」:填名称、API Key、接口地址(一般用默认)。保存后点「获取模型列表」自动拉取该厂商的模型;网关不提供列表接口时直接手工填模型名即可。同一厂商可配多把 Key,按顺序轮换,某把欠费自动切下一把。") + '</p>' +
|
|
8936
9055
|
'<h3 class="help-h3">' + t("第二步:认识协议徽章") + '</h3>' +
|
|
8937
9056
|
'<p class="help-p">' + t("新供应商默认自动识别协议,保存后在后台实测支持哪些调用形态,结果以徽章标在卡片上。带「· chat」表示该模型只适合站内直连对话;codex 这类只讲 responses 协议的 CLI 用不了它,绑定页会自动剔除,不用自己排查。") + '</p>' +
|
|
8938
|
-
'<h3 class="help-h3">' + t("
|
|
8939
|
-
'<p class="help-p">' + t("
|
|
9057
|
+
'<h3 class="help-h3">' + t("第三步:启用本机智能体") + '</h3>' +
|
|
9058
|
+
'<p class="help-p">' + t("无需预先绑定模型。CodeBee 默认按任务类型、难度、成本和健康状态自动推荐;需要固定厂商、模型或降级顺序时,再到「设置 → 模型调度(可选)」指定。") + '</p>' +
|
|
8940
9059
|
'<h3 class="help-h3">' + t("报错速查") + '</h3>' +
|
|
8941
9060
|
'<ul class="help-list">' +
|
|
8942
9061
|
'<li><b>401</b><span>' + t("Key 无效或过期——检查或更换 Key。") + '</span></li>' +
|
|
@@ -9001,7 +9120,7 @@ function helpChapterBody(id) {
|
|
|
9001
9120
|
return '<div class="help-qa"><b>' + t("点「获取模型列表」报 404?") + '</b><p>' + t("部分网关不提供模型列表接口,属正常现象;只要能正常对话就不用管,模型名手工填即可。") + '</p></div>' +
|
|
9002
9121
|
'<div class="help-qa"><b>' + t("报 503 / 无可用渠道?") + '</b><p>' + t("该模型当前没有可用渠道,最常见是余额耗尽。查一下余额或换个模型;同一厂商配了多把 Key 会自动轮换。") + '</p></div>' +
|
|
9003
9122
|
'<div class="help-qa"><b>' + t("「冷却中」是什么意思?") + '</b><p>' + t("一把 Key 连续失败会被暂停 30 分钟,防止反复撞墙烧钱,到期自动恢复;也可以在密钥旁手动重置。") + '</p></div>' +
|
|
9004
|
-
'<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("
|
|
9123
|
+
'<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("新任务默认立即运行,不会排队;达到并发保护上限时本次会直接失败并提示稍后重试。只有自动续跑退避会显示预定时间;历史版本遗留的排队记录会由恢复机制立即接管或收口。") + '</p></div>' +
|
|
9005
9124
|
'<div class="help-qa"><b>' + t("状态里写着「将于 HH:MM 自动续跑」?") + '</b><p>' + t("这一步失败了,正在退避等待自动重试,到点会接着跑,不需要手动干预;等不及也可以在详情页手动重试。") + '</p></div>' +
|
|
9006
9125
|
'<div class="help-qa"><b>' + t("生成的文件在哪?") + '</b><p>' + t("写作类任务的章节、封面、报告都落在任务的运行目录,详情页「成果」页签可浏览和预览。代码类任务则在仓库的任务分支上改代码,详情页「版本」里审阅后再合并。") + '</p></div>' +
|
|
9007
9126
|
'<div class="help-qa"><b>' + t("忘了令牌 / 手机打不开页面?") + '</b><p>' + t("服务启动日志里有带令牌的完整访问地址;远程设备必须用带令牌的 URL 打开(或在令牌门里输入一次),否则会一直要求授权。") + '</p></div>' +
|
|
@@ -9194,6 +9313,7 @@ async function savePetSkin(skin) {
|
|
|
9194
9313
|
async function exportDiagBundle() {
|
|
9195
9314
|
const b = $("diag-export");
|
|
9196
9315
|
if (b) b.disabled = true;
|
|
9316
|
+
const busyToken = requestBusyStart({ method: "GET", busy: true, busyElement: b });
|
|
9197
9317
|
try {
|
|
9198
9318
|
const r = await fetch("/api/diagnostics/bundle", { headers: authHeaders() });
|
|
9199
9319
|
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
@@ -9209,6 +9329,7 @@ async function exportDiagBundle() {
|
|
|
9209
9329
|
} catch (e) {
|
|
9210
9330
|
toast(t("诊断包导出失败:") + e.message, true);
|
|
9211
9331
|
} finally {
|
|
9332
|
+
requestBusyEnd(busyToken);
|
|
9212
9333
|
if (b) b.disabled = false;
|
|
9213
9334
|
}
|
|
9214
9335
|
}
|
|
@@ -9216,7 +9337,7 @@ async function exportDiagBundle() {
|
|
|
9216
9337
|
/* 一键反馈 Issue:拉脱敏摘要 → 预填 GitHub Issue 新建页(用户亲手提交,不自动回传) */
|
|
9217
9338
|
async function reportIssue() {
|
|
9218
9339
|
try {
|
|
9219
|
-
const s = await api("/api/diagnostics/issue-summary");
|
|
9340
|
+
const s = await api("/api/diagnostics/issue-summary", { busy: true });
|
|
9220
9341
|
const url = "https://github.com/Vercel-By-WXP/CodeBee/issues/new" +
|
|
9221
9342
|
"?title=" + encodeURIComponent(s.title || "") +
|
|
9222
9343
|
"&body=" + encodeURIComponent(s.body || "");
|
|
@@ -9304,7 +9425,7 @@ async function suStartupCheck() {
|
|
|
9304
9425
|
}
|
|
9305
9426
|
|
|
9306
9427
|
/* ---------------------------------------------------------- 页签 & 初始化 */
|
|
9307
|
-
const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "
|
|
9428
|
+
const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
|
|
9308
9429
|
const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
|
|
9309
9430
|
|
|
9310
9431
|
function tabTitle(name) {
|
|
@@ -10062,7 +10183,7 @@ function renderBackupPreview(pv) {
|
|
|
10062
10183
|
rows.push('<span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
|
|
10063
10184
|
+ esc(pv.external_workdirs.join("、")) + "</span>");
|
|
10064
10185
|
if ((pv.busy_runs || []).length)
|
|
10065
|
-
rows.push('<span class="bad">' + esc(t("
|
|
10186
|
+
rows.push('<span class="bad">' + esc(t("有任务正在运行或等待自动续跑,先等它们结束再导入")) + "</span>");
|
|
10066
10187
|
el.innerHTML = rows.map((s) => "<div>" + s + "</div>").join("");
|
|
10067
10188
|
if (!(pv.busy_runs || []).length) $("bi-apply").classList.remove("hidden");
|
|
10068
10189
|
}
|
|
@@ -10746,7 +10867,7 @@ function cmdkOps() {
|
|
|
10746
10867
|
{ icon: "i-blocks", label: t("插件市场"), run: () => switchTab("market") },
|
|
10747
10868
|
{ icon: "i-book", label: t("经验库"), run: () => switchTab("skills") },
|
|
10748
10869
|
{ icon: "i-sigma", label: t("知识库"), run: () => switchTab("knowledge") },
|
|
10749
|
-
{ icon: "i-cpu", label: t("
|
|
10870
|
+
{ icon: "i-cpu", label: t("本机智能体"), run: () => switchTab("agents") },
|
|
10750
10871
|
{ icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
|
|
10751
10872
|
{ icon: "i-gear", label: t("设置"), run: () => enterSettings() },
|
|
10752
10873
|
{ icon: "i-bee", label: t("帮助中心"), run: () => welcomeOpen() },
|