Netx tools: load from netx remote /mcp, add per-tool toggles

- Drop the TypeScript netx tool implementations and the hard-coded
  TOOLS_BY_GROUP list; register netx__* tools from the server's
  tools/list (group from _meta.netx.group) and forward tools/call.
- Thin Streamable HTTP client on standard fetch (fetchImpl injectable);
  cached catalog when netx is unreachable, refresh to reconnect.
- disabledTools (deny list; new server tools follow their group) and
  readOnlyOnly (only readOnlyHint=true tools), re-registered live.
- Settings card: per-group expandable tool rows with switches and a
  write marker; skills append a disabled-tools note.
- Toggles only control model visibility; netx enforces security.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 22:08:22 +08:00
parent 2def53e7c7
commit 2ef3a43c63
38 changed files with 5049 additions and 17029 deletions

View file

@ -682,7 +682,10 @@ var EMPTY2 = {
groups: [],
plane: "",
tokenConfigured: null,
apiUrl: ""
apiUrl: "",
mcpPhase: "idle",
mcpError: "",
mcpFromCache: false
};
function asToolsMountView(value) {
if (value === null || typeof value !== "object" || Array.isArray(value))
@ -741,7 +744,29 @@ function asToolsMountView(value) {
groups = [...new Set(mounts.flatMap((m) => m.groups))];
}
const toolsEnabled = !(connection && typeof connection === "object" && connection.toolsEnabled === false);
return { count, version, groups, plane, error, tokenConfigured, apiUrl, mounts, toolsEnabled };
let mcpPhase = "idle";
let mcpError = "";
let mcpFromCache = false;
if (row.mcp !== null && typeof row.mcp === "object" && !Array.isArray(row.mcp)) {
const mcp = row.mcp;
mcpPhase = typeof mcp.phase === "string" ? mcp.phase : "idle";
mcpError = typeof mcp.error === "string" ? mcp.error : "";
mcpFromCache = mcp.fromCache === true;
}
return {
count,
version,
groups,
plane,
error,
tokenConfigured,
apiUrl,
mounts,
toolsEnabled,
mcpPhase,
mcpError,
mcpFromCache
};
}
async function fetchToolsStatus(call, signal) {
const result = await call(NETXOPS_RPC_CHANNEL3, TOOLS_STATUS_ENDPOINT, {}, signal);
@ -753,8 +778,10 @@ async function fetchToolsStatus(call, signal) {
function toolsMountTone(view) {
if (!view)
return "neutral";
if (view.error)
if (view.error || view.mcpPhase === "error")
return "err";
if (view.mcpPhase === "offline" || view.mcpFromCache)
return "warn";
if (!view.toolsEnabled)
return "neutral";
if (view.count > 0)
@ -810,6 +837,12 @@ var CSS = `
.dsh-nx-groupBlock+.dsh-nx-groupBlock{border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)}
.dsh-nx-groupTitle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-groupChecks{display:flex;flex-wrap:wrap;gap:8px 24px}
.dsh-nx-toolList{margin:0}
.dsh-nx-toolList>summary{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-toolRows{display:flex;flex-direction:column;gap:6px;padding:8px 0 0}
.dsh-nx-toolRow{align-items:flex-start}
.dsh-nx-toolMeta{display:flex;flex-direction:column;gap:2px;min-width:0}
.dsh-nx-toolName{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:500;overflow-wrap:anywhere}
.dsh-nx-targets{margin-top:0}
.dsh-nx-diagnostics{padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)}
.dsh-nx-diagnostics[open] summary{margin-bottom:8px}
@ -996,52 +1029,6 @@ var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
topology: Object.freeze({ inPreset: false, public: false }),
bizMonitor: Object.freeze({ inPreset: true, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
"netx__queryNmsNeInventory",
"netx__getNmsNe",
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
topology: Object.freeze([
"netx__getTopologyTree",
"netx__getTopologyView",
"netx__createTopologyFolder",
"netx__addTopologyViewNodes",
"netx__removeTopologyViewNodes",
"netx__copyTopologyViewNodes",
"netx__updateTopologyViewPositions",
"netx__projectTopologyNeighbors",
"netx__queryTopologyFabricNodes",
"netx__classifyTopologyFabricNodes",
"netx__queryTopologyNeighborhood",
"netx__queryTopologyEdges",
"netx__layoutTopologyView",
"netx__suggestSinkHubs",
"netx__analyzeTopologyViewLayout"
]),
bizMonitor: Object.freeze([
"netx__listBizMonitors",
"netx__listBizMonitorBatches",
"netx__getBizMonitorContext",
"netx__getBizMonitorBoard",
"netx__listBizMonitorReds",
"netx__getBizMonitorDiffs",
"netx__getBizCollectBatch",
"netx__getBizCollectCommandRaw"
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
ops: "ops",
topology: "topology",
@ -1059,6 +1046,30 @@ function parsePresetIds(value) {
return [...new Set(current.split(/[\s,,]+/).filter(Boolean))];
}
// src/netx/mcp-catalog.ts
function parseDisabledTools(value) {
if (Array.isArray(value)) {
return [...new Set(value.filter((item) => typeof item === "string" && item.trim().length > 0).map((s) => s.trim()))];
}
if (typeof value !== "string")
return [];
const trimmed = value.trim();
if (!trimmed)
return [];
if (trimmed.startsWith("[")) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
return [...new Set(parsed.filter((item) => typeof item === "string" && item.trim().length > 0).map((s) => s.trim()))];
}
} catch {}
}
return [...new Set(trimmed.split(/[\s,,]+/).filter(Boolean))];
}
function serializeDisabledTools(names) {
return JSON.stringify([...new Set(names.map((n) => n.trim()).filter(Boolean))].sort());
}
// src/client/NetxopsCard.tsx
var jsx_runtime = require("react/jsx-runtime");
function phaseLocaleKey(phase) {
@ -1112,9 +1123,12 @@ function toolsBadgeLabel(t, view) {
return t("toolsStatusUnknown");
if (!view.toolsEnabled)
return t("toolsDisabled");
if (view.error) {
if (view.error || view.mcpPhase === "error") {
return t("toolsStatusError");
}
if (view.mcpPhase === "offline" || view.mcpFromCache) {
return t("mcpDisconnected");
}
if (view.count > 0) {
return fillTemplate(t("toolsStatusMounted"), {
count: String(view.count)
@ -1420,7 +1434,58 @@ function CapabilityGroupBlock(props) {
})
]
}),
props.targets
props.targets,
props.tools ? /* @__PURE__ */ jsx_runtime.jsxs("details", {
className: "dsh-nx-toolList",
children: [
/* @__PURE__ */ jsx_runtime.jsx("summary", {
children: props.toolCount ?? props.tools.length
}),
props.tools.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-hint",
children: props.emptyLabel ?? ""
}) : /* @__PURE__ */ jsx_runtime.jsx("div", {
className: "dsh-nx-toolRows",
children: props.tools.map((tool) => {
const enabled = !(props.disabledTools?.has(tool.name) ?? false);
const desc = tool.description.trim();
const short = desc.length > 72 ? `${desc.slice(0, 72)}…` : desc;
return /* @__PURE__ */ jsx_runtime.jsxs("label", {
className: "dsh-nx-checkRow dsh-nx-toolRow",
children: [
/* @__PURE__ */ jsx_runtime.jsx("input", {
type: "checkbox",
checked: enabled,
disabled: props.disabled,
onChange: (event) => {
props.onToggleTool?.(tool.name, event.target.checked);
}
}),
/* @__PURE__ */ jsx_runtime.jsxs("span", {
className: "dsh-nx-toolMeta",
children: [
/* @__PURE__ */ jsx_runtime.jsxs("span", {
className: "dsh-nx-toolName",
children: [
tool.name,
tool.readOnlyHint !== true ? /* @__PURE__ */ jsx_runtime.jsx("span", {
className: "dsh-nx-badgeMuted",
children: props.writeBadge ?? ""
}) : null
]
}),
short ? /* @__PURE__ */ jsx_runtime.jsx("span", {
className: "dsh-nx-hint",
children: short
}) : null
]
})
]
}, tool.name);
})
})
]
}) : null
]
});
}
@ -1677,9 +1742,23 @@ function NetxopsCard(props) {
/* @__PURE__ */ jsx_runtime.jsx("h3", {
children: t("sectionCapabilities")
}),
/* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
tone: toolsMountTone(state.toolsStatus),
label: toolsBadgeLabel(t, state.toolsStatus)
/* @__PURE__ */ jsx_runtime.jsxs("span", {
className: "dsh-nx-badges",
children: [
/* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
tone: toolsMountTone(state.toolsStatus),
label: toolsBadgeLabel(t, state.toolsStatus)
}),
/* @__PURE__ */ jsx_runtime.jsx("button", {
type: "button",
className: "dsh-nx-btn",
disabled: disabled || state.mcpRefreshBusy,
onClick: () => {
props.refreshMcpCatalog();
},
children: state.mcpRefreshBusy ? t("mcpRefreshing") : t("mcpRefresh")
})
]
})
]
}),
@ -1696,60 +1775,104 @@ function NetxopsCard(props) {
onReset: () => {
props.resetField("toolsEnabled");
},
error: state.toolsStatus?.error ? /* @__PURE__ */ jsx_runtime.jsx("p", {
error: state.toolsStatus?.error || state.toolsStatus?.mcpError || state.mcpCatalog?.error ? /* @__PURE__ */ jsx_runtime.jsxs("p", {
className: "dsh-nx-invalid",
role: "status",
children: state.toolsStatus.error
children: [
state.toolsStatus?.error || state.toolsStatus?.mcpError || state.mcpCatalog?.error,
state.toolsStatus?.mcpFromCache || state.mcpCatalog?.fromCache ? ` · ${t("mcpDisconnected")}` : ""
]
}) : null
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupOps"),
toolCount: TOOLS_BY_GROUP.ops.length,
targets: targets("groupOpsPresets", state.groupOpsPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupOpsInPreset,
published: state.groupOpsPublic,
/* @__PURE__ */ jsx_runtime.jsx(ToggleField, {
id: "netxops-readonly-only",
label: t("readOnlyOnly"),
checked: state.readOnlyOnly.text === "true",
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupOpsInPreset", checked ? "true" : "false");
overridden: state.readOnlyOnly.overridden,
resetLabel: t("reset"),
onToggle: (checked) => {
props.edit("readOnlyOnly", String(checked));
},
onEditPublic: (checked) => {
props.edit("groupOpsPublic", checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupTopology"),
toolCount: TOOLS_BY_GROUP.topology.length,
targets: targets("groupTopologyPresets", state.groupTopologyPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupTopologyInPreset,
published: state.groupTopologyPublic,
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupTopologyInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupTopologyPublic", checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupBizMonitor"),
toolCount: TOOLS_BY_GROUP.bizMonitor.length,
targets: targets("groupBizMonitorPresets", state.groupBizMonitorPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupBizMonitorInPreset,
published: state.groupBizMonitorPublic,
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupBizMonitorInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupBizMonitorPublic", checked ? "true" : "false");
onReset: () => {
props.resetField("readOnlyOnly");
}
}),
(() => {
const catalogTools = state.mcpCatalog?.tools ?? [];
const byGroup = {
ops: catalogTools.filter((tool) => tool.group === "ops"),
topology: catalogTools.filter((tool) => tool.group === "topology"),
bizMonitor: catalogTools.filter((tool) => tool.group === "bizMonitor")
};
const disabledSet = new Set(parseDisabledTools(state.disabledTools.text));
const toolProps = {
disabledTools: disabledSet,
writeBadge: t("toolWriteBadge"),
emptyLabel: t("toolListEmpty"),
onToggleTool: (name, enabled) => {
props.setToolEnabled(name, enabled);
}
};
return /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, {
children: [
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupOps"),
toolCount: byGroup.ops.length,
tools: byGroup.ops,
targets: targets("groupOpsPresets", state.groupOpsPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupOpsInPreset,
published: state.groupOpsPublic,
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupOpsInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupOpsPublic", checked ? "true" : "false");
},
...toolProps
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupTopology"),
toolCount: byGroup.topology.length,
tools: byGroup.topology,
targets: targets("groupTopologyPresets", state.groupTopologyPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupTopologyInPreset,
published: state.groupTopologyPublic,
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupTopologyInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupTopologyPublic", checked ? "true" : "false");
},
...toolProps
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupBizMonitor"),
toolCount: byGroup.bizMonitor.length,
tools: byGroup.bizMonitor,
targets: targets("groupBizMonitorPresets", state.groupBizMonitorPresets),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupBizMonitorInPreset,
published: state.groupBizMonitorPublic,
disabled: publicationDisabled,
onEditInPreset: (checked) => {
props.edit("groupBizMonitorInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupBizMonitorPublic", checked ? "true" : "false");
},
...toolProps
})
]
});
})(),
state.toolsStatus && state.toolsStatus.mounts.length > 0 ? /* @__PURE__ */ jsx_runtime.jsxs("details", {
className: "dsh-nx-diagnostics",
children: [
@ -2097,6 +2220,22 @@ function apiUrlField(field) {
}
};
}
function booleanField(field) {
return {
field,
format: (value) => value === true ? "true" : "false",
parse: (text) => {
const normalized = text.trim().toLowerCase();
if (normalized === "true" || normalized === "1" || normalized === "yes") {
return { kind: "set", value: true };
}
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "") {
return { kind: "clear" };
}
return;
}
};
}
function booleanFieldPersistFalse(field) {
return {
field,
@ -2306,6 +2445,68 @@ class CardForm {
}
}
// src/client/mcp-catalog-view.ts
var TOOLS_CATALOG_ENDPOINT = "tools.catalog";
var TOOLS_REFRESH_ENDPOINT = "tools.refresh";
var EMPTY3 = {
phase: "idle",
mcpUrl: "",
error: "",
listedAt: null,
fromCache: false,
tools: []
};
function asMcpCatalogView(value) {
if (value === null || typeof value !== "object" || Array.isArray(value))
return { ...EMPTY3 };
const row = value;
const tools = [];
if (Array.isArray(row.tools)) {
for (const raw of row.tools) {
if (!raw || typeof raw !== "object" || Array.isArray(raw))
continue;
const t = raw;
const name = typeof t.name === "string" ? t.name : "";
if (!name)
continue;
const group = t.group === "ops" || t.group === "topology" || t.group === "bizMonitor" ? t.group : null;
tools.push({
name,
description: typeof t.description === "string" ? t.description : "",
readOnlyHint: t.readOnlyHint === true,
group
});
}
}
return {
phase: typeof row.phase === "string" ? row.phase : "idle",
mcpUrl: typeof row.mcpUrl === "string" ? row.mcpUrl : "",
error: typeof row.error === "string" ? row.error : "",
listedAt: typeof row.listedAt === "string" ? row.listedAt : null,
fromCache: row.fromCache === true,
tools
};
}
async function fetchMcpCatalog(call, signal) {
const result = await call(NETXOPS_RPC_CHANNEL3, TOOLS_CATALOG_ENDPOINT, {}, signal);
if (result !== null && typeof result === "object" && result.ok === true) {
return asMcpCatalogView(result.value);
}
return { ...EMPTY3 };
}
async function refreshMcpCatalogRemote(call, signal) {
const result = await call(NETXOPS_RPC_CHANNEL3, TOOLS_REFRESH_ENDPOINT, {}, signal);
if (result !== null && typeof result === "object") {
const row = result;
const view = asMcpCatalogView(row.value);
if (typeof row.error === "string" && row.error && !view.error) {
return { ...view, error: row.error, phase: view.phase === "connected" ? view.phase : "error" };
}
return view;
}
return { ...EMPTY3, phase: "error", error: "refresh_failed" };
}
// src/client/controller.ts
var NETXOPS_NS = "netxops";
var NETXOPS_SECTION_ID = "dsh-netxops";
@ -2328,6 +2529,9 @@ class NetxopsCardController {
alarmPushStatus = null;
toolsStatus = null;
toolsStatusInFlight = false;
mcpCatalog = null;
mcpCatalogInFlight = false;
mcpRefreshBusy = false;
presetCatalog = [];
presetCatalogError = false;
presetCatalogInFlight = false;
@ -2375,6 +2579,8 @@ class NetxopsCardController {
booleanFieldPersistFalse("alarmPushEnabled"),
booleanFieldPersistFalse("alarmDeliverDsh"),
booleanFieldPersistFalse("alarmDeliverIm"),
booleanField("readOnlyOnly"),
textField("disabledTools"),
textField("imTargets"),
textField("imBotId"),
textField("imTargetId")
@ -2412,6 +2618,10 @@ class NetxopsCardController {
this.toolsStatus = null;
changed = true;
}
if (this.mcpCatalog !== null) {
this.mcpCatalog = null;
changed = true;
}
this.presetCatalog = [];
this.presetCatalogError = true;
changed = true;
@ -2435,6 +2645,7 @@ class NetxopsCardController {
this.startStatusPoll();
this.refreshAlarmPushStatus();
this.refreshToolsStatus();
this.refreshMcpCatalogStatus();
this.refreshPresetCatalog();
this.reloadKbFromHost();
this.refreshKbPreview();
@ -2455,6 +2666,7 @@ class NetxopsCardController {
this.pollTimer = setInterval(() => {
this.refreshAlarmPushStatus();
this.refreshToolsStatus();
this.refreshMcpCatalogStatus();
this.refreshPresetCatalog();
this.refreshKbStatus();
this.refreshImDeliveryCatalog();
@ -2499,6 +2711,50 @@ class NetxopsCardController {
this.toolsStatusInFlight = false;
}
}
async refreshMcpCatalogStatus() {
const call = this.rpcCall;
if (call === undefined || this.mcpCatalogInFlight)
return;
this.mcpCatalogInFlight = true;
try {
const next = await fetchMcpCatalog(call);
const prev = this.mcpCatalog;
if (prev && JSON.stringify(prev) === JSON.stringify(next))
return;
this.mcpCatalog = next;
this.store.set(this.projection());
} catch {} finally {
this.mcpCatalogInFlight = false;
}
}
async runMcpRefresh() {
const call = this.rpcCall;
if (call === undefined || this.mcpRefreshBusy)
return;
this.mcpRefreshBusy = true;
this.store.set(this.projection());
try {
const next = await refreshMcpCatalogRemote(call);
this.mcpCatalog = next;
await this.refreshToolsStatus();
} catch {} finally {
this.mcpRefreshBusy = false;
this.store.set(this.projection());
}
}
setToolEnabled(serverName, enabled) {
const name = serverName.trim();
if (!name)
return;
const current = new Set(parseDisabledTools(this.form.field("disabledTools").text));
if (enabled)
current.delete(name);
else
current.add(name);
const next = serializeDisabledTools([...current]);
this.form.actions().edit("disabledTools", next);
this.form.save(["disabledTools"]);
}
async refreshPresetCatalog() {
const call = this.rpcCall;
if (!call || this.presetCatalogInFlight)
@ -2759,6 +3015,10 @@ class NetxopsCardController {
apiTokenRemoteReady: this.credential.remoteReady,
alarmPushStatus: this.alarmPushStatus,
toolsStatus: this.toolsStatus,
mcpCatalog: this.mcpCatalog,
mcpRefreshBusy: this.mcpRefreshBusy,
disabledTools: this.form.field("disabledTools"),
readOnlyOnly: this.form.field("readOnlyOnly"),
kbStatus: this.kbPreview ?? this.kbStatus,
kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined || this.rpcCall !== undefined,
kbUiError: this.kbUiError,
@ -2826,7 +3086,9 @@ class NetxopsCardController {
"groupBizMonitorInPreset",
"groupBizMonitorPublic",
"groupKbInPreset",
"groupKbPublic"
"groupKbPublic",
"disabledTools",
"readOnlyOnly"
]);
return {
hooks: { netxopsCard: this.store },
@ -2854,6 +3116,12 @@ class NetxopsCardController {
},
browseKbRoot: () => {
this.browseKbRoot();
},
refreshMcpCatalog: () => {
this.runMcpRefresh();
},
setToolEnabled: (serverName, enabled) => {
this.setToolEnabled(serverName, enabled);
}
};
}
@ -2969,6 +3237,12 @@ var en = {
toolsStatusEmpty: "No active tools",
toolsStatusError: "Publication failed",
toolsStatusUnknown: "Status unavailable",
mcpDisconnected: "Disconnected",
mcpRefresh: "Refresh",
mcpRefreshing: "Refreshing…",
readOnlyOnly: "Read-only tools only",
toolWriteBadge: "write",
toolListEmpty: "No tools yet",
sessionsExportCount: "{count} sessions",
sessionsExportUnavailable: "Export unavailable",
sessionsExportNoPersistence: "Session store unavailable.",
@ -3056,6 +3330,12 @@ var zh = {
toolsStatusEmpty: "暂无活跃工具",
toolsStatusError: "发布异常",
toolsStatusUnknown: "状态不可用",
mcpDisconnected: "未连接",
mcpRefresh: "刷新",
mcpRefreshing: "刷新中…",
readOnlyOnly: "只允许只读工具",
toolWriteBadge: "写",
toolListEmpty: "暂无工具清单",
sessionsExportCount: "{count} 个会话",
sessionsExportUnavailable: "无法导出",
sessionsExportNoPersistence: "会话存储不可用。",
@ -3196,7 +3476,9 @@ var MEMORY_DEFAULTS = {
groupBizMonitorPublic: false,
kbRoot: "",
groupKbInPreset: true,
groupKbPublic: false
groupKbPublic: false,
disabledTools: "[]",
readOnlyOnly: false
};
function apply(ctx) {
ctx.effect(() => {