codebee 0.1.98 → 0.1.100
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 +12 -1
- package/README.md +4 -4
- package/app/core/acp_client.py +426 -0
- package/app/core/backend_profiles.py +188 -0
- package/app/core/builtin_agent.py +48 -37
- package/app/core/command_auth.py +302 -0
- package/app/core/gitmod.py +6 -2
- package/app/core/jobs.py +3 -2
- package/app/core/manager.py +59 -4
- package/app/core/market_remote.py +20 -7
- package/app/core/modelhub.py +21 -5
- package/app/core/openapi.py +32 -0
- package/app/core/pipeline.py +34 -9
- package/app/core/project_memory.py +73 -6
- package/app/core/publish/auto.py +91 -20
- package/app/core/publish/fanqie.py +30 -0
- package/app/core/publish/flows-fanqie-calibrated.json +81 -0
- package/app/core/publish/flows-qimao-calibrated.json +58 -0
- package/app/core/publish/ledger.py +24 -1
- package/app/core/publish/manager.py +36 -9
- package/app/core/publish/qimao.py +17 -0
- package/app/core/quality_gate.py +7 -0
- package/app/core/registry.py +13 -0
- package/app/core/runner.py +13 -1
- package/app/main.py +75 -2
- package/app/ui/app.js +167 -15
- package/app/ui/board.css +5 -1
- package/app/ui/board.js +16 -0
- package/app/ui/i18n.js +26 -1
- package/app/ui/index.html +20 -1
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -812,8 +812,14 @@ function onTypeChange() {
|
|
|
812
812
|
}
|
|
813
813
|
|
|
814
814
|
function directProviders() {
|
|
815
|
-
/* 停用厂商不列出(用户拍板 2026-09-21「停用的不弄出来」);只列启用且有 key
|
|
816
|
-
|
|
815
|
+
/* 停用厂商不列出(用户拍板 2026-09-21「停用的不弄出来」);只列启用且有 key 的。
|
|
816
|
+
* 模型名单已抓取但启用数为零的也不列——「随厂商推荐」没有可落的模型。 */
|
|
817
|
+
return (S.providers || []).filter((p) => {
|
|
818
|
+
if (p.enabled === false || !p.api_key) return false;
|
|
819
|
+
const ms = p.models;
|
|
820
|
+
if (Array.isArray(ms) && ms.length && !ms.some((m) => m.enabled !== false && !m.hidden)) return false;
|
|
821
|
+
return true;
|
|
822
|
+
});
|
|
817
823
|
}
|
|
818
824
|
|
|
819
825
|
function renderDirectModelPicker() {
|
|
@@ -826,7 +832,8 @@ function renderDirectModelPicker() {
|
|
|
826
832
|
if (prev && provs.some((p) => p.id === prev)) ps.value = prev;
|
|
827
833
|
const p = provs.find((x) => x.id === ps.value);
|
|
828
834
|
const oldModel = ms.value;
|
|
829
|
-
|
|
835
|
+
// 只列启用模型(停用/已删除的与后端 resolve 同口径;已选模型被停用时自然回落「随厂商推荐」)
|
|
836
|
+
const names = p ? (p.models || []).filter((m) => !m.hidden && m.enabled !== false)
|
|
830
837
|
.map((m) => m.name).filter(Boolean) : [];
|
|
831
838
|
ms.innerHTML = '<option value="">' + t("随厂商推荐") + '</option>' + names.map((name) =>
|
|
832
839
|
'<option value="' + esc(name) + '">' + esc(name) + "</option>").join("");
|
|
@@ -4757,7 +4764,8 @@ window.loadProjectMemory = async function (taskId) {
|
|
|
4757
4764
|
box.innerHTML = entries.map((entry) => {
|
|
4758
4765
|
const canApprove = entry.status === "pending" && !entry.expired && entry.verified;
|
|
4759
4766
|
const canRestore = entry.status === "archived" && !entry.expired && entry.verified;
|
|
4760
|
-
const
|
|
4767
|
+
const feedback = entry.status === "approved" ? '<button type="button" class="ghost small" onclick="feedbackProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',true,' + Number(entry.version) + ')">' + esc(t("有用")) + '</button><button type="button" class="ghost small" onclick="feedbackProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',false,' + Number(entry.version) + ')">' + esc(t("无用")) + '</button>' : "";
|
|
4768
|
+
const actions = feedback + (canApprove ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'approve\',' + Number(entry.version) + ')">' + esc(t("批准")) + "</button><button type=\"button\" class=\"ghost small\" onclick=\"decideProjectMemory('" + jsq(taskId) + "','" + jsq(entry.id) + "','reject'," + Number(entry.version) + ')">' + esc(t("拒绝")) + "</button>" : "") +
|
|
4761
4769
|
(entry.status === "approved" ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'archive\',' + Number(entry.version) + ')">' + esc(t("归档")) + "</button>" : "") +
|
|
4762
4770
|
(canRestore ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'restore\',' + Number(entry.version) + ')">' + esc(t("恢复")) + "</button>" : "");
|
|
4763
4771
|
return '<article class="rd-memory-row"><div><strong>' + esc(entry.title || t("项目记忆")) + '</strong><span class="chip">' + esc(entry.status || "") + (entry.expired ? " · " + esc(t("已过期")) : "") + '</span></div><pre>' + esc(entry.content || "") + '</pre><div class="rd-memory-actions">' + actions + "</div></article>";
|
|
@@ -4775,6 +4783,16 @@ window.decideProjectMemory = async function (taskId, id, action, version) {
|
|
|
4775
4783
|
} catch (error) { toast(t("项目记忆操作失败:") + error.message, true); }
|
|
4776
4784
|
};
|
|
4777
4785
|
|
|
4786
|
+
window.feedbackProjectMemory = async function (taskId, id, useful, version) {
|
|
4787
|
+
try {
|
|
4788
|
+
await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory", {
|
|
4789
|
+
method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
4790
|
+
body: JSON.stringify({ id, action: "feedback", useful, expected_version: version }),
|
|
4791
|
+
});
|
|
4792
|
+
await loadProjectMemory(taskId);
|
|
4793
|
+
} catch (error) { toast(t("项目记忆反馈失败:") + error.message, true); }
|
|
4794
|
+
};
|
|
4795
|
+
|
|
4778
4796
|
window.loadRunCheckpoints = async function (requestedRunId) {
|
|
4779
4797
|
const runId = requestedRunId || S.detailRunId || ((S.state || {}).task_latest || {})[S.detailTaskKey]?.id;
|
|
4780
4798
|
if (!runId) return;
|
|
@@ -6166,7 +6184,7 @@ function pbBlock(task, platform) {
|
|
|
6166
6184
|
const run = (S.pubAuto && S.pubAuto.running &&
|
|
6167
6185
|
S.pubAuto.running.platform === platform) ? S.pubAuto.running : null;
|
|
6168
6186
|
if (run && run.status === "running") {
|
|
6169
|
-
btns += '<span class="pb-book">' + esc(t("自动发布中 ") + run.done + "/" + run.total) + "</span>";
|
|
6187
|
+
btns += '<span class="pb-book">' + esc((run.as_draft ? t("存草稿中 ") : t("自动发布中 ")) + run.done + "/" + run.total) + "</span>";
|
|
6170
6188
|
} else if (au.pending > 0) {
|
|
6171
6189
|
// 门禁不再禁用按钮:点了走 pbApiWithQualityOverride 的复审/强制对话,
|
|
6172
6190
|
// 死按钮只会让用户无路可走(2026-10-08 实案:guard false 全灰点不了)
|
|
@@ -6174,6 +6192,12 @@ function pbBlock(task, platform) {
|
|
|
6174
6192
|
' title="' + esc(au.guard_ok ? t("按章号顺序逐章填稿(人工模式每点一次填一章,浏览器里提交后再点发下一章)") : t("护栏提示:") + (au.guard_reason || "")) + '"' +
|
|
6175
6193
|
' onclick="pbPublishAll(\'' + esc(task.id) + "', '" + platform + '\')">' +
|
|
6176
6194
|
t("发布全部待发") + t("(") + au.pending + t(")") + "</button>";
|
|
6195
|
+
// 全部发草稿(2026-10-09):质量闸拦发布不拦草稿——逐章自动填稿点
|
|
6196
|
+
// 「存草稿」连跑到清空,提交发布由用户到平台草稿箱手工完成
|
|
6197
|
+
btns += ' <button class="ghost" ' + (busy || au.draft_ok === false ? "disabled" : "") +
|
|
6198
|
+
' title="' + esc(au.draft_ok !== false ? t("逐章自动填稿并点「存草稿」:内容落平台草稿箱不上线,不受质量门禁拦截,无需逐章人工确认;完成后到平台检查并提交发布") : t("护栏提示:") + (au.draft_reason || "")) + '"' +
|
|
6199
|
+
' onclick="pbDraftAll(\'' + esc(task.id) + "', '" + platform + '\')">' +
|
|
6200
|
+
t("全部存草稿") + t("(") + au.pending + t(")") + "</button>";
|
|
6177
6201
|
}
|
|
6178
6202
|
// 批量选择:待发章号 chips 按卷分组(卷计划来自服务端 items[].volume),
|
|
6179
6203
|
// 「发布所选」人工模式 / 「直发所选」逐章自动提交(护栏照常生效)。
|
|
@@ -6222,10 +6246,23 @@ function pbBlock(task, platform) {
|
|
|
6222
6246
|
(au.guard_ok ? "" : ";" + t("护栏提示:") + (au.guard_reason || ""))) + '"' +
|
|
6223
6247
|
' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',true)">' +
|
|
6224
6248
|
t("直发所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6249
|
+
'<button class="ghost" ' + (busy || !sel.size || au.draft_ok === false ? "disabled" : "") +
|
|
6250
|
+
' title="' + esc(t("所选章节逐章自动存草稿(不上线、不受质量门禁拦截),完成后到平台检查并提交发布") +
|
|
6251
|
+
(au.draft_ok !== false ? "" : ";" + t("护栏提示:") + (au.draft_reason || ""))) + '"' +
|
|
6252
|
+
' onclick="pbDraftSelected(\'' + esc(task.id) + "','" + platform + '\')">' +
|
|
6253
|
+
t("存草稿所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6225
6254
|
"</div></div>";
|
|
6226
6255
|
}
|
|
6227
|
-
if (au.pending > 0 &&
|
|
6228
|
-
|
|
6256
|
+
if (au.pending > 0 && au.draft_ok === false) {
|
|
6257
|
+
// 硬护栏(建书未确认/每日上限/连败退避):存草稿也走不了,红色示警
|
|
6258
|
+
btns += '<div class="pb-err">' + esc(au.draft_reason || au.guard_reason || t("护栏拦截")) + "</div>";
|
|
6259
|
+
} else if (au.pending > 0 && !au.guard_ok && au.quality_blockers && au.quality_blockers.length) {
|
|
6260
|
+
// 纯质量闸拦截:不给死路——提示先存草稿(2026-10-09 全部发草稿通路)
|
|
6261
|
+
btns += '<div class="pb-hint">' + esc(t("质量闸未过:") + au.quality_blockers.join(t(";")) +
|
|
6262
|
+
t("——可先「全部存草稿」落平台(不上线),正式发布前需复审达标或强制放行")) + "</div>";
|
|
6263
|
+
}
|
|
6264
|
+
if (au.drafted > 0) {
|
|
6265
|
+
btns += '<div class="pb-hint">' + esc(t("已存草稿 {0} 章(未发布):到平台章节管理/草稿箱逐章检查后提交发布", au.drafted)) + "</div>";
|
|
6229
6266
|
}
|
|
6230
6267
|
if (run && run.status === "manual_pause") {
|
|
6231
6268
|
// 人工模式一轮只填一章:等用户在浏览器提交后再发起(连发会导航离开
|
|
@@ -6580,6 +6617,38 @@ window.pbPublishAll = async function (taskId, platform) { const au = (((S.pubAu
|
|
|
6580
6617
|
S._pbSig = ""; pbKick();
|
|
6581
6618
|
};
|
|
6582
6619
|
|
|
6620
|
+
/* 全部发草稿(2026-10-09):逐章自动填稿点「存草稿」连跑到清空,不上线、
|
|
6621
|
+
* 不受质量闸拦、无需人工确认;提交发布由用户到平台草稿箱手工完成。 */
|
|
6622
|
+
window.pbDraftAll = async function (taskId, platform) {
|
|
6623
|
+
const au = (((S.pubAuto && S.pubAuto.books) || [])
|
|
6624
|
+
.find((b) => b.platform === platform)) || {};
|
|
6625
|
+
if (!au.pending) return;
|
|
6626
|
+
if (!(await uiConfirm(t("将对全部 {0} 章待发稿逐章自动填稿并点「存草稿」:内容落平台草稿箱不上线,不受质量门禁拦截,无需逐章人工确认(章间隔约 1 分钟防风控)。跑完后请到平台章节管理/草稿箱逐章检查,再手工提交发布。继续?", au.pending),
|
|
6627
|
+
{ ok: t("开始存草稿") }))) return;
|
|
6628
|
+
try {
|
|
6629
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
6630
|
+
{ method: "POST", body: JSON.stringify({ platform, as_draft: true }) });
|
|
6631
|
+
toast(t("存草稿已启动:逐章自动填稿并保存草稿,进度见发布台"));
|
|
6632
|
+
} catch (e) { toast(t("存草稿启动失败:") + e.message, true); }
|
|
6633
|
+
S._pbSig = ""; pbKick();
|
|
6634
|
+
};
|
|
6635
|
+
|
|
6636
|
+
window.pbDraftSelected = async function (taskId, platform) {
|
|
6637
|
+
const key = taskId + ":" + platform;
|
|
6638
|
+
const ent = (S.pbSel || {})[key];
|
|
6639
|
+
const chapters = ent ? [...ent.sel].sort((a, b) => a - b) : [];
|
|
6640
|
+
if (!chapters.length) return;
|
|
6641
|
+
if (!(await uiConfirm(t("将对所选 {0} 章(第 {1} 章—第 {2} 章)逐章自动填稿并点「存草稿」(不上线、不受质量门禁拦截),完成后到平台章节管理/草稿箱检查并提交发布。继续?",
|
|
6642
|
+
chapters.length, chapters[0], chapters[chapters.length - 1]),
|
|
6643
|
+
{ ok: t("开始存草稿") }))) return;
|
|
6644
|
+
try {
|
|
6645
|
+
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
6646
|
+
{ method: "POST", body: JSON.stringify({ platform, chapters, as_draft: true }) });
|
|
6647
|
+
toast(t("存草稿已启动:逐章自动填稿并保存草稿,进度见发布台"));
|
|
6648
|
+
} catch (e) { toast(t("存草稿启动失败:") + e.message, true); }
|
|
6649
|
+
S._pbSig = ""; pbKick();
|
|
6650
|
+
};
|
|
6651
|
+
|
|
6583
6652
|
function pbKick() {
|
|
6584
6653
|
const key = detailSideTaskKey();
|
|
6585
6654
|
if (!key) return;
|
|
@@ -9781,7 +9850,8 @@ function rdModelMenuRender() {
|
|
|
9781
9850
|
t("管理模型…") + "</span></span></button></div>";
|
|
9782
9851
|
} else {
|
|
9783
9852
|
const p = directProviders().find((x) => x.id === rdMenuProv);
|
|
9784
|
-
|
|
9853
|
+
// 只列启用模型:停用的选了也会被运行时拒(builtin_agent.resolve 对账启用名单)
|
|
9854
|
+
const names = p ? (p.models || []).filter((m) => !m.hidden && m.enabled !== false)
|
|
9785
9855
|
.map((m) => m.name).filter(Boolean) : [];
|
|
9786
9856
|
menu.innerHTML =
|
|
9787
9857
|
'<button type="button" class="type-item" data-back="1"><span class="ti-body"><span class="ti-name">‹ ' +
|
|
@@ -10486,6 +10556,14 @@ function apnSyncActive() {
|
|
|
10486
10556
|
/* ---------------------------------------------------------- 智能体目录:模型选择 */
|
|
10487
10557
|
const MAX_ORCH_MODELS = 3; // 与后端 modelhub.MAX_BIND_MODELS、runner 降级链保持一致
|
|
10488
10558
|
|
|
10559
|
+
/* 供应商有无可用密钥:keys 结构以「有启用且未冷却」为准;老数据单 KEY 看
|
|
10560
|
+
* api_key 镜像(与后端 _chain_keys / resolve_binding 的「无可用密钥」同口径) */
|
|
10561
|
+
function provKeyOk(p) {
|
|
10562
|
+
const ks = (p || {}).keys || [];
|
|
10563
|
+
if (ks.length) return ks.some((k) => k.enabled !== false && !k.cooling);
|
|
10564
|
+
return !!(p || {}).api_key;
|
|
10565
|
+
}
|
|
10566
|
+
|
|
10489
10567
|
/* 供应商 → 已启用模型(按优先级),用于分组下拉与多选面板 */
|
|
10490
10568
|
function modelGroups() {
|
|
10491
10569
|
const out = [];
|
|
@@ -10496,7 +10574,7 @@ function modelGroups() {
|
|
|
10496
10574
|
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
|
10497
10575
|
.map((m) => m.name).filter(Boolean);
|
|
10498
10576
|
if (ms.length) out.push({ id: p.id, name: p.name, models: ms,
|
|
10499
|
-
protocol: p.protocol || "", caps: p.wire_caps || {} });
|
|
10577
|
+
protocol: p.protocol || "", caps: p.wire_caps || {}, keyOk: provKeyOk(p) });
|
|
10500
10578
|
}
|
|
10501
10579
|
return out;
|
|
10502
10580
|
}
|
|
@@ -10556,9 +10634,10 @@ function bindAllowedProtocols(kind) {
|
|
|
10556
10634
|
function modelSelectHtml(id, current, kind) {
|
|
10557
10635
|
const cur = fmtModel(current);
|
|
10558
10636
|
const allow = bindAllowedProtocols(kind);
|
|
10637
|
+
// 无可用密钥的供应商不进下拉:默认模型配上去解析时也会被跳过(死配置)
|
|
10559
10638
|
const groups = modelGroups().filter((g) => {
|
|
10560
10639
|
const p = (S.providers || []).find((x) => x.id === g.id);
|
|
10561
|
-
return provAdaptedProto(p, allow, kind);
|
|
10640
|
+
return g.keyOk !== false && provAdaptedProto(p, allow, kind);
|
|
10562
10641
|
});
|
|
10563
10642
|
let opts = '<option value="">' + t("(未设置)") + '</option>';
|
|
10564
10643
|
if (cur && !groups.some((g) => g.models.includes(cur))) {
|
|
@@ -10685,7 +10764,7 @@ function chainProvUsable(p, allow, kind) {
|
|
|
10685
10764
|
if (!p || p.enabled === false) return false;
|
|
10686
10765
|
if (!provAdaptedProto(p, allow, kind)) return false;
|
|
10687
10766
|
const ks = p.keys || [];
|
|
10688
|
-
if (!ks.length) return
|
|
10767
|
+
if (!ks.length) return !!p.api_key; // 老数据单 KEY(api_key 镜像);两者皆无=没配密钥,不推荐
|
|
10689
10768
|
return ks.some((k) => k.enabled !== false && !k.cooling);
|
|
10690
10769
|
}
|
|
10691
10770
|
|
|
@@ -10847,16 +10926,19 @@ function bindPanel(c) {
|
|
|
10847
10926
|
? t("注入该厂商凭据") : t("已适配 · 注入该厂商凭据"))) + '</span></div>' +
|
|
10848
10927
|
g.models.map((m) => {
|
|
10849
10928
|
// 本供应商注入本 CLI 实际走的 wire(与后端 _entry_endpoint 同规则);
|
|
10850
|
-
// 无命中 =
|
|
10929
|
+
// 无命中 = 勾了也会被解析层整条跳过,标死防止白配。
|
|
10930
|
+
// 无可用密钥同理:勾了也是死条目(解析层「无可用密钥」跳过)
|
|
10851
10931
|
const wire = provAdaptedProto(g, allow, c.orch_kind);
|
|
10852
|
-
const dead = !wire;
|
|
10932
|
+
const dead = !wire || g.keyOk === false;
|
|
10853
10933
|
const has = st.chain.some((x) => x.p === g.id && x.m === m);
|
|
10854
10934
|
return '<label class="oitem' + (dead ? " dim" : "") + '"><input type="checkbox" value="' + esc(m) + '" data-p="' + esc(g.id) + '"' +
|
|
10855
10935
|
(has ? " checked" : "") + (dead ? " disabled" : "") +
|
|
10856
10936
|
" onchange=\"bindPick('" + esc(c.id) + "', this, this.checked)\">" + esc(m) +
|
|
10857
10937
|
(modelHasImage(g.id, m)
|
|
10858
10938
|
? ' <span class="tag ok" title="' + esc(t("支持图片输入")) + '">' + t("图") + "</span>" : "") +
|
|
10859
|
-
(dead ? ' <span class="hint warn">' + esc(
|
|
10939
|
+
(dead ? ' <span class="hint warn">' + esc(!wire
|
|
10940
|
+
? t("无可用 wire,解析时跳过")
|
|
10941
|
+
: t("无可用密钥,解析时跳过")) + "</span>" : "") +
|
|
10860
10942
|
"</label>";
|
|
10861
10943
|
}).join("") +
|
|
10862
10944
|
"</div>").join("") + "</div>";
|
|
@@ -11995,7 +12077,8 @@ function renderAuModelPicker(pid, model) {
|
|
|
11995
12077
|
ps.value = provs.some((p) => p.id === keepP) ? keepP : "";
|
|
11996
12078
|
const fillModels = (keepM) => {
|
|
11997
12079
|
const p = provs.find((x) => x.id === ps.value);
|
|
11998
|
-
|
|
12080
|
+
// 只列启用模型:停用的不允许在定时任务里选(运行时同样会拒)
|
|
12081
|
+
const names = p ? (p.models || []).filter((m) => !m.hidden && m.enabled !== false)
|
|
11999
12082
|
.map((m) => m.name).filter(Boolean) : [];
|
|
12000
12083
|
ms.innerHTML = '<option value="">' + t("随厂商推荐") + "</option>" + names.map((name) =>
|
|
12001
12084
|
'<option value="' + esc(name) + '">' + esc(name) + "</option>").join("");
|
|
@@ -13290,8 +13373,77 @@ async function loadSettings() {
|
|
|
13290
13373
|
}
|
|
13291
13374
|
} catch (e) { /* 忽略 */ }
|
|
13292
13375
|
loadSettingsV2(); // 引擎调参卡独立拉取(挂了不影响基础设置)
|
|
13376
|
+
loadBackendProfiles();
|
|
13377
|
+
loadCommandAuth();
|
|
13378
|
+
}
|
|
13379
|
+
|
|
13380
|
+
async function loadBackendProfiles() {
|
|
13381
|
+
const box = $("acp-profiles");
|
|
13382
|
+
if (!box) return;
|
|
13383
|
+
try {
|
|
13384
|
+
const data = await api("/api/backend-profiles");
|
|
13385
|
+
const rows = data.profiles || [];
|
|
13386
|
+
box.innerHTML = rows.length ? rows.map((row) =>
|
|
13387
|
+
'<div class="list-row"><span><b>' + esc(row.label || row.id) + '</b> · ' + esc(row.command || "") +
|
|
13388
|
+
(row.enabled === false ? ' · ' + esc(t("已停用")) : '') + '</span><button class="ghost small" type="button" onclick="deleteAcpProfile(\'' + jsq(row.id) + '\')">' + esc(t("删除")) + '</button></div>'
|
|
13389
|
+
).join("") : '<span class="hint">' + esc(t("暂无 ACP 后端")) + '</span>';
|
|
13390
|
+
} catch (error) {
|
|
13391
|
+
box.innerHTML = '<span class="hint">' + esc(t("ACP 配置加载失败:")) + esc(error.message) + '</span>';
|
|
13392
|
+
}
|
|
13293
13393
|
}
|
|
13294
13394
|
|
|
13395
|
+
window.saveAcpProfile = async function () {
|
|
13396
|
+
const msg = $("acp-msg");
|
|
13397
|
+
try {
|
|
13398
|
+
const argsText = ($("acp-args") || {}).value || "[]";
|
|
13399
|
+
const args = JSON.parse(argsText);
|
|
13400
|
+
if (!Array.isArray(args)) throw new Error(t("参数必须是 JSON 数组"));
|
|
13401
|
+
await api("/api/backend-profiles", { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
13402
|
+
body: JSON.stringify({ label: $("acp-label").value, command: $("acp-command").value,
|
|
13403
|
+
args, workspace_path: $("acp-workspace").value }) });
|
|
13404
|
+
if (msg) msg.textContent = t("ACP 后端已保存");
|
|
13405
|
+
["acp-label", "acp-command", "acp-args", "acp-workspace"].forEach((id) => { const el = $(id); if (el) el.value = id === "acp-args" ? "[]" : ""; });
|
|
13406
|
+
await loadBackendProfiles();
|
|
13407
|
+
await refreshState();
|
|
13408
|
+
} catch (error) { if (msg) msg.textContent = t("ACP 保存失败:") + error.message; }
|
|
13409
|
+
};
|
|
13410
|
+
|
|
13411
|
+
window.deleteAcpProfile = async function (id) {
|
|
13412
|
+
try {
|
|
13413
|
+
await api("/api/backend-profiles/" + encodeURIComponent(id) + "/delete", { method: "POST", headers: authHeaders(), body: "{}" });
|
|
13414
|
+
await loadBackendProfiles();
|
|
13415
|
+
await refreshState();
|
|
13416
|
+
} catch (error) { toast(t("删除 ACP 失败:") + error.message, true); }
|
|
13417
|
+
};
|
|
13418
|
+
|
|
13419
|
+
async function loadCommandAuth() {
|
|
13420
|
+
const pendingBox = $("command-auth-pending");
|
|
13421
|
+
const mode = $("command-auth-mode");
|
|
13422
|
+
if (!pendingBox || !mode) return;
|
|
13423
|
+
try {
|
|
13424
|
+
const data = await api("/api/command-auth");
|
|
13425
|
+
mode.value = data.mode || "legacy";
|
|
13426
|
+
const rows = data.pending || [];
|
|
13427
|
+
pendingBox.innerHTML = rows.length ? rows.map((row) =>
|
|
13428
|
+
'<div class="list-row"><span><code>' + esc(row.command || "") + '</code> · ' + esc(row.source || "") + '</span><button class="ghost small" type="button" onclick="decideCommandAuth(\'' + jsq(row.id) + '\',true)">' + esc(t("批准一次")) + '</button><button class="ghost small" type="button" onclick="decideCommandAuth(\'' + jsq(row.id) + '\',false)">' + esc(t("拒绝")) + '</button></div>'
|
|
13429
|
+
).join("") : '<span class="hint">' + esc(t("暂无待批准命令")) + '</span>';
|
|
13430
|
+
} catch (error) { pendingBox.innerHTML = '<span class="hint">' + esc(t("授权状态加载失败:")) + esc(error.message) + '</span>'; }
|
|
13431
|
+
}
|
|
13432
|
+
|
|
13433
|
+
window.saveCommandAuthMode = async function () {
|
|
13434
|
+
try {
|
|
13435
|
+
await api("/api/command-auth", { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()), body: JSON.stringify({ mode: $("command-auth-mode").value }) });
|
|
13436
|
+
await loadCommandAuth();
|
|
13437
|
+
} catch (error) { const msg = $("command-auth-msg"); if (msg) msg.textContent = t("授权策略保存失败:") + error.message; }
|
|
13438
|
+
};
|
|
13439
|
+
|
|
13440
|
+
window.decideCommandAuth = async function (id, approve) {
|
|
13441
|
+
try {
|
|
13442
|
+
await api("/api/command-auth/requests/" + encodeURIComponent(id), { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()), body: JSON.stringify({ approve, scope: "once" }) });
|
|
13443
|
+
await loadCommandAuth();
|
|
13444
|
+
} catch (error) { const msg = $("command-auth-msg"); if (msg) msg.textContent = t("命令授权操作失败:") + error.message; }
|
|
13445
|
+
};
|
|
13446
|
+
|
|
13295
13447
|
/* ---------------- settings_v2 引擎调参卡(schema 驱动) ----------------
|
|
13296
13448
|
* /api/settings-v2 拉 describe(含字段元数据),按 namespace 渲染控件;
|
|
13297
13449
|
* 保存走 /api/settings-v2/<ns> mutate(expected_revision CAS,409=别处已改,
|
package/app/ui/board.css
CHANGED
|
@@ -207,7 +207,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
207
207
|
}
|
|
208
208
|
.k-lab { font-size: 11px; color: var(--t3); letter-spacing: .8px; }
|
|
209
209
|
.k-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
|
210
|
-
|
|
210
|
+
/* k-num 不许换行:卡高 78px 固定 + overflow hidden,换行必裁字(「2246.7万」案);
|
|
211
|
+
放不下由 fitKpis() 降档,仍放不下才在末档省略号 */
|
|
212
|
+
.k-num { font: 700 27px/1 var(--mono); white-space: nowrap; }
|
|
213
|
+
.k-num.sm { font-size: 21px; }
|
|
214
|
+
.k-num.xs { font-size: 16px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
211
215
|
.k-sub { font: 11px var(--mono); color: var(--t2); white-space: nowrap; }
|
|
212
216
|
.k-run .k-num { color: var(--live-hi); }
|
|
213
217
|
.k-queue .k-num { color: var(--data); }
|
package/app/ui/board.js
CHANGED
|
@@ -42,6 +42,20 @@ function parseTs(s) {
|
|
|
42
42
|
const t = Date.parse(String(s || "").replace(" ", "T"));
|
|
43
43
|
return isNaN(t) ? 0 : t;
|
|
44
44
|
}
|
|
45
|
+
/* 指标条长数值自适应:行宽放不下就逐档降字号(27→21→16px)。
|
|
46
|
+
判据用 k-row 的 scrollWidth:num/sub 都 nowrap 不收缩,超宽才会外溢。 */
|
|
47
|
+
function fitKpis() {
|
|
48
|
+
document.querySelectorAll("#kpis .k-row").forEach((row) => {
|
|
49
|
+
const num = row.querySelector(".k-num");
|
|
50
|
+
if (!num) return;
|
|
51
|
+
num.classList.remove("sm", "xs");
|
|
52
|
+
if (row.scrollWidth <= row.clientWidth + 1) return;
|
|
53
|
+
num.classList.add("sm");
|
|
54
|
+
if (row.scrollWidth <= row.clientWidth + 1) return;
|
|
55
|
+
num.classList.remove("sm");
|
|
56
|
+
num.classList.add("xs");
|
|
57
|
+
});
|
|
58
|
+
}
|
|
45
59
|
function esc(s) {
|
|
46
60
|
return String(s == null ? "" : s).replace(/[&<>"']/g,
|
|
47
61
|
(c) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c]));
|
|
@@ -167,6 +181,7 @@ function render(d) {
|
|
|
167
181
|
|
|
168
182
|
$("#k-tokens").textContent = fmtTokens(usage.tokens);
|
|
169
183
|
$("#k-use-sub").textContent = fmtCost(usage.cost_usd) + " · " + (Number(usage.calls) || 0) + " " + t("次调用");
|
|
184
|
+
fitKpis();
|
|
170
185
|
|
|
171
186
|
// 顶栏态势:整屏只在这里说一次「有没有活」
|
|
172
187
|
const st = $("#hd-state");
|
|
@@ -493,6 +508,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
493
508
|
if (e.key === "T" || e.key === "t") $("#sw-mode").click();
|
|
494
509
|
if (e.key === "S" || e.key === "s") $("#sw-skin").click();
|
|
495
510
|
});
|
|
511
|
+
window.addEventListener("resize", fitKpis);
|
|
496
512
|
|
|
497
513
|
/* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
|
|
498
514
|
setInterval(() => {
|
package/app/ui/i18n.js
CHANGED
|
@@ -824,6 +824,7 @@
|
|
|
824
824
|
"openai 面仅 chat 形态(codex 只讲 responses)": "openai face is chat-only (codex speaks responses only)",
|
|
825
825
|
"openai 面仅 chat 形态(codex 只讲 responses),解析时跳过": "openai face is chat-only (codex speaks responses only) — skipped when resolving",
|
|
826
826
|
"无可用 wire,解析时跳过": "no usable wire — skipped when resolving",
|
|
827
|
+
"无可用密钥,解析时跳过": "no usable key — skipped when resolving",
|
|
827
828
|
"该 CLI 只接受特定 wire 协议的供应商,当前没有匹配项——先到「模型接入」页导入对应协议的网关。": "This CLI only accepts providers on a specific wire protocol and none match — import a compatible gateway on the Models page first.",
|
|
828
829
|
"%1 个供应商 wire 协议不匹配已隐藏(降级只在同协议网关间进行)。": "%1 provider(s) hidden due to wire-protocol mismatch — fallback stays within the same protocol.",
|
|
829
830
|
"已适配(%1 wire)": "adapted (%1 wire)",
|
|
@@ -1407,7 +1408,7 @@
|
|
|
1407
1408
|
"每套皮肤都是一份完整配色,并且各带「日间 / 夜间」两版,明暗切换不会丢皮肤。选中的皮肤立即生效并记在这台设备上(手机与电脑各自独立)。": "Each skin is a complete palette, with day / night variants — switching brightness never loses your skin. The selected skin applies instantly and is remembered on this device (phone and computer are independent).",
|
|
1408
1409
|
"系统自己的经验沉淀:": "The system's own distilled experience:",
|
|
1409
1410
|
"这台设备第一次连接,请输入访问令牌": "First connection from this device — enter the access token",
|
|
1410
|
-
"
|
|
1411
|
+
"(如七猫签约标准,自动注入同类任务的规划与评审提示词) 与": " (e.g. the Qimao signing standard, auto-injected into planning & review prompts for matching tasks)",
|
|
1411
1412
|
"(每次任务结束由编排者复盘本次评审暴露的问题,去重沉淀,下次同类任务自动带上)——越跑越好,无需人工维护。": " (after each task the orchestrator reviews the problems this review exposed, dedupes and distills them, and re-attaches them to the next similar task) — it gets better with every run, no manual upkeep.",
|
|
1412
1413
|
"条": " entries",
|
|
1413
1414
|
"(暂无,跑一次任务后自动生成)": " (none yet — generated after the first run)",
|
|
@@ -2600,6 +2601,30 @@
|
|
|
2600
2601
|
"自动发布失败:": "Auto-publish failed: ",
|
|
2601
2602
|
"已填好一章,请在浏览器确认提交": "One chapter is filled — please review and submit in the browser",
|
|
2602
2603
|
"护栏拦截": "Blocked by guardrail",
|
|
2604
|
+
"全部存草稿": "Save all as drafts",
|
|
2605
|
+
"存草稿所选": "Draft selected",
|
|
2606
|
+
"逐章自动填稿并点「存草稿」:内容落平台草稿箱不上线,不受质量门禁拦截,无需逐章人工确认;完成后到平台检查并提交发布": "Fills each chapter and clicks \"Save draft\": content lands in the platform's draft box without going live, bypasses the quality gate, no per-chapter confirmation; review and submit on the platform when done",
|
|
2607
|
+
"所选章节逐章自动存草稿(不上线、不受质量门禁拦截),完成后到平台检查并提交发布": "Saves the selected chapters as drafts one by one (not live, quality gate not applied); review and submit on the platform when done",
|
|
2608
|
+
"存草稿中 ": "Saving drafts ",
|
|
2609
|
+
"已存草稿 {0} 章(未发布):到平台章节管理/草稿箱逐章检查后提交发布": "{0} chapters saved as drafts (not published): review each in the platform's chapter management/draft box, then submit",
|
|
2610
|
+
"质量闸未过:": "Quality gate not passed: ",
|
|
2611
|
+
"——可先「全部存草稿」落平台(不上线),正式发布前需复审达标或强制放行": " — you can \"Save all as drafts\" first (not live); publishing requires a passing re-review or a forced override",
|
|
2612
|
+
"将对全部 {0} 章待发稿逐章自动填稿并点「存草稿」:内容落平台草稿箱不上线,不受质量门禁拦截,无需逐章人工确认(章间隔约 1 分钟防风控)。跑完后请到平台章节管理/草稿箱逐章检查,再手工提交发布。继续?": "Will fill and save every pending chapter ({0} total) as a draft: content lands in the platform's draft box without going live, quality gate not applied, no per-chapter confirmation (~1 min pacing between chapters). Review each draft on the platform and submit manually when done. Continue?",
|
|
2613
|
+
"将对所选 {0} 章(第 {1} 章—第 {2} 章)逐章自动填稿并点「存草稿」(不上线、不受质量门禁拦截),完成后到平台章节管理/草稿箱检查并提交发布。继续?": "Will fill and save the selected {0} chapters (ch. {1}–ch. {2}) as drafts (not live, quality gate not applied); review and submit on the platform when done. Continue?",
|
|
2614
|
+
"开始存草稿": "Start saving drafts",
|
|
2615
|
+
"存草稿已启动:逐章自动填稿并保存草稿,进度见发布台": "Draft saving started: chapters are filled and saved one by one — watch progress in the publish panel",
|
|
2616
|
+
"存草稿启动失败:": "Failed to start draft saving: ",
|
|
2617
|
+
"发布所选": "Publish selected",
|
|
2618
|
+
"直发所选": "Auto-publish selected",
|
|
2619
|
+
"开始直发": "Start auto-publish",
|
|
2620
|
+
"清空": "Clear",
|
|
2621
|
+
"待发 ": "Pending ",
|
|
2622
|
+
" 章(按卷计划自动分卷)": " chapters (volumes auto-assigned per plan)",
|
|
2623
|
+
"直发已启动,进度见发布台": "Auto-publish started — track progress in the publish panel",
|
|
2624
|
+
"所选章节逐章自动提交(无人值守),护栏每日上限/连败退避照常生效": "Selected chapters submit automatically one by one (unattended); guards (daily cap / failure backoff) stay active",
|
|
2625
|
+
"将对所选 {0} 章(第 {1} 章—第 {2} 章)逐章自动填稿并提交发布,无需人工确认;护栏(每日上限/连续失败暂停)生效。确定直发?": "Will fill and submit the selected {0} chapters (ch. {1}–ch. {2}) one by one automatically, no manual confirmation; guards (daily cap / consecutive-failure pause) stay active. Start auto-publish?",
|
|
2626
|
+
"将按章号顺序逐章填所选 {0} 章(第 {1} 章—第 {2} 章)。人工模式每轮填一章并停在表单页,由你在浏览器里确认提交。分卷会按卷计划自动选/建。继续?": "Will fill the selected {0} chapters (ch. {1}–ch. {2}) in order. Manual mode fills one chapter per round and stops at the form page — you confirm each submission in the browser. Volumes are auto-picked/created per plan. Continue?",
|
|
2627
|
+
"护栏提示:": "Guardrail notice: ",
|
|
2603
2628
|
|
|
2604
2629
|
// —— 报告区 / 状态 / 杂项 ——
|
|
2605
2630
|
"将于 {0} 自动续跑": "Auto-resumes at {0}",
|
package/app/ui/index.html
CHANGED
|
@@ -957,7 +957,7 @@
|
|
|
957
957
|
<div class="panel wide">
|
|
958
958
|
<div class="panel-head"><h2 data-i18n="经验库">经验库</h2>
|
|
959
959
|
<button class="hhelp" data-help-topic="features" title="这是什么?点开看帮助" data-i18n-title="这是什么?点开看帮助" aria-label="帮助" data-i18n-aria="帮助"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button></div>
|
|
960
|
-
<p class="hint"><span data-i18n="系统自己的经验沉淀:">系统自己的经验沉淀:</span><b data-i18n="内置规范包">内置规范包</b><span data-i18n="
|
|
960
|
+
<p class="hint"><span data-i18n="系统自己的经验沉淀:">系统自己的经验沉淀:</span><b data-i18n="内置规范包">内置规范包</b><span data-i18n="(如七猫签约标准,自动注入同类任务的规划与评审提示词) 与">(如七猫签约标准,自动注入同类任务的规划与评审提示词)
|
|
961
961
|
与</span><b data-i18n="运行中自动学到的教训">运行中自动学到的教训</b><span data-i18n="(每次任务结束由编排者复盘本次评审暴露的问题,去重沉淀,下次同类任务自动带上)——越跑越好,无需人工维护。">(每次任务结束由编排者复盘本次评审暴露的问题,去重沉淀,下次同类任务自动带上)——越跑越好,无需人工维护。</span></p>
|
|
962
962
|
<h3 data-i18n="内置规范包" class="sec-title">内置规范包</h3>
|
|
963
963
|
<div id="skill-packs" class="cards"></div>
|
|
@@ -1076,6 +1076,25 @@
|
|
|
1076
1076
|
</div>
|
|
1077
1077
|
<p data-i18n="内置智能体(直连对话/直接执行)会把服务器的工具并入工具循环(mcp__服务器__工具 名)。命令在你本机执行,与「执行命令」工具同一自主权级别;工具清单缓存 5 分钟。留空 = 不启用。" class="hint">内置智能体(直连对话/直接执行)会把服务器的工具并入工具循环(mcp__服务器__工具 名)。命令在你本机执行,与「执行命令」工具同一自主权级别;工具清单缓存 5 分钟。留空 = 不启用。</p>
|
|
1078
1078
|
</div>
|
|
1079
|
+
<h3 class="sec-title">ACP Backend Profiles</h3>
|
|
1080
|
+
<p class="hint">接入遵循 Agent Client Protocol v1 的本地或 SSH stdio Agent。密钥只保存在本机,读取时始终脱敏。</p>
|
|
1081
|
+
<div class="field">
|
|
1082
|
+
<div id="acp-profiles" class="list"></div>
|
|
1083
|
+
<div class="input-row" style="margin-top:6px">
|
|
1084
|
+
<input id="acp-label" type="text" placeholder="名称">
|
|
1085
|
+
<input id="acp-command" type="text" placeholder="启动命令,例如 acp-agent">
|
|
1086
|
+
<input id="acp-args" type="text" placeholder="参数 JSON 数组,例如 [\"--stdio\"]">
|
|
1087
|
+
<input id="acp-workspace" type="text" placeholder="Agent 工作区(可空)">
|
|
1088
|
+
<button class="ghost small" type="button" onclick="saveAcpProfile()">保存 ACP</button>
|
|
1089
|
+
</div>
|
|
1090
|
+
<p id="acp-msg" class="hint"></p>
|
|
1091
|
+
</div>
|
|
1092
|
+
<h3 class="sec-title">命令授权</h3>
|
|
1093
|
+
<div class="field">
|
|
1094
|
+
<div class="input-row"><select id="command-auth-mode"><option value="legacy">legacy:自动执行</option><option value="restricted">restricted:逐条等待批准</option></select><button class="ghost small" type="button" onclick="saveCommandAuthMode()">保存策略</button></div>
|
|
1095
|
+
<div id="command-auth-pending" class="list"></div>
|
|
1096
|
+
<p id="command-auth-msg" class="hint"></p>
|
|
1097
|
+
</div>
|
|
1079
1098
|
<h3 data-i18n="推送通知" class="sec-title">推送通知</h3>
|
|
1080
1099
|
<div class="field"><label data-i18n="群机器人 Webhook(钉钉 / 飞书 / 企业微信,按域名自动适配)">群机器人 Webhook(钉钉 / 飞书 / 企业微信,按域名自动适配)</label>
|
|
1081
1100
|
<div class="input-row">
|
package/package.json
CHANGED