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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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(() => {

File diff suppressed because it is too large Load diff