mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 00:43:18 +08:00
Remove MCP wire suppression; keep specialist binding only.
Drop Admin 线侧策略 UI/APIs and gut tool_wire_policy to schema+size prep so MCP tools are gated solely by expert bindings.
This commit is contained in:
parent
4277ad6b14
commit
fe29b43695
10 changed files with 127 additions and 2211 deletions
|
|
@ -4873,17 +4873,6 @@ function pluginsPagerBar(totalHolder, pageRef, onRepaint) {
|
|||
return { wrap, sync };
|
||||
}
|
||||
|
||||
/** 与后端 `load_tool_policies_dict` 一致:0;1–9998;≥9999 → 9999 */
|
||||
function normalizeWirePolicyLevel(raw) {
|
||||
const s = String(raw ?? "").trim();
|
||||
if (s === "") return 0;
|
||||
const n = Number(s);
|
||||
if (!Number.isFinite(n)) return 0;
|
||||
if (n >= 9999) return 9999;
|
||||
if (n <= 0) return 0;
|
||||
return Math.min(Math.trunc(n), 9998);
|
||||
}
|
||||
|
||||
async function renderPlugins() {
|
||||
const p = await apiGet("/admin/api/plugins");
|
||||
let toolPolicy = {
|
||||
|
|
@ -6181,580 +6170,6 @@ async function renderPlugins() {
|
|||
repaintUsageCalls();
|
||||
const usageSummaryPager = pluginsPagerBar(usageSummaryTotalHolder, usageSummaryPageRef, repaintUsageSummary);
|
||||
const usageCallsPager = pluginsPagerBar(usageCallsTotalHolder, usageCallsPageRef, repaintUsageCalls);
|
||||
let toolWire = { tools: [], config: {}, policies: {}, penalty_state: {}, role: "" };
|
||||
try {
|
||||
toolWire = await apiGet("/admin/api/mcp/tool-wire");
|
||||
} catch (_) {}
|
||||
const twc = toolWire.config || {};
|
||||
const wireTools = Array.isArray(toolWire.tools) ? toolWire.tools : [];
|
||||
const wireRoleMode = String(toolWire.role_mode || "restricted");
|
||||
const wireRoleSelect = el("select", { class: "input" }, [
|
||||
el("option", { value: "", text: "global(默认)" }),
|
||||
el("option", { value: "manager", text: "manager(全能者)" }),
|
||||
...availableSpecialists
|
||||
.filter((x) => String(x) !== "manager")
|
||||
.map((sp) => el("option", { value: String(sp), text: String(sp) })),
|
||||
]);
|
||||
wireRoleSelect.value = String(toolWire.role || "");
|
||||
const wireCfgStatus = el("div", { class: "muted", text: "" });
|
||||
const wirePolStatus = el("div", { class: "muted", text: "" });
|
||||
const wireRoleModeSelect = el("select", { class: "input" }, [
|
||||
el("option", { value: "restricted", text: "受限(启用惩罚机制)" }),
|
||||
el("option", { value: "unrestricted", text: "不受限(惩罚无效)" }),
|
||||
el("option", { value: "forbidden", text: "禁止(MCP 全禁)" }),
|
||||
]);
|
||||
wireRoleModeSelect.value = wireRoleMode;
|
||||
const saveWireRoleModeBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "保存 role 模式",
|
||||
onclick: async () => {
|
||||
const role = String(wireRoleSelect.value || "").trim();
|
||||
if (!role) {
|
||||
wireCfgStatus.textContent = "[role-mode] 请选择具体 role(非 global)";
|
||||
return;
|
||||
}
|
||||
const r = await apiPost("/admin/api/mcp/tool-wire/role-mode", {
|
||||
role,
|
||||
mode: String(wireRoleModeSelect.value || "restricted"),
|
||||
});
|
||||
wireCfgStatus.textContent = `[role-mode] ` + JSON.stringify(r);
|
||||
markPrewarmReminder("tool_wire_role_mode_changed");
|
||||
router();
|
||||
},
|
||||
});
|
||||
const applyWireRoleSelectorState = () => {
|
||||
const isGlobal = !String(wireRoleSelect.value || "").trim();
|
||||
saveWireRoleModeBtn.disabled = isGlobal;
|
||||
wireRoleModeSelect.disabled = isGlobal;
|
||||
if (isGlobal) {
|
||||
wireCfgStatus.textContent = "[role-mode] global 不支持 role 模式设置,请选择具体 role。";
|
||||
}
|
||||
};
|
||||
const inpWirePolicy = el("select", { class: "input" }, [
|
||||
el("option", { value: "inherit", text: "inherit(随 URL;DashScope 默认开)" }),
|
||||
el("option", { value: "always", text: "always(不按 URL,始终启用分层)" }),
|
||||
el("option", { value: "never", text: "never(关分层;9999 仍过滤)" }),
|
||||
]);
|
||||
inpWirePolicy.value = String(twc.wire_policy || "inherit");
|
||||
const inpTopN = el("input", { class: "input", type: "number", min: "3", max: "80", value: String(twc.top_n_full ?? 20) });
|
||||
const inpStaleH = el("input", { class: "input", type: "number", step: "0.25", value: String(twc.stale_hours ?? 3) });
|
||||
const inpPenMin = el("input", { class: "input", type: "number", value: String(twc.penalty_minutes ?? 30) });
|
||||
const inpMedS = el("input", { class: "input", type: "number", value: String(twc.medium_rank_start ?? 21) });
|
||||
const inpMedE = el("input", { class: "input", type: "number", value: String(twc.medium_rank_end ?? 50) });
|
||||
const inpMedDesc = el("input", { class: "input", type: "number", value: String(twc.medium_desc_chars ?? 520) });
|
||||
const inpMinCap = el("input", { class: "input", type: "number", value: String(twc.minimal_desc_cap ?? 80) });
|
||||
const inpPenaltyEnabled = el("input", { type: "checkbox" });
|
||||
inpPenaltyEnabled.checked = !Boolean(twc.penalty_disable);
|
||||
const saveWireCfgBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "保存全局参数",
|
||||
onclick: async () => {
|
||||
const r = await apiPost("/admin/api/mcp/tool-wire/config", {
|
||||
wire_policy: inpWirePolicy.value,
|
||||
top_n_full: Number(inpTopN.value),
|
||||
stale_hours: Number(inpStaleH.value),
|
||||
penalty_minutes: Number(inpPenMin.value),
|
||||
medium_rank_start: Number(inpMedS.value),
|
||||
medium_rank_end: Number(inpMedE.value),
|
||||
medium_desc_chars: Number(inpMedDesc.value),
|
||||
minimal_desc_cap: Number(inpMinCap.value),
|
||||
penalty_disable: !Boolean(inpPenaltyEnabled.checked),
|
||||
});
|
||||
wireCfgStatus.textContent = JSON.stringify(r);
|
||||
markPrewarmReminder("tool_wire_config_changed");
|
||||
router();
|
||||
},
|
||||
});
|
||||
const resetPenaltyStateBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "重置惩罚状态",
|
||||
onclick: async () => {
|
||||
if (!window.confirm("确认重置当前 MCP 工具惩罚状态?该操作会立即清空 penalty state。")) return;
|
||||
const qs = wireRoleSelect.value ? ("?role=" + encodeURIComponent(wireRoleSelect.value)) : "";
|
||||
const r = await apiPost("/admin/api/mcp/tool-wire/penalty/reset" + qs, {});
|
||||
wireCfgStatus.textContent = JSON.stringify(r);
|
||||
markPrewarmReminder("tool_wire_penalty_reset");
|
||||
router();
|
||||
},
|
||||
});
|
||||
const draftPolicies = {};
|
||||
wireTools.forEach((t) => {
|
||||
if (t.policy_in_db) draftPolicies[t.wire_name] = normalizeWirePolicyLevel(t.policy_level);
|
||||
else draftPolicies[t.wire_name] = null;
|
||||
});
|
||||
const wireToolBody = el("tbody");
|
||||
const wireToolsPageRef = { value: 1 };
|
||||
const wireToolsTotalHolder = { value: wireTools.length };
|
||||
const wireCheckedSet = new Set();
|
||||
const wireSubMatch = (a, pat) => {
|
||||
const p = String(pat || "").trim();
|
||||
if (!p) return true;
|
||||
return String(a ?? "").toLowerCase().includes(p.toLowerCase());
|
||||
};
|
||||
const wireFOnlyChecked = el("input", { type: "checkbox", title: "仅显示已勾选行" });
|
||||
const wireFServer = el("input", { class: "input", placeholder: "含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFTool = el("input", { class: "input", placeholder: "含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFWire = el("input", { class: "input", placeholder: "含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFExpert = el("input", { class: "input", placeholder: "专家含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFCountMin = el("input", { class: "input", type: "number", placeholder: "≥", style: "width:100%;box-sizing:border-box;" });
|
||||
const wireFCountMax = el("input", { class: "input", type: "number", placeholder: "≤", style: "width:100%;box-sizing:border-box;" });
|
||||
const wireFLastTs = el("input", { class: "input", placeholder: "含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFPenalty = el("input", { class: "input", placeholder: "含", style: "width:100%;min-width:64px;box-sizing:border-box;" });
|
||||
const wireFLevelMin = el("input", { class: "input", type: "number", placeholder: "等级≥", style: "width:100%;box-sizing:border-box;" });
|
||||
const wireFLevelMax = el("input", { class: "input", type: "number", placeholder: "等级≤", style: "width:100%;box-sizing:border-box;" });
|
||||
const wireFilterCountLabel = el("span", { class: "muted", text: "" });
|
||||
const wireRoleModeHint = el("div", { class: "muted", text: "" });
|
||||
const wireRowMatchesFilters = (t) => {
|
||||
if (wireFOnlyChecked.checked && !wireCheckedSet.has(t.wire_name)) return false;
|
||||
if (!wireSubMatch(t.server_id, wireFServer.value)) return false;
|
||||
if (!wireSubMatch(t.mcp_tool_name, wireFTool.value)) return false;
|
||||
if (!wireSubMatch(t.wire_name, wireFWire.value)) return false;
|
||||
const ex = specialistsBoundToServer(t.server_id).join(", ");
|
||||
if (!wireSubMatch(ex, wireFExpert.value)) return false;
|
||||
const cnt = Number(t.count || 0);
|
||||
if (String(wireFCountMin.value).trim() && cnt < Number(wireFCountMin.value)) return false;
|
||||
if (String(wireFCountMax.value).trim() && cnt > Number(wireFCountMax.value)) return false;
|
||||
const ph = (t.penalty && t.penalty.unblock_hint) || "";
|
||||
if (!wireSubMatch(ph, wireFPenalty.value)) return false;
|
||||
if (!wireSubMatch(t.last_ts || "", wireFLastTs.value)) return false;
|
||||
const lvRaw = draftPolicies[t.wire_name];
|
||||
const lvNum = lvRaw === null || lvRaw === undefined ? null : Number(lvRaw);
|
||||
const minS = String(wireFLevelMin.value).trim();
|
||||
if (minS) {
|
||||
const m = Number(minS);
|
||||
if (Number.isFinite(m)) {
|
||||
if (lvNum === null || lvNum === undefined) {
|
||||
if (m > 0) return false;
|
||||
} else if (lvNum < m) return false;
|
||||
}
|
||||
}
|
||||
const maxS = String(wireFLevelMax.value).trim();
|
||||
if (maxS && lvNum !== null && lvNum !== undefined) {
|
||||
const m = Number(maxS);
|
||||
if (Number.isFinite(m) && lvNum > m) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const roleModeBadge = (modeRaw) => {
|
||||
const mode = String(modeRaw || "restricted");
|
||||
if (mode === "unrestricted") {
|
||||
return el("span", { class: "badge badge--ok", text: "unrestricted" });
|
||||
}
|
||||
if (mode === "forbidden") {
|
||||
return el("span", { class: "badge badge--bad", text: "forbidden" });
|
||||
}
|
||||
return el("span", { class: "badge badge--mode-restricted", text: "restricted" });
|
||||
};
|
||||
const getWireToolsFiltered = () => wireTools.filter((x) => wireRowMatchesFilters(x));
|
||||
const paintWireToolRows = () => {
|
||||
wireToolBody.innerHTML = "";
|
||||
if (!wireTools.length) {
|
||||
wireToolBody.appendChild(el("tr", {}, [el("td", { text: "暂无已缓存工具(对各 MCP 点 Sync Tools)", colspan: "9" })]));
|
||||
return;
|
||||
}
|
||||
const list = getWireToolsFiltered();
|
||||
if (!list.length) {
|
||||
wireToolBody.appendChild(el("tr", {}, [el("td", { text: "无匹配行(请调整筛选)", colspan: "9" })]));
|
||||
return;
|
||||
}
|
||||
const start = (wireToolsPageRef.value - 1) * PLUGINS_PAGE_SIZE;
|
||||
list.slice(start, start + PLUGINS_PAGE_SIZE).forEach((t) => {
|
||||
const rowCb = el("input", { type: "checkbox" });
|
||||
rowCb.checked = wireCheckedSet.has(t.wire_name);
|
||||
rowCb.addEventListener("change", () => {
|
||||
if (rowCb.checked) wireCheckedSet.add(t.wire_name);
|
||||
else wireCheckedSet.delete(t.wire_name);
|
||||
});
|
||||
const lv0 = draftPolicies[t.wire_name];
|
||||
const lvlSel = el(
|
||||
"select",
|
||||
{
|
||||
class: "input",
|
||||
title: "按 role:默认(继承全局惩罚)/ 不惩罚 / 永禁",
|
||||
style: "width:140px;max-width:100%;",
|
||||
"data-wire-level": "1",
|
||||
},
|
||||
[
|
||||
el("option", { value: "", text: "默认(继承)" }),
|
||||
el("option", { value: "0", text: "不惩罚(0)" }),
|
||||
el("option", { value: "9999", text: "永禁(9999)" }),
|
||||
],
|
||||
);
|
||||
lvlSel.value = lv0 === null || lv0 === undefined ? "" : String(normalizeWirePolicyLevel(lv0));
|
||||
const syncLevelFromSelect = () => {
|
||||
const v = String(lvlSel.value || "").trim();
|
||||
if (!v) {
|
||||
draftPolicies[t.wire_name] = null;
|
||||
lvlSel.value = "";
|
||||
return;
|
||||
}
|
||||
const n = normalizeWirePolicyLevel(v);
|
||||
draftPolicies[t.wire_name] = n;
|
||||
lvlSel.value = String(n);
|
||||
};
|
||||
lvlSel.addEventListener("change", syncLevelFromSelect);
|
||||
const ph = (t.penalty && t.penalty.unblock_hint) || "-";
|
||||
const exCell = specialistsBoundToServer(t.server_id).join(", ") || "—";
|
||||
wireToolBody.appendChild(
|
||||
el(
|
||||
"tr",
|
||||
{ "data-wire-name": t.wire_name },
|
||||
[
|
||||
el("td", {}, [rowCb]),
|
||||
tdCell(t.server_id, 20),
|
||||
tdCell(t.mcp_tool_name, 22),
|
||||
tdCell(t.wire_name, 32),
|
||||
tdCell(exCell, 20),
|
||||
tdCell(String(t.count || 0), 8),
|
||||
tdCell(String(t.last_ts || "-"), 22),
|
||||
el("td", {}, [roleModeBadge(wireRoleModeSelect.value || "restricted")]),
|
||||
el("td", {
|
||||
text: ph,
|
||||
title: ph,
|
||||
style: "max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
|
||||
}),
|
||||
el("td", {}, [lvlSel]),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
const applyWireRoleModeUiState = () => {
|
||||
const mode = String(wireRoleModeSelect.value || "restricted");
|
||||
const disabled = mode !== "restricted";
|
||||
if (mode === "unrestricted") {
|
||||
wireRoleModeHint.textContent = "当前 role 为不受限:单工具惩罚策略不生效。";
|
||||
} else if (mode === "forbidden") {
|
||||
wireRoleModeHint.textContent = "当前 role 为禁止:MCP 全禁,单工具惩罚策略不生效。";
|
||||
} else {
|
||||
wireRoleModeHint.textContent = "";
|
||||
}
|
||||
const staticControls = [
|
||||
wireFOnlyChecked,
|
||||
wireFServer,
|
||||
wireFTool,
|
||||
wireFWire,
|
||||
wireFExpert,
|
||||
wireFCountMin,
|
||||
wireFCountMax,
|
||||
wireFLastTs,
|
||||
wireFPenalty,
|
||||
wireFLevelMin,
|
||||
wireFLevelMax,
|
||||
clearWireFiltersBtn,
|
||||
bulkLvlInput,
|
||||
applyBulkWireBtn,
|
||||
saveWirePolBtn,
|
||||
];
|
||||
staticControls.forEach((node) => {
|
||||
if (node) node.disabled = disabled;
|
||||
});
|
||||
Array.from(wireToolBody.querySelectorAll("input,select,button")).forEach((el0) => {
|
||||
el0.disabled = disabled;
|
||||
});
|
||||
};
|
||||
const wireToolsPager = pluginsPagerBar(wireToolsTotalHolder, wireToolsPageRef, paintWireToolRows);
|
||||
const refreshWireToolsFiltered = () => {
|
||||
const list = getWireToolsFiltered();
|
||||
wireToolsTotalHolder.value = list.length;
|
||||
wireFilterCountLabel.textContent = `筛选 ${list.length} / 共 ${wireTools.length} 条`;
|
||||
const maxPage = Math.max(1, Math.ceil(list.length / PLUGINS_PAGE_SIZE) || 1);
|
||||
if (wireToolsPageRef.value > maxPage) wireToolsPageRef.value = maxPage;
|
||||
if (wireToolsPageRef.value < 1) wireToolsPageRef.value = 1;
|
||||
paintWireToolRows();
|
||||
wireToolsPager.sync();
|
||||
applyWireRoleModeUiState();
|
||||
};
|
||||
const onWireFilterChange = () => {
|
||||
wireToolsPageRef.value = 1;
|
||||
refreshWireToolsFiltered();
|
||||
};
|
||||
[
|
||||
wireFServer,
|
||||
wireFTool,
|
||||
wireFWire,
|
||||
wireFExpert,
|
||||
wireFCountMin,
|
||||
wireFCountMax,
|
||||
wireFLastTs,
|
||||
wireFPenalty,
|
||||
wireFLevelMin,
|
||||
wireFLevelMax,
|
||||
].forEach((inp) => inp.addEventListener("input", onWireFilterChange));
|
||||
wireFOnlyChecked.addEventListener("change", onWireFilterChange);
|
||||
wireRoleModeSelect.addEventListener("change", applyWireRoleModeUiState);
|
||||
wireRoleSelect.addEventListener("change", applyWireRoleSelectorState);
|
||||
const bulkLvlInput = el(
|
||||
"select",
|
||||
{ class: "input", title: "批量写入", style: "width:140px;" },
|
||||
[
|
||||
el("option", { value: "0", text: "不惩罚(0)" }),
|
||||
el("option", { value: "9999", text: "永禁(9999)" }),
|
||||
el("option", { value: "", text: "清空(继承)" }),
|
||||
],
|
||||
);
|
||||
bulkLvlInput.value = "0";
|
||||
const clearWireFiltersBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "清除筛选",
|
||||
onclick: () => {
|
||||
wireFServer.value = "";
|
||||
wireFTool.value = "";
|
||||
wireFWire.value = "";
|
||||
wireFExpert.value = "";
|
||||
wireFCountMin.value = "";
|
||||
wireFCountMax.value = "";
|
||||
wireFLastTs.value = "";
|
||||
wireFPenalty.value = "";
|
||||
wireFLevelMin.value = "";
|
||||
wireFLevelMax.value = "";
|
||||
wireFOnlyChecked.checked = false;
|
||||
onWireFilterChange();
|
||||
},
|
||||
});
|
||||
const applyBulkWireBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "批量应用到选中",
|
||||
onclick: async () => {
|
||||
const bulkRaw = String(bulkLvlInput.value ?? "").trim();
|
||||
const lv = bulkRaw === "" ? null : normalizeWirePolicyLevel(bulkRaw);
|
||||
const selectedWireNames = [];
|
||||
Array.from(wireToolBody.querySelectorAll("tr")).forEach((tr) => {
|
||||
const cb = tr.querySelector("input[type=checkbox]");
|
||||
if (!cb || !cb.checked) return;
|
||||
const wn = tr.getAttribute("data-wire-name");
|
||||
const sel = tr.querySelector("[data-wire-level]");
|
||||
if (wn && sel) {
|
||||
selectedWireNames.push(String(wn));
|
||||
draftPolicies[wn] = lv;
|
||||
sel.value = lv === null ? "" : String(lv);
|
||||
}
|
||||
});
|
||||
if (selectedWireNames.length) {
|
||||
try {
|
||||
if (lv === null) {
|
||||
await apiPost("/admin/api/mcp/tool-wire/policies", {
|
||||
role: String(wireRoleSelect.value || ""),
|
||||
policies: {},
|
||||
clears: selectedWireNames,
|
||||
});
|
||||
} else {
|
||||
await apiPost("/admin/api/mcp/tool-wire/policies/batch", {
|
||||
role: String(wireRoleSelect.value || ""),
|
||||
level: Number(lv),
|
||||
wire_names: selectedWireNames,
|
||||
});
|
||||
}
|
||||
markPrewarmReminder("tool_wire_policies_batch_changed");
|
||||
} catch (err) {
|
||||
wirePolStatus.textContent = `[批量] 后端批量写入失败: ${String((err && err.message) || err)}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
wirePolStatus.textContent = "[批量] 已写入后端并更新本地视图";
|
||||
},
|
||||
});
|
||||
const saveWirePolBtn = el("button", {
|
||||
class: "btn btn--primary",
|
||||
text: "保存工具策略",
|
||||
onclick: async () => {
|
||||
Array.from(wireToolBody.querySelectorAll("tr")).forEach((tr) => {
|
||||
const wn = tr.getAttribute("data-wire-name");
|
||||
const sel = tr.querySelector("[data-wire-level]");
|
||||
if (wn && sel) {
|
||||
const raw = String(sel.value ?? "").trim();
|
||||
draftPolicies[wn] = raw === "" ? null : normalizeWirePolicyLevel(raw);
|
||||
}
|
||||
});
|
||||
const pol = {};
|
||||
const clears = [];
|
||||
wireTools.forEach((t) => {
|
||||
const wn = t.wire_name;
|
||||
const v = draftPolicies[wn];
|
||||
if (v === null || v === undefined) clears.push(wn);
|
||||
else pol[wn] = v;
|
||||
});
|
||||
const r = await apiPost("/admin/api/mcp/tool-wire/policies", {
|
||||
role: String(wireRoleSelect.value || ""),
|
||||
policies: pol,
|
||||
clears,
|
||||
});
|
||||
wirePolStatus.textContent = JSON.stringify(r);
|
||||
markPrewarmReminder("tool_wire_policies_changed");
|
||||
router();
|
||||
},
|
||||
});
|
||||
refreshWireToolsFiltered();
|
||||
applyWireRoleSelectorState();
|
||||
applyWireRoleModeUiState();
|
||||
const foldToolPolicy = pluginsFold(`【1】工具策略与已注册插件(${pluginCatalog.length})`, [
|
||||
el("div", { class: "muted", text: "Tool policy(并发 / 轮次 / MCP·插件开关)与 Python 工具插件表" }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "Turn max tool workers (1-32)" }),
|
||||
turnMaxWorkersInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "Turn max tool rounds (1-300)" }),
|
||||
turnMaxRoundsInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "Turn max context messages (10-400)" }),
|
||||
turnMaxCtxInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "SSE queue maxsize (200-50000)" }),
|
||||
sseQueueMaxsizeInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "Tool log max chars (20000-2000000)" }),
|
||||
toolLogMaxCharsInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [enableMcpToolsCb, document.createTextNode(" Enable MCP tools")])]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [enablePluginToolsCb, document.createTextNode(" Enable plugin tools")])]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [enableRunCommandCb, document.createTextNode(" Enable run_command (high-risk)")])]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [toolContextTruncateCb, document.createTextNode(" Compress tool result in agent context (50 chars + hint)")])]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [chatShowTtftDebugCb, document.createTextNode(" Show TTFT debug timings in chat status")])]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "Tool message max chars to LLM (0=unlimited, 4096-500000 recommended)" }),
|
||||
toolLlmMessageMaxCharsInput,
|
||||
]),
|
||||
el("div", { class: "muted", text: "Set 0 to disable truncation. If some gateways return 400 for oversized tool messages, set back to 24000." }),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "MCP filesystem extra roots (| separated)" }),
|
||||
mcpFilesystemExtraRootsInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "MCP env allowlist (comma separated)" }),
|
||||
mcpEnvAllowlistInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "oclaw retryable error codes (comma separated)" }),
|
||||
oclawRetryableErrorCodesInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [el("label", { class: "kv" }, [oclawRetryCodesStrictModeCb, document.createTextNode(" Strict mode: reject unknown retry codes")])]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "WeCom longconn workers (1-8)" }),
|
||||
wecomLongconnWorkersInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "WeCom inbound queue maxsize (20-5000)" }),
|
||||
wecomLongconnInboundQueueInput,
|
||||
]),
|
||||
el("div", { class: "row" }, [saveToolPolicyBtn]),
|
||||
toolPolicyStatus,
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table" }, [
|
||||
el("thead", {}, [el("tr", {}, [el("th", { text: t("table.name") }), el("th", { text: t("table.version") }), el("th", { text: t("table.entryPoint") }), el("th", { text: t("table.enabled") })])]),
|
||||
pluginTbody,
|
||||
]),
|
||||
]),
|
||||
pluginPager.wrap,
|
||||
]);
|
||||
const foldMcpMarket = pluginsFold("【2】MCP 市场 / 依赖 / Trending / 检索结果", [
|
||||
el("div", { class: "muted", text: failureText ? `Failure summary: ${failureText}` : "Failure summary: -" }),
|
||||
el("div", { class: "muted", text: "本地依赖检查" }),
|
||||
depWrap,
|
||||
el("div", { class: "row" }, [marketQ, marketBtn, marketRefreshBtn]),
|
||||
el("div", { class: "muted", text: "Trending" }),
|
||||
trendingWrap,
|
||||
el("div", { class: "muted", text: "Market search" }),
|
||||
marketWrap,
|
||||
]);
|
||||
const foldMcpInstall = pluginsFold("【3】MCP 安装(表单 / JSON / 运维)", [
|
||||
el("div", { class: "row" }, [sourceType, sourceRef, version]),
|
||||
el("div", { class: "row" }, [entryCmd, entryArgs, installBtn]),
|
||||
el("div", { class: "muted", text: "CLI direct install (paste one command)" }),
|
||||
el("div", { class: "row" }, [cliInstallInput, cliInstallBtn]),
|
||||
el("div", { class: "muted", text: "常用命令行安装示例(可先本机验证,再填上方表单)" }),
|
||||
el("pre", {
|
||||
class: "pre",
|
||||
text:
|
||||
`# npm 包(本地安装)
|
||||
npm install mcp-fetch-server
|
||||
|
||||
# 直接运行(推荐)
|
||||
npx -y mcp-fetch-server
|
||||
|
||||
# 全局安装后运行
|
||||
npm install -g mcp-fetch-server
|
||||
mcp-fetch-server
|
||||
|
||||
# Python 包示例
|
||||
pip install mcp-server-time
|
||||
python -m mcp_server_time
|
||||
|
||||
# Python(Git URL / VCS)示例:必须显式指定 entry module
|
||||
pip install git+https://github.com/philschmid/code-sandbox-mcp.git && python -m code_sandbox_mcp`,
|
||||
}),
|
||||
el("div", { class: "muted", text: "JSON install (single object or array)" }),
|
||||
jsonInstallInput,
|
||||
el("div", { class: "row" }, [jsonInstallBtn]),
|
||||
el("div", { class: "row", style: "flex-wrap:wrap;align-items:center;gap:8px;" }, [
|
||||
checkUpdatesBtn,
|
||||
updateOutdatedBtn,
|
||||
checkAllBtn,
|
||||
e2eCheckBtn,
|
||||
updateAllBtn,
|
||||
repairWeakBtn,
|
||||
repairWeakScopeLabel,
|
||||
]),
|
||||
installStatus,
|
||||
preflightFixWrap,
|
||||
]);
|
||||
const mcpExportJsonBtn = el("button", {
|
||||
class: "btn",
|
||||
text: "Export JSON (download)",
|
||||
title: "Download uninstall/reinstallable snapshot (same shape as “Install from JSON”)",
|
||||
onclick: async () => {
|
||||
let r;
|
||||
try {
|
||||
r = await apiGet("/admin/api/mcp/export");
|
||||
} catch (err) {
|
||||
installStatus.textContent = "[export] " + String((err && err.message) || err);
|
||||
return;
|
||||
}
|
||||
if (!r || r.ok !== true || !r.document) {
|
||||
installStatus.textContent = "[export] failed: " + JSON.stringify(r);
|
||||
return;
|
||||
}
|
||||
const text = JSON.stringify(r.document, null, 2) + "\n";
|
||||
const blob = new Blob([text], { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "mcp_registry_migrated.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
installStatus.textContent =
|
||||
"[export] downloaded mcp_registry_migrated.json" + (r.local_path ? " ; on server: " + r.local_path : "");
|
||||
},
|
||||
});
|
||||
const foldMcpInstalled = pluginsFold(`【4】已安装 MCP 服务(${mcpServerList.length})`, [
|
||||
el("div", { class: "row", style: "align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:8px;" }, [
|
||||
mcpExportJsonBtn,
|
||||
el("div", {
|
||||
class: "muted",
|
||||
text: "新安装/重装/卸载(删记录)成功后自动写入: src/_local/mcp_registry_migrated.json,便于换机迁移。",
|
||||
}),
|
||||
]),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table table--compact" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
el("th", { text: "server_id" }),
|
||||
el("th", { text: "source" }),
|
||||
el("th", { text: "ref" }),
|
||||
el("th", { text: "version" }),
|
||||
el("th", { text: "entry" }),
|
||||
el("th", { text: "tools" }),
|
||||
el("th", { text: "update" }),
|
||||
el("th", { text: "enabled" }),
|
||||
el("th", { text: "health" }),
|
||||
el("th", { text: "actions" }),
|
||||
])]),
|
||||
mcpInstalledTbody,
|
||||
]),
|
||||
]),
|
||||
mcpInstalledPager.wrap,
|
||||
]);
|
||||
const foldMcpUsage = pluginsFold(
|
||||
`【5】MCP 用量(summary ${usageSummaryList.length} / calls ${usageCallsList.length})`,
|
||||
[
|
||||
|
|
@ -6789,109 +6204,7 @@ pip install git+https://github.com/philschmid/code-sandbox-mcp.git && python -m
|
|||
usageCallsPager.wrap,
|
||||
],
|
||||
);
|
||||
const foldWireGlobal = pluginsFold("【6】线侧策略 — 全局参数", [
|
||||
el("div", {
|
||||
class: "muted",
|
||||
text: "按 mcp__server__tool;wire_policy=always 不依赖 base_url。",
|
||||
}),
|
||||
el("div", { class: "row", style: "flex-wrap:wrap;gap:8px;align-items:center;" }, [
|
||||
el("label", { text: "wire_policy" }),
|
||||
inpWirePolicy,
|
||||
el("label", { text: "Top N 全量" }),
|
||||
inpTopN,
|
||||
el("label", { text: "全局闲置(h)" }),
|
||||
inpStaleH,
|
||||
el("label", { text: "罚时长(min)" }),
|
||||
inpPenMin,
|
||||
]),
|
||||
el("div", { class: "row", style: "flex-wrap:wrap;gap:8px;align-items:center;" }, [
|
||||
el("label", { text: "medium rank" }),
|
||||
inpMedS,
|
||||
inpMedE,
|
||||
el("label", { text: "medium 描述上限" }),
|
||||
inpMedDesc,
|
||||
el("label", { text: "minimal 描述" }),
|
||||
inpMinCap,
|
||||
el("label", {}, [inpPenaltyEnabled, el("span", { text: "启用惩罚机制", style: "margin-left:6px;" })]),
|
||||
]),
|
||||
el("div", { class: "row" }, [
|
||||
el("label", { text: "role" }),
|
||||
wireRoleSelect,
|
||||
wireRoleModeSelect,
|
||||
saveWireRoleModeBtn,
|
||||
saveWireCfgBtn,
|
||||
resetPenaltyStateBtn,
|
||||
wireCfgStatus,
|
||||
]),
|
||||
]);
|
||||
const wireLevelHint = el("div", {
|
||||
class: "muted",
|
||||
style: "font-size:12px;line-height:1.45;margin-bottom:6px;",
|
||||
text:
|
||||
"留空=未配置(运行时走全局闲置惩罚与线侧分层/压缩)。0=该 role 下此工具不参与闲置惩罚。任意整数 1–9998:闲置与罚均为 N×10 分钟;≥9999 视为 9999 永久不上送。新安装 MCP 在 Sync Tools 后出现新行,默认留空即自动走全局,直至你在本页保存。",
|
||||
});
|
||||
const foldWireTools = pluginsFold(`【7】线侧策略 — 已安装工具(${wireTools.length})`, [
|
||||
wireLevelHint,
|
||||
wireRoleModeHint,
|
||||
el("div", { class: "row", style: "flex-wrap:wrap;gap:8px;align-items:center;" }, [
|
||||
wireFilterCountLabel,
|
||||
clearWireFiltersBtn,
|
||||
el("span", { class: "muted", text: "批量等级" }),
|
||||
bulkLvlInput,
|
||||
applyBulkWireBtn,
|
||||
saveWirePolBtn,
|
||||
wirePolStatus,
|
||||
]),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table table--compact" }, [
|
||||
el("thead", {}, [
|
||||
el("tr", {}, [
|
||||
el("th", { text: "选" }),
|
||||
el("th", { text: "server" }),
|
||||
el("th", { text: "tool" }),
|
||||
el("th", { text: "wire_name" }),
|
||||
el("th", { text: "专家(绑定推导)" }),
|
||||
el("th", { text: "count" }),
|
||||
el("th", { text: "last_ts" }),
|
||||
el("th", { text: "effective_mode" }),
|
||||
el("th", { text: "惩罚/解封" }),
|
||||
el("th", { text: "策略" }),
|
||||
]),
|
||||
el("tr", {}, [
|
||||
el("th", {}, [wireFOnlyChecked]),
|
||||
el("th", {}, [wireFServer]),
|
||||
el("th", {}, [wireFTool]),
|
||||
el("th", {}, [wireFWire]),
|
||||
el("th", {}, [wireFExpert]),
|
||||
el("th", {}, [
|
||||
el("div", { style: "display:flex;flex-direction:column;gap:4px;" }, [wireFCountMin, wireFCountMax]),
|
||||
]),
|
||||
el("th", {}, [wireFLastTs]),
|
||||
el("th", { text: "-" }),
|
||||
el("th", {}, [wireFPenalty]),
|
||||
el("th", {}, [
|
||||
el("div", { style: "display:flex;flex-direction:column;gap:4px;" }, [wireFLevelMin, wireFLevelMax]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
wireToolBody,
|
||||
]),
|
||||
]),
|
||||
wireToolsPager.wrap,
|
||||
]);
|
||||
wireRoleSelect.addEventListener("change", async () => {
|
||||
try {
|
||||
const qs = wireRoleSelect.value ? ("?role=" + encodeURIComponent(wireRoleSelect.value)) : "";
|
||||
toolWire = await apiGet("/admin/api/mcp/tool-wire" + qs);
|
||||
wireCfgStatus.textContent = `[role] switched to ${String(toolWire.role || "global")}`;
|
||||
wireRoleModeSelect.value = String(toolWire.role_mode || "restricted");
|
||||
// reload current page to rebuild wireTools + drafts cleanly
|
||||
router();
|
||||
} catch (err) {
|
||||
wireCfgStatus.textContent = `[role] load failed: ${String((err && err.message) || err)}`;
|
||||
}
|
||||
});
|
||||
const foldExpertBindingDash = pluginsFold("【8】专家 MCP 绑定看板(自动)", [
|
||||
const foldExpertBindingDash = pluginsFold("【6】专家 MCP 绑定看板(自动)", [
|
||||
el("div", {
|
||||
class: "muted",
|
||||
text: "按当前绑定草稿与已安装 MCP 的 tools 列表汇总;专家列表来自 SPECIALISTS 与绑定 mapping 键,随扩展自动增减。",
|
||||
|
|
@ -6907,7 +6220,7 @@ pip install git+https://github.com/philschmid/code-sandbox-mcp.git && python -m
|
|||
]),
|
||||
]),
|
||||
]);
|
||||
const foldMcpBinding = pluginsFold("【9】MCP 专家绑定(编辑)", [
|
||||
const foldMcpBinding = pluginsFold("【7】MCP 专家绑定(编辑)", [
|
||||
el("div", { class: "muted", text: "Bind MCP servers to specialists (many-to-many)." }),
|
||||
el("div", { class: "row" }, [el("label", { text: "Specialist" }), specialistSelect, selectAllBindingBtn, clearBindingBtn, saveBindingBtn]),
|
||||
bindingListWrap,
|
||||
|
|
@ -6932,8 +6245,6 @@ pip install git+https://github.com/philschmid/code-sandbox-mcp.git && python -m
|
|||
el("div", { id: "plugins-install" }, [foldMcpInstall]),
|
||||
el("div", { id: "plugins-instances" }, [foldMcpInstalled]),
|
||||
foldMcpUsage,
|
||||
foldWireGlobal,
|
||||
foldWireTools,
|
||||
foldExpertBindingDash,
|
||||
el("div", { id: "plugins-binding" }, [foldMcpBinding]),
|
||||
]),
|
||||
|
|
@ -9078,7 +8389,7 @@ async function renderSkills() {
|
|||
});
|
||||
const llmToolsBox = el("details", { style: "margin:10px 0 14px 0;" }, [
|
||||
el("summary", { text: "LLM tools preview (after wire policy)", style: "cursor:pointer;user-select:none;" }),
|
||||
el("div", { class: "muted", style: "margin:8px 0;line-height:1.5;", text: "Preview the final tools injected to the model for a role (internal + MCP + role_mode + permanent bans + wire policy tiers/penalty)." }),
|
||||
el("div", { class: "muted", style: "margin:8px 0;line-height:1.5;", text: "Preview the final tools injected to the model for a role (internal + MCP + role_mode + MCP specialist binding)." }),
|
||||
llmToolsStatus,
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center;" }, [
|
||||
el("label", { text: "Role" }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue