feat(card): show cached netx skills per capability group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 23:23:40 +08:00
parent fa82f530e9
commit 056f2d4ea3
9 changed files with 285 additions and 96 deletions

View file

@ -29,7 +29,7 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
@ -62,7 +62,7 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
@ -95,7 +95,7 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
@ -128,7 +128,7 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
@ -161,7 +161,7 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
@ -194,27 +194,27 @@
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/client.js": { "lib/client.js": {
"output": "16f3328f3d747ebcfc02accbc09419868db10618569fab7493e3875ada9d6b45", "output": "76d73a9e8354e55c27d60c90d17206b7d9eb096829a64cbfc15ab1152c7db49b",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1", "scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
"src/client/NetxopsCard.tsx": "ab8c18fdfd5550b89c8d3b0df46e0448abc27b824a6cd644e9e364fdaceafd46", "src/client/NetxopsCard.tsx": "51d85f150fd13fc5377de29a4e80b992e860514e0700884218f9f9c7bdcd4cc1",
"src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31", "src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31",
"src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9", "src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9",
"src/client/controller.ts": "d3353feda6f6b004dd094b1fc90d743003b55ce5a9d0b46c8fb9c1eb33925cc1", "src/client/controller.ts": "d3353feda6f6b004dd094b1fc90d743003b55ce5a9d0b46c8fb9c1eb33925cc1",
"src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117", "src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117",
"src/client/index.ts": "1f73ae8e8c02e13b47c5679869359444485b4621eca20a15c632289f22e5d760", "src/client/index.ts": "1f73ae8e8c02e13b47c5679869359444485b4621eca20a15c632289f22e5d760",
"src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10", "src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10",
"src/client/locales.ts": "5acc66e429bfd9e91c628e6d825582d9fa2f4bebcc6d114384ca5628f5deb61f", "src/client/locales.ts": "b9aa9cac558119c3a81e773dc3b49378f0c37fb2220696a484a92bf19aabb5d6",
"src/client/mcp-catalog-view.ts": "68a79f1de44760c1e08edad53e1df47eb82124924b9bb680bb430db3c7edb89a", "src/client/mcp-catalog-view.ts": "3066ba1fd56b19e099e1f58ba9699a4d17de9f5733e27a4f4da0c4f58809a545",
"src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd", "src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd",
"src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb", "src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb",
"src/client/snapshot-store.ts": "25fdcae95490a9d7f1f520ec90981cefb4fdd576b340717e9087f1ca82c6563d", "src/client/snapshot-store.ts": "25fdcae95490a9d7f1f520ec90981cefb4fdd576b340717e9087f1ca82c6563d",
@ -231,14 +231,14 @@
} }
}, },
"lib/index.js": { "lib/index.js": {
"output": "b3712d7ccff953a8fa1ae0b0339ab3b247d65ce21d4690817d15c168d2876275", "output": "515aafc5a26d8680cedbe970751b5f565e098c8f691014fb0f9a1fe9edd6f82f",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8", "scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
"src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc", "src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc",
"src/index.ts": "d5f7417240c56ec24bf9b456be26749da7130a60d84659f7dd1941965682c086", "src/index.ts": "f583927e67045d83362930d8c416d540c1d8b08b18b2ad78e2a3d72ea03df559",
"src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62", "src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62",
"src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf", "src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf",
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f", "src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
@ -271,7 +271,7 @@
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967", "src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6", "src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6",
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409", "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139",
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5" "src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"

View file

@ -1435,6 +1435,14 @@ function CapabilityGroupBlock(props) {
] ]
}), }),
props.targets, props.targets,
props.skills ? /* @__PURE__ */ jsx_runtime.jsxs("p", {
className: "dsh-nx-hint",
children: [
props.skillsLabel ?? "Skill",
":",
props.skills.length === 0 ? props.skillsNoneLabel ?? "" : props.skills.map((skill) => `${skill.name}(${props.skillFilesLabel?.(skill.files) ?? skill.files})`).join("、")
]
}) : null,
props.tools ? /* @__PURE__ */ jsx_runtime.jsxs("details", { props.tools ? /* @__PURE__ */ jsx_runtime.jsxs("details", {
className: "dsh-nx-toolList", className: "dsh-nx-toolList",
children: [ children: [
@ -1806,7 +1814,12 @@ function NetxopsCard(props) {
bizMonitor: catalogTools.filter((tool) => tool.group === "bizMonitor") bizMonitor: catalogTools.filter((tool) => tool.group === "bizMonitor")
}; };
const disabledSet = new Set(parseDisabledTools(state.disabledTools.text)); const disabledSet = new Set(parseDisabledTools(state.disabledTools.text));
const catalogSkills = state.mcpCatalog?.skills ?? [];
const skillsOf = (group) => catalogSkills.filter((skill) => skill.group === group);
const toolProps = { const toolProps = {
skillsLabel: t("groupSkills"),
skillsNoneLabel: t("groupSkillsNone"),
skillFilesLabel: (count) => t("skillFiles").replace("{count}", String(count)),
disabledTools: disabledSet, disabledTools: disabledSet,
writeBadge: t("toolWriteBadge"), writeBadge: t("toolWriteBadge"),
emptyLabel: t("toolListEmpty"), emptyLabel: t("toolListEmpty"),
@ -1820,6 +1833,7 @@ function NetxopsCard(props) {
title: t("groupOps"), title: t("groupOps"),
toolCount: byGroup.ops.length, toolCount: byGroup.ops.length,
tools: byGroup.ops, tools: byGroup.ops,
skills: skillsOf("ops"),
targets: targets("groupOpsPresets", state.groupOpsPresets), targets: targets("groupOpsPresets", state.groupOpsPresets),
inPresetLabel: t("groupInPreset"), inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"), publicLabel: t("groupPublic"),
@ -1838,6 +1852,7 @@ function NetxopsCard(props) {
title: t("groupTopology"), title: t("groupTopology"),
toolCount: byGroup.topology.length, toolCount: byGroup.topology.length,
tools: byGroup.topology, tools: byGroup.topology,
skills: skillsOf("topology"),
targets: targets("groupTopologyPresets", state.groupTopologyPresets), targets: targets("groupTopologyPresets", state.groupTopologyPresets),
inPresetLabel: t("groupInPreset"), inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"), publicLabel: t("groupPublic"),
@ -1856,6 +1871,7 @@ function NetxopsCard(props) {
title: t("groupBizMonitor"), title: t("groupBizMonitor"),
toolCount: byGroup.bizMonitor.length, toolCount: byGroup.bizMonitor.length,
tools: byGroup.bizMonitor, tools: byGroup.bizMonitor,
skills: skillsOf("bizMonitor"),
targets: targets("groupBizMonitorPresets", state.groupBizMonitorPresets), targets: targets("groupBizMonitorPresets", state.groupBizMonitorPresets),
inPresetLabel: t("groupInPreset"), inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"), publicLabel: t("groupPublic"),
@ -2454,7 +2470,9 @@ var EMPTY3 = {
error: "", error: "",
listedAt: null, listedAt: null,
fromCache: false, fromCache: false,
tools: [] tools: [],
skills: [],
skillsListedAt: null
}; };
function asMcpCatalogView(value) { function asMcpCatalogView(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) if (value === null || typeof value !== "object" || Array.isArray(value))
@ -2478,13 +2496,28 @@ function asMcpCatalogView(value) {
}); });
} }
} }
const skills = [];
if (Array.isArray(row.skills)) {
for (const raw of row.skills) {
if (!raw || typeof raw !== "object" || Array.isArray(raw))
continue;
const s = raw;
const group = s.group === "ops" || s.group === "topology" || s.group === "bizMonitor" ? s.group : null;
const name = typeof s.name === "string" ? s.name : "";
if (!group || !name)
continue;
skills.push({ group, name, files: typeof s.files === "number" ? s.files : 0 });
}
}
return { return {
phase: typeof row.phase === "string" ? row.phase : "idle", phase: typeof row.phase === "string" ? row.phase : "idle",
mcpUrl: typeof row.mcpUrl === "string" ? row.mcpUrl : "", mcpUrl: typeof row.mcpUrl === "string" ? row.mcpUrl : "",
error: typeof row.error === "string" ? row.error : "", error: typeof row.error === "string" ? row.error : "",
listedAt: typeof row.listedAt === "string" ? row.listedAt : null, listedAt: typeof row.listedAt === "string" ? row.listedAt : null,
fromCache: row.fromCache === true, fromCache: row.fromCache === true,
tools tools,
skills,
skillsListedAt: typeof row.skillsListedAt === "string" ? row.skillsListedAt : null
}; };
} }
async function fetchMcpCatalog(call, signal) { async function fetchMcpCatalog(call, signal) {
@ -3243,6 +3276,9 @@ var en = {
readOnlyOnly: "Read-only tools only", readOnlyOnly: "Read-only tools only",
toolWriteBadge: "write", toolWriteBadge: "write",
toolListEmpty: "No tools yet", toolListEmpty: "No tools yet",
groupSkills: "Skills",
groupSkillsNone: "None pulled from netx",
skillFiles: "{count} files",
sessionsExportCount: "{count} sessions", sessionsExportCount: "{count} sessions",
sessionsExportUnavailable: "Export unavailable", sessionsExportUnavailable: "Export unavailable",
sessionsExportNoPersistence: "Session store unavailable.", sessionsExportNoPersistence: "Session store unavailable.",
@ -3336,6 +3372,9 @@ var zh = {
readOnlyOnly: "只允许只读工具", readOnlyOnly: "只允许只读工具",
toolWriteBadge: "写", toolWriteBadge: "写",
toolListEmpty: "暂无工具清单", toolListEmpty: "暂无工具清单",
groupSkills: "Skill",
groupSkillsNone: "未从 netx 拉到",
skillFiles: "{count} 个文件",
sessionsExportCount: "{count} 个会话", sessionsExportCount: "{count} 个会话",
sessionsExportUnavailable: "无法导出", sessionsExportUnavailable: "无法导出",
sessionsExportNoPersistence: "会话存储不可用。", sessionsExportNoPersistence: "会话存储不可用。",

View file

@ -2,7 +2,7 @@ import { createRequire } from "node:module";
var __require = /* @__PURE__ */ createRequire(import.meta.url); var __require = /* @__PURE__ */ createRequire(import.meta.url);
// src/index.ts // src/index.ts
import { readFileSync as readFileSync9 } from "node:fs"; import { readFileSync as readFileSync10 } from "node:fs";
import { dirname as dirname8, join as join14 } from "node:path"; import { dirname as dirname8, join as join14 } from "node:path";
import { fileURLToPath as fileURLToPath4 } from "node:url"; import { fileURLToPath as fileURLToPath4 } from "node:url";
import z from "@deepseek-ai/schemastery"; import z from "@deepseek-ai/schemastery";
@ -1391,7 +1391,9 @@ import {
existsSync as existsSync3, existsSync as existsSync3,
mkdirSync as mkdirSync2, mkdirSync as mkdirSync2,
readdirSync, readdirSync,
readFileSync as readFileSync3,
rmSync, rmSync,
statSync as statSync2,
writeFileSync as writeFileSync3 writeFileSync as writeFileSync3
} from "node:fs"; } from "node:fs";
import { homedir as homedir3 } from "node:os"; import { homedir as homedir3 } from "node:os";
@ -1449,6 +1451,40 @@ function replaceSkillsCache(apiUrl, files) {
`, "utf8"); `, "utf8");
return root; return root;
} }
function countFiles(dir) {
let n = 0;
for (const entry of readdirSync(dir)) {
const full = join3(dir, entry);
n += statSync2(full).isDirectory() ? countFiles(full) : 1;
}
return n;
}
function listCachedSkills(apiUrl) {
const root = skillsCacheDir(apiUrl);
const skills = [];
let listedAt = null;
if (!apiUrl.trim() || !existsSync3(root))
return { listedAt, skills };
try {
const meta = JSON.parse(readFileSync3(join3(root, ".meta.json"), "utf8"));
if (typeof meta.listedAt === "string")
listedAt = meta.listedAt;
} catch {}
for (const group of CAPABILITY_GROUP_IDS) {
const groupDir = join3(root, SKILL_DIR_BY_GROUP[group]);
if (!existsSync3(groupDir))
continue;
try {
for (const name of readdirSync(groupDir).sort()) {
const dir = join3(groupDir, name);
if (!statSync2(dir).isDirectory() || !existsSync3(join3(dir, "SKILL.md")))
continue;
skills.push({ group, name, files: countFiles(dir) });
}
} catch {}
}
return { listedAt, skills };
}
// src/netx/group-skills.ts // src/netx/group-skills.ts
function parseFrontmatter(raw) { function parseFrontmatter(raw) {
@ -1659,11 +1695,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync5 } from "node:fs";
import { join as join7 } from "node:path"; import { join as join7 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
import { basename, dirname as dirname3, join as join5, resolve } from "node:path"; import { basename, dirname as dirname3, join as join5, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -1710,7 +1746,7 @@ function findManifest(kbRoot, maxDepth = 3) {
} }
let rootStat; let rootStat;
try { try {
rootStat = statSync2(root); rootStat = statSync3(root);
} catch { } catch {
return { paths: [], error: `kbRoot not found: ${root}` }; return { paths: [], error: `kbRoot not found: ${root}` };
} }
@ -1720,7 +1756,7 @@ function findManifest(kbRoot, maxDepth = 3) {
const direct = join5(root, "MANIFEST.json"); const direct = join5(root, "MANIFEST.json");
if (existsSync4(direct)) { if (existsSync4(direct)) {
try { try {
if (statSync2(direct).isFile()) if (statSync3(direct).isFile())
return { paths: [direct] }; return { paths: [direct] };
} catch {} } catch {}
} }
@ -1731,7 +1767,7 @@ function findManifest(kbRoot, maxDepth = 3) {
const candidate = join5(dir, "MANIFEST.json"); const candidate = join5(dir, "MANIFEST.json");
if (existsSync4(candidate)) { if (existsSync4(candidate)) {
try { try {
if (statSync2(candidate).isFile()) if (statSync3(candidate).isFile())
found.push(candidate); found.push(candidate);
} catch {} } catch {}
} }
@ -1749,7 +1785,7 @@ function findManifest(kbRoot, maxDepth = 3) {
const full = join5(dir, name); const full = join5(dir, name);
let st; let st;
try { try {
st = statSync2(full); st = statSync3(full);
} catch { } catch {
continue; continue;
} }
@ -1769,7 +1805,7 @@ function findManifest(kbRoot, maxDepth = 3) {
const full = join5(root, name); const full = join5(root, name);
let st; let st;
try { try {
st = statSync2(full); st = statSync3(full);
} catch { } catch {
continue; continue;
} }
@ -1859,7 +1895,7 @@ function resolveKbRoot(kbRoot, maxDepth = 3) {
return unconfiguredKbSnapshot(); return unconfiguredKbSnapshot();
let root = resolve(trimmed); let root = resolve(trimmed);
try { try {
const st = statSync2(root); const st = statSync3(root);
if (st.isFile()) { if (st.isFile()) {
if (basename(root).toLowerCase() === "manifest.json") { if (basename(root).toLowerCase() === "manifest.json") {
root = dirname3(root); root = dirname3(root);
@ -1884,7 +1920,7 @@ function resolveKbRoot(kbRoot, maxDepth = 3) {
const manifestPath = located.paths[0]; const manifestPath = located.paths[0];
let raw; let raw;
try { try {
raw = readFileSync3(manifestPath, "utf8"); raw = readFileSync4(manifestPath, "utf8");
} catch (error) { } catch (error) {
return errorSnapshot(`cannot read MANIFEST: ${error instanceof Error ? error.message : String(error)}`); return errorSnapshot(`cannot read MANIFEST: ${error instanceof Error ? error.message : String(error)}`);
} }
@ -1926,7 +1962,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync5, realpathSync, statSync as statSync3 } from "node:fs"; import { existsSync as existsSync5, realpathSync, statSync as statSync4 } from "node:fs";
import { join as join6, relative, resolve as resolve2, sep } from "node:path"; import { join as join6, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -2046,7 +2082,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync3(realFile).isFile()) { if (!statSync4(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -2111,9 +2147,9 @@ function readKbIdentity(snapshot) {
const full = join7(dir, name); const full = join7(dir, name);
let text; let text;
try { try {
if (!statSync4(full).isFile()) if (!statSync5(full).isFile())
continue; continue;
text = readFileSync4(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync5(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -2239,9 +2275,9 @@ import {
existsSync as existsSync6, existsSync as existsSync6,
mkdirSync as mkdirSync3, mkdirSync as mkdirSync3,
readdirSync as readdirSync4, readdirSync as readdirSync4,
readFileSync as readFileSync5, readFileSync as readFileSync6,
rmSync as rmSync2, rmSync as rmSync2,
statSync as statSync5, statSync as statSync6,
writeFileSync as writeFileSync4 writeFileSync as writeFileSync4
} from "node:fs"; } from "node:fs";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
@ -2404,7 +2440,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join8(dir, name); const full = join8(dir, name);
let st; let st;
try { try {
st = statSync5(full); st = statSync6(full);
} catch { } catch {
continue; continue;
} }
@ -2438,7 +2474,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; import { readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync7 } from "node:fs";
import { extname, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path"; import { extname, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]); var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]); var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -2483,14 +2519,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync6(target.absolutePath); const st = statSync7(target.absolutePath);
if (st.isDirectory()) { if (st.isDirectory()) {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => { const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync6(join9(target.absolutePath, entry.name)).size; size = statSync7(join9(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -2499,7 +2535,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync6(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync7(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -2544,7 +2580,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync6(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync7(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -2574,7 +2610,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync6(full); const st = statSync7(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -2604,7 +2640,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync6(dir); const st = statSync7(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -3273,7 +3309,7 @@ function replyReminder(setting) {
// src/netx/session-export.ts // src/netx/session-export.ts
import { homedir as homedir5, hostname as osHostname } from "node:os"; import { homedir as homedir5, hostname as osHostname } from "node:os";
// ../../netxops/node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs // node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
import { createRequire as createRequire3 } from "module"; import { createRequire as createRequire3 } from "module";
var require2 = createRequire3("/"); var require2 = createRequire3("/");
var _a; var _a;
@ -4540,7 +4576,7 @@ function mountNetxopsWebRoute(ctx, channel, handler) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync7 } from "node:fs"; import { readFileSync as readFileSync8 } from "node:fs";
import { dirname as dirname6, join as join12 } from "node:path"; import { dirname as dirname6, join as join12 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -5085,7 +5121,7 @@ async function refreshMcpCatalog(input) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync7(join12(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync8(join12(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -5217,13 +5253,13 @@ function registerNetxTools(ctx, connection, options) {
import { createScope, scopeOf } from "@deepseek-ai/dsh-scope"; import { createScope, scopeOf } from "@deepseek-ai/dsh-scope";
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync8 } from "node:fs"; import { readFileSync as readFileSync9 } from "node:fs";
import { dirname as dirname7, join as join13 } from "node:path"; import { dirname as dirname7, join as join13 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url"; import { fileURLToPath as fileURLToPath3 } from "node:url";
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname7(fileURLToPath3(import.meta.url)); const here = dirname7(fileURLToPath3(import.meta.url));
const pkg = JSON.parse(readFileSync8(join13(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync9(join13(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -5476,6 +5512,24 @@ function installPresetPublication(ctx) {
} }
// src/index.ts // src/index.ts
function mcpCatalogPayload(catalog) {
const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? "");
return {
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
tools: catalog.tools.map((tool2) => ({
name: tool2.name,
description: tool2.description,
readOnlyHint: tool2.readOnlyHint,
group: tool2.group ?? null
})),
skills: cached.skills,
skillsListedAt: cached.listedAt
};
}
var name = "netxops"; var name = "netxops";
var inject = ["tools"]; var inject = ["tools"];
var NETXOPS_RPC_CHANNEL = "/netxops"; var NETXOPS_RPC_CHANNEL = "/netxops";
@ -5550,7 +5604,7 @@ function resolveApiUrlForConnection(value) {
function packageVersion3() { function packageVersion3() {
try { try {
const here = dirname8(fileURLToPath4(import.meta.url)); const here = dirname8(fileURLToPath4(import.meta.url));
const pkg = JSON.parse(readFileSync9(join14(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync10(join14(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -6147,23 +6201,7 @@ function apply(ctx, config = Config({})) {
}; };
} }
if (endpoint === "tools.catalog") { if (endpoint === "tools.catalog") {
const catalog = getMcpCatalogState(); return { ok: true, value: mcpCatalogPayload(getMcpCatalogState()) };
return {
ok: true,
value: {
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
tools: catalog.tools.map((tool2) => ({
name: tool2.name,
description: tool2.description,
readOnlyHint: tool2.readOnlyHint,
group: tool2.group ?? null
}))
}
};
} }
if (endpoint === "tools.refresh") { if (endpoint === "tools.refresh") {
const connection = getNetxConnection(); const connection = getNetxConnection();
@ -6177,14 +6215,7 @@ function apply(ctx, config = Config({})) {
}); });
return { return {
ok: catalog.phase === "connected", ok: catalog.phase === "connected",
value: { value: { ...mcpCatalogPayload(catalog), toolCount: catalog.tools.length },
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
toolCount: catalog.tools.length
},
...catalog.error ? { error: catalog.error } : {} ...catalog.error ? { error: catalog.error } : {}
}; };
} }

View file

@ -23,7 +23,7 @@ import { ensureStyles } from './styles.ts'
import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts' import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts'
import { parsePresetIds } from '../netx/capability-groups.ts' import { parsePresetIds } from '../netx/capability-groups.ts'
import { parseDisabledTools } from '../netx/mcp-catalog.ts' import { parseDisabledTools } from '../netx/mcp-catalog.ts'
import type { McpToolRow } from './mcp-catalog-view.ts' import type { McpSkillRow, McpToolRow } from './mcp-catalog-view.ts'
import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client' import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
@ -368,6 +368,10 @@ function CapabilityGroupBlock(props: {
writeBadge?: string writeBadge?: string
emptyLabel?: string emptyLabel?: string
onToggleTool?: (name: string, enabled: boolean) => void onToggleTool?: (name: string, enabled: boolean) => void
skills?: McpSkillRow[]
skillsLabel?: string
skillsNoneLabel?: string
skillFilesLabel?: (count: number) => string
}) { }) {
return ( return (
<div className="dsh-nx-groupBlock" role="group" aria-label={props.title}> <div className="dsh-nx-groupBlock" role="group" aria-label={props.title}>
@ -396,6 +400,18 @@ function CapabilityGroupBlock(props: {
</label> </label>
</div> </div>
{props.targets} {props.targets}
{props.skills
? (
<p className="dsh-nx-hint">
{props.skillsLabel ?? 'Skill'}:
{props.skills.length === 0
? (props.skillsNoneLabel ?? '')
: props.skills
.map((skill) => `${skill.name}(${props.skillFilesLabel?.(skill.files) ?? skill.files})`)
.join('、')}
</p>
)
: null}
{props.tools {props.tools
? ( ? (
<details className="dsh-nx-toolList"> <details className="dsh-nx-toolList">
@ -654,7 +670,12 @@ export function NetxopsCard(props: NetxopsCardProps) {
bizMonitor: catalogTools.filter((tool) => tool.group === 'bizMonitor'), bizMonitor: catalogTools.filter((tool) => tool.group === 'bizMonitor'),
} }
const disabledSet = new Set(parseDisabledTools(state.disabledTools.text)) const disabledSet = new Set(parseDisabledTools(state.disabledTools.text))
const catalogSkills = state.mcpCatalog?.skills ?? []
const skillsOf = (group: McpSkillRow['group']) => catalogSkills.filter((skill) => skill.group === group)
const toolProps = { const toolProps = {
skillsLabel: t('groupSkills'),
skillsNoneLabel: t('groupSkillsNone'),
skillFilesLabel: (count: number) => t('skillFiles').replace('{count}', String(count)),
disabledTools: disabledSet, disabledTools: disabledSet,
writeBadge: t('toolWriteBadge'), writeBadge: t('toolWriteBadge'),
emptyLabel: t('toolListEmpty'), emptyLabel: t('toolListEmpty'),
@ -666,6 +687,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
title={t('groupOps')} title={t('groupOps')}
toolCount={byGroup.ops.length} toolCount={byGroup.ops.length}
tools={byGroup.ops} tools={byGroup.ops}
skills={skillsOf('ops')}
targets={targets('groupOpsPresets', state.groupOpsPresets)} targets={targets('groupOpsPresets', state.groupOpsPresets)}
inPresetLabel={t('groupInPreset')} inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')} publicLabel={t('groupPublic')}
@ -684,6 +706,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
title={t('groupTopology')} title={t('groupTopology')}
toolCount={byGroup.topology.length} toolCount={byGroup.topology.length}
tools={byGroup.topology} tools={byGroup.topology}
skills={skillsOf('topology')}
targets={targets('groupTopologyPresets', state.groupTopologyPresets)} targets={targets('groupTopologyPresets', state.groupTopologyPresets)}
inPresetLabel={t('groupInPreset')} inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')} publicLabel={t('groupPublic')}
@ -702,6 +725,7 @@ export function NetxopsCard(props: NetxopsCardProps) {
title={t('groupBizMonitor')} title={t('groupBizMonitor')}
toolCount={byGroup.bizMonitor.length} toolCount={byGroup.bizMonitor.length}
tools={byGroup.bizMonitor} tools={byGroup.bizMonitor}
skills={skillsOf('bizMonitor')}
targets={targets('groupBizMonitorPresets', state.groupBizMonitorPresets)} targets={targets('groupBizMonitorPresets', state.groupBizMonitorPresets)}
inPresetLabel={t('groupInPreset')} inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')} publicLabel={t('groupPublic')}

View file

@ -72,6 +72,9 @@ export type NetxopsLocaleKey =
| 'readOnlyOnly' | 'readOnlyOnly'
| 'toolWriteBadge' | 'toolWriteBadge'
| 'toolListEmpty' | 'toolListEmpty'
| 'groupSkills'
| 'groupSkillsNone'
| 'skillFiles'
| 'sessionsExportCount' | 'sessionsExportCount'
| 'sessionsExportUnavailable' | 'sessionsExportUnavailable'
| 'sessionsExportNoPersistence' | 'sessionsExportNoPersistence'
@ -165,6 +168,9 @@ export const en: Record<NetxopsLocaleKey, string> = {
readOnlyOnly: 'Read-only tools only', readOnlyOnly: 'Read-only tools only',
toolWriteBadge: 'write', toolWriteBadge: 'write',
toolListEmpty: 'No tools yet', toolListEmpty: 'No tools yet',
groupSkills: 'Skills',
groupSkillsNone: 'None pulled from netx',
skillFiles: '{count} files',
sessionsExportCount: '{count} sessions', sessionsExportCount: '{count} sessions',
sessionsExportUnavailable: 'Export unavailable', sessionsExportUnavailable: 'Export unavailable',
sessionsExportNoPersistence: 'Session store unavailable.', sessionsExportNoPersistence: 'Session store unavailable.',
@ -259,6 +265,9 @@ export const zh: Record<NetxopsLocaleKey, string> = {
readOnlyOnly: '只允许只读工具', readOnlyOnly: '只允许只读工具',
toolWriteBadge: '写', toolWriteBadge: '写',
toolListEmpty: '暂无工具清单', toolListEmpty: '暂无工具清单',
groupSkills: 'Skill',
groupSkillsNone: '未从 netx 拉到',
skillFiles: '{count} 个文件',
sessionsExportCount: '{count} 个会话', sessionsExportCount: '{count} 个会话',
sessionsExportUnavailable: '无法导出', sessionsExportUnavailable: '无法导出',
sessionsExportNoPersistence: '会话存储不可用。', sessionsExportNoPersistence: '会话存储不可用。',

View file

@ -15,6 +15,12 @@ export interface McpToolRow {
group: 'ops' | 'topology' | 'bizMonitor' | null group: 'ops' | 'topology' | 'bizMonitor' | null
} }
export interface McpSkillRow {
group: 'ops' | 'topology' | 'bizMonitor'
name: string
files: number
}
export interface McpCatalogView { export interface McpCatalogView {
phase: string phase: string
mcpUrl: string mcpUrl: string
@ -22,6 +28,9 @@ export interface McpCatalogView {
listedAt: string | null listedAt: string | null
fromCache: boolean fromCache: boolean
tools: McpToolRow[] tools: McpToolRow[]
/** Skills in the local cache (pulled from netx `/mcp` resources). */
skills: McpSkillRow[]
skillsListedAt: string | null
} }
const EMPTY: McpCatalogView = { const EMPTY: McpCatalogView = {
@ -31,6 +40,8 @@ const EMPTY: McpCatalogView = {
listedAt: null, listedAt: null,
fromCache: false, fromCache: false,
tools: [], tools: [],
skills: [],
skillsListedAt: null,
} }
export function asMcpCatalogView(value: unknown): McpCatalogView { export function asMcpCatalogView(value: unknown): McpCatalogView {
@ -54,6 +65,17 @@ export function asMcpCatalogView(value: unknown): McpCatalogView {
}) })
} }
} }
const skills: McpSkillRow[] = []
if (Array.isArray(row.skills)) {
for (const raw of row.skills) {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue
const s = raw as Record<string, unknown>
const group = s.group === 'ops' || s.group === 'topology' || s.group === 'bizMonitor' ? s.group : null
const name = typeof s.name === 'string' ? s.name : ''
if (!group || !name) continue
skills.push({ group, name, files: typeof s.files === 'number' ? s.files : 0 })
}
}
return { return {
phase: typeof row.phase === 'string' ? row.phase : 'idle', phase: typeof row.phase === 'string' ? row.phase : 'idle',
mcpUrl: typeof row.mcpUrl === 'string' ? row.mcpUrl : '', mcpUrl: typeof row.mcpUrl === 'string' ? row.mcpUrl : '',
@ -61,6 +83,8 @@ export function asMcpCatalogView(value: unknown): McpCatalogView {
listedAt: typeof row.listedAt === 'string' ? row.listedAt : null, listedAt: typeof row.listedAt === 'string' ? row.listedAt : null,
fromCache: row.fromCache === true, fromCache: row.fromCache === true,
tools, tools,
skills,
skillsListedAt: typeof row.skillsListedAt === 'string' ? row.skillsListedAt : null,
} }
} }

View file

@ -81,6 +81,7 @@ import {
} from './netx/model-language.ts' } from './netx/model-language.ts'
import { normalizeApiUrl, type NormalizeApiUrlResult } from './netx/http.ts' import { normalizeApiUrl, type NormalizeApiUrlResult } from './netx/http.ts'
import { publishNetxConnection, getNetxConnection, watchNetxConnection } from './netx/runtime.ts' import { publishNetxConnection, getNetxConnection, watchNetxConnection } from './netx/runtime.ts'
import { listCachedSkills } from './netx/skills-cache.ts'
import { import {
getSessionsExportStatus, getSessionsExportStatus,
NETXOPS_SESSIONS_EXPORT_PATH, NETXOPS_SESSIONS_EXPORT_PATH,
@ -105,6 +106,26 @@ import {
watchMcpCatalog, watchMcpCatalog,
} from './netx/mcp-store.ts' } from './netx/mcp-store.ts'
/** Plugins-card catalog payload: tools from the MCP store + skills from the disk cache. */
function mcpCatalogPayload(catalog: ReturnType<typeof getMcpCatalogState>) {
const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? '')
return {
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
tools: catalog.tools.map((tool) => ({
name: tool.name,
description: tool.description,
readOnlyHint: tool.readOnlyHint,
group: tool.group ?? null,
})),
skills: cached.skills,
skillsListedAt: cached.listedAt,
}
}
// The event is absent from older peer declarations, where it is never emitted. // The event is absent from older peer declarations, where it is never emitted.
declare module '@deepseek-ai/cordis' { declare module '@deepseek-ai/cordis' {
interface Events { interface Events {
@ -1064,23 +1085,7 @@ export function apply(ctx: Context, config: ConfigInput<Config> = Config({} as C
} }
} }
if (endpoint === 'tools.catalog') { if (endpoint === 'tools.catalog') {
const catalog = getMcpCatalogState() return { ok: true, value: mcpCatalogPayload(getMcpCatalogState()) }
return {
ok: true,
value: {
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
tools: catalog.tools.map((tool) => ({
name: tool.name,
description: tool.description,
readOnlyHint: tool.readOnlyHint,
group: tool.group ?? null,
})),
},
}
} }
if (endpoint === 'tools.refresh') { if (endpoint === 'tools.refresh') {
const connection = getNetxConnection() const connection = getNetxConnection()
@ -1093,14 +1098,7 @@ export function apply(ctx: Context, config: ConfigInput<Config> = Config({} as C
}) })
return { return {
ok: catalog.phase === 'connected', ok: catalog.phase === 'connected',
value: { value: { ...mcpCatalogPayload(catalog), toolCount: catalog.tools.length },
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,
error: catalog.error,
listedAt: catalog.listedAt,
fromCache: catalog.fromCache,
toolCount: catalog.tools.length,
},
...(catalog.error ? { error: catalog.error } : {}), ...(catalog.error ? { error: catalog.error } : {}),
} }
} }

View file

@ -9,12 +9,15 @@ import {
existsSync, existsSync,
mkdirSync, mkdirSync,
readdirSync, readdirSync,
readFileSync,
rmSync, rmSync,
statSync,
writeFileSync, writeFileSync,
} from 'node:fs' } from 'node:fs'
import { homedir } from 'node:os' import { homedir } from 'node:os'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { import {
CAPABILITY_GROUP_IDS,
SKILL_DIR_BY_GROUP, SKILL_DIR_BY_GROUP,
type NetxCapabilityGroupId, type NetxCapabilityGroupId,
} from './capability-groups.ts' } from './capability-groups.ts'
@ -105,3 +108,47 @@ export function hasSkillsCache(apiUrl: string): boolean {
return false return false
} }
} }
/** One cached skill as shown on the Plugins card. */
export interface CachedSkillSummary {
group: NetxCapabilityGroupId
name: string
files: number
}
function countFiles(dir: string): number {
let n = 0
for (const entry of readdirSync(dir)) {
const full = join(dir, entry)
n += statSync(full).isDirectory() ? countFiles(full) : 1
}
return n
}
/** Skills currently in the cache for `apiUrl` (what the plugin will mount). */
export function listCachedSkills(apiUrl: string): { listedAt: string | null; skills: CachedSkillSummary[] } {
const root = skillsCacheDir(apiUrl)
const skills: CachedSkillSummary[] = []
let listedAt: string | null = null
if (!apiUrl.trim() || !existsSync(root)) return { listedAt, skills }
try {
const meta = JSON.parse(readFileSync(join(root, '.meta.json'), 'utf8')) as { listedAt?: unknown }
if (typeof meta.listedAt === 'string') listedAt = meta.listedAt
} catch {
// no meta yet
}
for (const group of CAPABILITY_GROUP_IDS) {
const groupDir = join(root, SKILL_DIR_BY_GROUP[group])
if (!existsSync(groupDir)) continue
try {
for (const name of readdirSync(groupDir).sort()) {
const dir = join(groupDir, name)
if (!statSync(dir).isDirectory() || !existsSync(join(dir, 'SKILL.md'))) continue
skills.push({ group, name, files: countFiles(dir) })
}
} catch {
// unreadable group dir → skip
}
}
return { listedAt, skills }
}

View file

@ -14,6 +14,7 @@ import { refreshMcpCatalog, resetMcpCatalog } from '../src/netx/mcp-store.ts'
import { publishNetxConnection } from '../src/netx/runtime.ts' import { publishNetxConnection } from '../src/netx/runtime.ts'
import { import {
hasSkillsCache, hasSkillsCache,
listCachedSkills,
replaceSkillsCache, replaceSkillsCache,
skillsCacheDir, skillsCacheDir,
} from '../src/netx/skills-cache.ts' } from '../src/netx/skills-cache.ts'
@ -348,3 +349,19 @@ test('group off → skill not registered', async (t) => {
await registerGroupSkills(ctx, [], 'test') await registerGroupSkills(ctx, [], 'test')
assert.equal(registered.length, 0) assert.equal(registered.length, 0)
}) })
test('listCachedSkills summarizes cached skills per group for the Plugins card', async (t) => {
withTempHome(t)
replaceSkillsCache(API, [
{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL },
{ group: 'ops', skill: 'netx-ops', relPath: 'reference.md', text: 'ref' },
{ group: 'bizMonitor', skill: 'netx-biz-monitor', relPath: 'SKILL.md', text: OPS_SKILL },
])
const { listedAt, skills } = listCachedSkills(API)
assert.ok(listedAt)
assert.deepEqual(skills, [
{ group: 'ops', name: 'netx-ops', files: 2 },
{ group: 'bizMonitor', name: 'netx-biz-monitor', files: 1 },
])
assert.deepEqual(listCachedSkills('').skills, [])
})