mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-19 19:13:20 +08:00
feat(skills): pull group skills from netx /mcp resources, drop bundled copy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
277a61894d
commit
fa82f530e9
31 changed files with 2074 additions and 2027 deletions
|
|
@ -2,16 +2,16 @@
|
|||
"format": 1,
|
||||
"builds": {
|
||||
"lib/agent-tools-biz-monitor.js": {
|
||||
"output": "35b27ce03d1f8fe8b8bf919d1649d0506fd957dbcb1d7856c3991a2781559f11",
|
||||
"output": "11fd312eeb902daab190770bf9465fcbd18ff97e48ca2d4e86144cffa6691790",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-biz-monitor.ts": "201bd447198d5ee7cb1aa25031a3f6054fe12a8a221edd4915752696cca1537d",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -24,26 +24,27 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-common.js": {
|
||||
"output": "529aa5d79d0c40030920a8b8053797f43812bf6d8fa4cb6df7ea7e62a46e4777",
|
||||
"output": "9e40a55c9c823b76c8060aa5f328e89b1411091f1a23841e63505deb2de3e316",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-common.ts": "cf463e940bc2042d7462502de93d45380550da54f1965fa684d72f71b16bc41b",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -56,26 +57,27 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-nms.js": {
|
||||
"output": "75b7a381afdae9932b5d2ec9d4ca6fc63aa0e6da1c6ff1466c57759e6a932528",
|
||||
"output": "126ef9bc22cbe2814442efd68a0ff83ce29fdac7f9baed9602a7a186ef29db0f",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-nms.ts": "6505e51bfb2948756440b00724b19d907d410d34ef1dcbf08019774c0f9d7ef5",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -88,26 +90,27 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-ops.js": {
|
||||
"output": "16151108566b1f3313bfaace216ebb84656704c236b90cb468ba7f559dcd5e96",
|
||||
"output": "56fd1c68147c6d48cca50535f67ce3807dab5f35db4ca9d675fe8db731d9f1d2",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-ops.ts": "2f0331106de53f3dcdd8d61ce2d9da363f0a9b854f1dd7d98e27997c82d98a77",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -120,26 +123,27 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools-topology.js": {
|
||||
"output": "2e3b6268a7e11cab0717faaf4d13bc3ee5a195f1aea94960118211f6773dac28",
|
||||
"output": "7b683d38bbab4764684d88e6f0989177e60789e6b8923438b1c860c75d54fc26",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools-topology.ts": "22bf5f1c71a5b1d33bd3a894bca2c6e4c2fc72072c8a9d7bbc00e214dfafa9f8",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -152,26 +156,27 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
},
|
||||
"lib/agent-tools.js": {
|
||||
"output": "d14c5187ce4ea92c7a9e2a54949c6381c8ad9aede9a78264eeaca82d6618de6a",
|
||||
"output": "e831d2c6a671a9b4bcc2449a0e84772446848372925567704cdb0590a2bb7071",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
"src/agent-tools.ts": "b7e4f8476ba9c5f2754d859074c94129642d18883da293b80e310c09336bc5c5",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||
|
|
@ -184,11 +189,12 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
|
||||
}
|
||||
|
|
@ -196,7 +202,7 @@
|
|||
"lib/client.js": {
|
||||
"output": "16f3328f3d747ebcfc02accbc09419868db10618569fab7493e3875ada9d6b45",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
|
|
@ -214,7 +220,7 @@
|
|||
"src/client/snapshot-store.ts": "25fdcae95490a9d7f1f520ec90981cefb4fdd576b340717e9087f1ca82c6563d",
|
||||
"src/client/styles.ts": "db3cbdbd00e747baa963af6436a532ff4214ce379c162a84dadf837dbaa15de2",
|
||||
"src/client/tools-status-view.ts": "4ff296641164124748356a24e0d38ec88f09ee9826bf1a2ab51159026a2b2ca8",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
||||
|
|
@ -225,9 +231,9 @@
|
|||
}
|
||||
},
|
||||
"lib/index.js": {
|
||||
"output": "ceb3cb1b0e366d086fdca112904a73d8dbb0c9926064bbe13e8602e86267307d",
|
||||
"output": "b3712d7ccff953a8fa1ae0b0339ab3b247d65ce21d4690817d15c168d2876275",
|
||||
"inputs": {
|
||||
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
|
||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||
|
|
@ -238,9 +244,9 @@
|
|||
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
|
||||
"src/netx/alarm-push.ts": "96f3fca6a7fd50024a1d233ff43984fc0ff77261ce873b69396eb7ed5344f473",
|
||||
"src/netx/alarm-session.ts": "0df97efc3debb262573c19ade84d9d50584d58b21fa2c5ae5badd382915f18e3",
|
||||
"src/netx/capability-groups.ts": "d91046451e2c2d7968a4cae98dccc0a86c1b77c7855542d4edf604833db385a0",
|
||||
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
|
||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||
"src/netx/group-skills.ts": "e870e984569b7e3f5c05d50a1e489ddf4e222a738354df93a6a860bb35d7152c",
|
||||
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac",
|
||||
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
|
||||
"src/netx/heal-profile-entry.ts": "18c2b635f727bf41d38c7c38fcc0ce0c5391c9d98b2d662db354f1908defb6be",
|
||||
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439",
|
||||
|
|
@ -256,15 +262,16 @@
|
|||
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
|
||||
"src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843",
|
||||
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f",
|
||||
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
|
||||
"src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3",
|
||||
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32",
|
||||
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
|
||||
"src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e",
|
||||
"src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214",
|
||||
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
|
||||
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
|
||||
"src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6",
|
||||
"src/netx/skills-cache.ts": "3b5c01d3f2b914ca1275b7eb7b29a85738f05295250667c66c27a00650300409",
|
||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139",
|
||||
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ bun run release:pack
|
|||
|
||||
Development requires Node.js 22, pnpm 11.7 and Bun 1.3.13 or later. Building and package installation tests use only public development dependencies. Full Harness integration tests and type checking additionally need a local Harness runtime; see [maintainer instructions](docs/RELEASING.md).
|
||||
|
||||
Release checks detect stale or missing bundles, undeclared runtime dependencies, invalid metadata and missing skills. Tests install the actual npm tarball offline in an isolated profile. CI repeats the package checks on Windows, macOS and Linux.
|
||||
Release checks detect stale or missing bundles, undeclared runtime dependencies and invalid metadata. Tests install the actual npm tarball offline in an isolated profile. CI repeats the package checks on Windows, macOS and Linux.
|
||||
|
||||
Skills are maintained in the sibling `netx/skills` repository and copied into this package before release. Published installations use the bundled skill files; a source checkout can use the sibling repository or an explicit `NETX_SKILLS_ROOT`.
|
||||
Group skills (ops / topology / biz-monitor) are published by netx over `/mcp` `resources` and cached under `$DSH_HOME/netxops-mcp-skills/`. This package does not ship skill files. Operator-subset KB pack skills remain local (see [Knowledge base](docs/KNOWLEDGE_BASE.md)).
|
||||
|
||||
Implementation and UI validation details: [Chinese review](docs/PUBLICATION-REVIEW.zh.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
| Source (oclaw) | Destination |
|
||||
|----------------|-------------|
|
||||
| `runtime/workspaces/ops/ROLE_SYSTEM.md` | `presets/netxops/PERSONA.md` + persona in `agent.cordis.yml` (brand **Netx Ops**) |
|
||||
| `skills/_workspace/ops/ops-netx-ume-playbook/` + `ops-netx-managed-ne-playbook/` | **Canonical:** `netx/skills/ops/netx-ops/` (merged ops group) |
|
||||
| `skills/.../netx-topology` (MCP) | **Canonical:** `netx/skills/topology/netx-topology/` |
|
||||
| `skills/_workspace/ops/ops-netx-ume-playbook/` + `ops-netx-managed-ne-playbook/` | **Canonical:** `netx/skills/ops/netx-ops/` via netx `/mcp` resources |
|
||||
| `skills/.../netx-topology` (MCP) | **Canonical:** `netx/skills/topology/netx-topology/` via netx `/mcp` resources |
|
||||
| netx MCP HTTP surface | Host plugin `src/netx/*` + `src/index.ts`: settings namespace `netxops` + credential `NETX_API_TOKEN` → native `netx__*` tools |
|
||||
|
||||
Adaptations: removed oclaw Admin / WhatsApp / `ume_alarm_xlsx_report` / wiki capture / skill_auto_install; fiber/offline recipes use Raw/aggregate tools only. DSH no longer spawns `python -m netx_mcp`. Model-facing NMS tools renamed to `*Nms*`; ZTE UME remains the `nmsProvider=zte-ume` adapter.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Public release checklist
|
||||
|
||||
The public package ships precompiled JS, preset overlays, skills, metadata and user documentation. It has no install/prepare/prepack build hook and no third-party runtime dependencies. Optional peer packages are supplied by Harness. Publish a tested tarball rather than asking users to build or link the repository.
|
||||
The public package ships precompiled JS, preset overlays, metadata and user documentation. Group skills come from netx `/mcp` at runtime (not bundled). It has no install/prepare/prepack build hook and no third-party runtime dependencies. Optional peer packages are supplied by Harness. Publish a tested tarball rather than asking users to build or link the repository.
|
||||
|
||||
## Prepare the source
|
||||
|
||||
Use Node.js 22, pnpm 11.7 and Bun >=1.3.13. Update `package.json.version`, synchronize canonical skills if changed, then run:
|
||||
Use Node.js 22, pnpm 11.7 and Bun >=1.3.13. Update `package.json.version`, then run:
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
|
@ -29,7 +29,7 @@ bun test test tests
|
|||
|
||||
Use `web` when testing the Web runtime. Full tests include the current official volatile Schema implementation, real Cordis scopes, publication changes, UI settings transport, preset preservation and fresh commented profile patches.
|
||||
|
||||
`test:release` creates the actual npm tarball and installs it offline into a temporary profile with spaces and Chinese characters. Lifecycle scripts are allowed in this test, proving normal installation needs no build. It verifies all exports, client registration, bundled skills and metadata, and excludes source, tests, local config and developer scripts. When Harness is installed locally, it also activates that installed copy using actual Host peers and verifies tool/skill mounting and clean unload. The fixture never edits the user's profile or credentials and never calls a Netx production API.
|
||||
`test:release` creates the actual npm tarball and installs it offline into a temporary profile with spaces and Chinese characters. Lifecycle scripts are allowed in this test, proving normal installation needs no build. It verifies all exports, client registration and metadata, and excludes source, tests, local config and developer scripts. When Harness is installed locally, it also activates that installed copy using actual Host peers and verifies tool mounting (plus skills from a seeded MCP skills cache) and clean unload. The fixture never edits the user's profile or credentials and never calls a Netx production API.
|
||||
|
||||
CI runs package checks, builds and offline installation on Windows, macOS and Linux. CI without Harness checks package installation and client registration; Host activation is covered by the local integration suite. Wait for the platform jobs to pass before publishing.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Tools are discovered from netx **`/mcp`** (`tools/list`) and registered into DSH as `netx__<stem>`.
|
||||
|
||||
**One group ↔ one skill.** Group membership comes from `_meta.netx.group` on each MCP tool — there is no hardcoded catalog in the plugin.
|
||||
**One group ↔ one skill.** Group membership comes from `_meta.netx.group` on each MCP tool — there is no hardcoded catalog in the plugin. Skill markdown is pulled from MCP `resources` (`netx://skills/...`) into `$DSH_HOME/netxops-mcp-skills/` when the tool catalog refreshes.
|
||||
|
||||
| Group | Skill | Default in Ops preset |
|
||||
|-------|-------|------------------------|
|
||||
|
|
@ -24,4 +24,4 @@ Authorization, CLI show/display guards, and RBAC stay on the netx `/mcp` server.
|
|||
|
||||
## Offline
|
||||
|
||||
When `/mcp` is unreachable, the Plugins card shows the last cached tools/list and a disconnected hint. Registration uses that cache so toggles still work until connect recovers.
|
||||
When `/mcp` is unreachable, the Plugins card shows the last cached tools/list and a disconnected hint. Registration uses that in-memory tool cache so toggles still work until connect recovers. Skill files stay on disk from the last successful resources pull and are still registered after a plugin restart.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync6 } from "node:fs";
|
||||
import { dirname as dirname6, join as join10 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
|
||||
// src/netx/config-values.ts
|
||||
var volatileWrite = Symbol.for("cosmokit.volatile.write");
|
||||
|
|
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
constructor(message, status) {
|
||||
|
|
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
|
|||
`) || fallback;
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join(homedir(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
|
||||
// src/netx/mcp-store.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
|
||||
var EMPTY = Object.freeze({
|
||||
|
|
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
|
||||
const siblingCandidates = [
|
||||
join(here, "..", "..", "..", "netx", "skills"),
|
||||
join(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
import { join as join2 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join(here, "..", "presets", "netxops", "skills"),
|
||||
join(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -371,7 +431,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join(dir, "SKILL.md");
|
||||
const skillPath = join2(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
const full = join2(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
const skillPath = join2(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
|
|||
const paths = { skills: "_skills", ...snapshot.paths };
|
||||
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||
const absolute = resolve(snapshot.realRoot, rel);
|
||||
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||
return { key, relative: rel, absolute, exists: existsSync(absolute) };
|
||||
});
|
||||
}
|
||||
function renderKbPathTable(snapshot) {
|
||||
|
|
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
|
|||
}
|
||||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
||||
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
}
|
||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||
try {
|
||||
if (existsSync3(checked.absolutePath)) {
|
||||
if (existsSync2(checked.absolutePath)) {
|
||||
const realFile = realpathSync(checked.absolutePath);
|
||||
const realLocal = realpathSync(localRoot);
|
||||
const realRel = relative(realLocal, realFile);
|
||||
|
|
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join3("suggestions", kind);
|
||||
return join4("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||
return join4("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -722,9 +797,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join3("refs", "devices", host);
|
||||
return join4("refs", "devices", host);
|
||||
}
|
||||
return join3("refs", area);
|
||||
return join4("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join4(localRoot, "identity");
|
||||
const dir = join5(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
|
|
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync3(full).isFile())
|
||||
|
|
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
|
||||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync4,
|
||||
existsSync as existsSync3,
|
||||
mkdirSync,
|
||||
readdirSync as readdirSync3,
|
||||
readFileSync as readFileSync3,
|
||||
|
|
@ -883,7 +958,7 @@ import {
|
|||
statSync as statSync4,
|
||||
writeFileSync
|
||||
} from "node:fs";
|
||||
import { dirname as dirname3, join as join5 } from "node:path";
|
||||
import { dirname as dirname3, join as join6 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -929,11 +1004,11 @@ function createMemory(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -948,11 +1023,11 @@ function createDraft(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -965,11 +1040,11 @@ function createSuggestion(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -984,11 +1059,11 @@ function createRef(snapshot, args) {
|
|||
const localRoot = requireLocalRoot(snapshot);
|
||||
const name = refFileName(args.slug);
|
||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync4(target.absolutePath);
|
||||
const existed = existsSync3(target.absolutePath);
|
||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
|
|
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
|
|||
function updateLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
|
|
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
|
|||
function deleteLocalFile(snapshot, args) {
|
||||
const localRoot = requireLocalRoot(snapshot);
|
||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||
if (!existsSync4(target.absolutePath)) {
|
||||
if (!existsSync3(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
|
|
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
|
|||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join5(dir, name);
|
||||
const full = join6(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync4(full);
|
||||
|
|
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||
walkFiles(join6(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { extname, join as join7, 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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
|
|||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync5(join6(target.absolutePath, entry.name)).size;
|
||||
size = statSync5(join7(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
|
|||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join6(dir, entry.name);
|
||||
const full = join7(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store2() {
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -1331,7 +1406,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
|
|||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync2(dirname4(path), { recursive: true });
|
||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join8(snapshot.realRoot, rel);
|
||||
return join9(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join8(root, entry);
|
||||
const full = join9(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/http.ts
|
||||
function normalizeApiUrl(raw) {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed)
|
||||
return { ok: false, error: "empty" };
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return { ok: false, error: "unsupported_scheme" };
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
return { ok: false, error: "credentials_in_url" };
|
||||
}
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
|
||||
return { ok: true, url: `${parsed.origin}${path}${search}` };
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync5 } from "node:fs";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-schema.ts
|
||||
|
|
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname5(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
|
|||
// src/netx/group-tools-plugin.ts
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
482
lib/index.js
482
lib/index.js
|
|
@ -3,8 +3,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|||
|
||||
// src/index.ts
|
||||
import { readFileSync as readFileSync9 } from "node:fs";
|
||||
import { dirname as dirname8, join as join13 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
||||
import { dirname as dirname8, join as join14 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
||||
import * as DshSettings from "@deepseek-ai/dsh-settings";
|
||||
|
|
@ -1328,34 +1328,129 @@ function groupsForPreset(connection, presetId, only) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync as existsSync3 } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname as dirname2, join as join3 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync3(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname2(fileURLToPath2(import.meta.url));
|
||||
const checkout = [join3(here, "..", ".git"), join3(here, "..", "..", ".git")].some(existsSync3);
|
||||
const siblingCandidates = [
|
||||
join3(here, "..", "..", "..", "netx", "skills"),
|
||||
join3(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync3(candidate))
|
||||
return candidate;
|
||||
import { join as join4 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join3(here, "..", "presets", "netxops", "skills"),
|
||||
join3(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync3(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store2();
|
||||
if (JSON.stringify(state.connection) === JSON.stringify(next))
|
||||
return;
|
||||
state.connection = next;
|
||||
for (const listener of [...state.listeners]) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error("netxops: connection subscriber failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash as createHash2 } from "node:crypto";
|
||||
import {
|
||||
existsSync as existsSync3,
|
||||
mkdirSync as mkdirSync2,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync as writeFileSync3
|
||||
} from "node:fs";
|
||||
import { homedir as homedir3 } from "node:os";
|
||||
import { dirname as dirname2, join as join3 } from "node:path";
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join3(homedir3(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash2("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join3(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
function isSafeSegment(value) {
|
||||
return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00");
|
||||
}
|
||||
function isSafeRelPath(relPath) {
|
||||
if (!relPath || relPath.startsWith("/") || relPath.includes("\x00"))
|
||||
return false;
|
||||
const parts = relPath.replace(/\\/g, "/").split("/");
|
||||
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
||||
}
|
||||
function replaceSkillsCache(apiUrl, files) {
|
||||
const root = skillsCacheDir(apiUrl);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
mkdirSync2(root, { recursive: true });
|
||||
let written = 0;
|
||||
for (const file of files) {
|
||||
const groupDir = SKILL_DIR_BY_GROUP[file.group];
|
||||
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath))
|
||||
continue;
|
||||
const rel = file.relPath.replace(/\\/g, "/");
|
||||
const dest = join3(root, groupDir, file.skill, ...rel.split("/"));
|
||||
mkdirSync2(dirname2(dest), { recursive: true });
|
||||
const text = file.text.replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
writeFileSync3(dest, text, "utf8");
|
||||
written += 1;
|
||||
}
|
||||
const normalized = normalizeSkillsApiUrl(apiUrl);
|
||||
const meta = {
|
||||
apiUrl: normalized,
|
||||
mcpUrl: normalized ? `${normalized}/mcp` : "",
|
||||
listedAt: new Date().toISOString(),
|
||||
fileCount: written
|
||||
};
|
||||
writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)}
|
||||
`, "utf8");
|
||||
return root;
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -1402,7 +1497,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join3(dir, "SKILL.md");
|
||||
const skillPath = join4(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -1425,7 +1520,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join3(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join4(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -1435,7 +1530,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join3(groupDir, entry);
|
||||
const full = join4(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -1444,7 +1539,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join3(full, "SKILL.md");
|
||||
const skillPath = join4(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -1491,6 +1586,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -1498,10 +1602,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -1513,7 +1623,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join3(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join4(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -1549,12 +1659,12 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
|
||||
import { join as join6 } from "node:path";
|
||||
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
|
||||
import { join as join7 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
||||
import { basename, dirname as dirname3, join as join4, resolve } from "node:path";
|
||||
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
||||
import { basename, dirname as dirname3, join as join5, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -1607,7 +1717,7 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
if (!rootStat.isDirectory()) {
|
||||
return { paths: [], error: `kbRoot is not a directory: ${root}` };
|
||||
}
|
||||
const direct = join4(root, "MANIFEST.json");
|
||||
const direct = join5(root, "MANIFEST.json");
|
||||
if (existsSync4(direct)) {
|
||||
try {
|
||||
if (statSync2(direct).isFile())
|
||||
|
|
@ -1618,7 +1728,7 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
const walk = (dir, depth) => {
|
||||
if (depth > maxDepth)
|
||||
return;
|
||||
const candidate = join4(dir, "MANIFEST.json");
|
||||
const candidate = join5(dir, "MANIFEST.json");
|
||||
if (existsSync4(candidate)) {
|
||||
try {
|
||||
if (statSync2(candidate).isFile())
|
||||
|
|
@ -1629,14 +1739,14 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
return;
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
entries = readdirSync2(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync2(full);
|
||||
|
|
@ -1649,14 +1759,14 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
};
|
||||
let topEntries;
|
||||
try {
|
||||
topEntries = readdirSync(root);
|
||||
topEntries = readdirSync2(root);
|
||||
} catch {
|
||||
return { paths: [], error: `cannot read kbRoot: ${root}` };
|
||||
}
|
||||
for (const name of topEntries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join4(root, name);
|
||||
const full = join5(root, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync2(full);
|
||||
|
|
@ -1817,7 +1927,7 @@ function renderKbPathTable(snapshot) {
|
|||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync5, realpathSync, statSync as statSync3 } from "node:fs";
|
||||
import { join as join5, 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([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -1953,16 +2063,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join5("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join6("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join5("suggestions", kind);
|
||||
return join6("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join5("drafts", sanitizeSlug(d, "misc"));
|
||||
return join6("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -1973,9 +2083,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join5("refs", "devices", host);
|
||||
return join6("refs", "devices", host);
|
||||
}
|
||||
return join5("refs", area);
|
||||
return join6("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -1986,10 +2096,10 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join6(localRoot, "identity");
|
||||
const dir = join7(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -1998,7 +2108,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join6(dir, name);
|
||||
const full = join7(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync4(full).isFile())
|
||||
|
|
@ -2127,14 +2237,14 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync6,
|
||||
mkdirSync as mkdirSync2,
|
||||
readdirSync as readdirSync3,
|
||||
mkdirSync as mkdirSync3,
|
||||
readdirSync as readdirSync4,
|
||||
readFileSync as readFileSync5,
|
||||
rmSync,
|
||||
rmSync as rmSync2,
|
||||
statSync as statSync5,
|
||||
writeFileSync as writeFileSync3
|
||||
writeFileSync as writeFileSync4
|
||||
} from "node:fs";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -2183,9 +2293,9 @@ function createMemory(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2202,9 +2312,9 @@ function createDraft(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
writeFileSync4(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
action: existed ? "updated" : "created",
|
||||
|
|
@ -2219,9 +2329,9 @@ function createSuggestion(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2238,9 +2348,9 @@ function createRef(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2259,7 +2369,7 @@ function updateLocalFile(snapshot, args) {
|
|||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`;
|
||||
writeFileSync3(target.absolutePath, body, "utf8");
|
||||
writeFileSync4(target.absolutePath, body, "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
action: "updated",
|
||||
|
|
@ -2273,7 +2383,7 @@ function deleteLocalFile(snapshot, args) {
|
|||
if (!existsSync6(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
rmSync2(target.absolutePath, { force: false });
|
||||
return {
|
||||
ok: true,
|
||||
action: "deleted",
|
||||
|
|
@ -2284,14 +2394,14 @@ function deleteLocalFile(snapshot, args) {
|
|||
function walkFiles(dir, baseLocal, out) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync3(dir);
|
||||
entries = readdirSync4(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join7(dir, name);
|
||||
const full = join8(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync5(full);
|
||||
|
|
@ -2316,7 +2426,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join7(localRoot, r), localRoot, entries);
|
||||
walkFiles(join8(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -2328,8 +2438,8 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
}
|
||||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
||||
import { extname, join as join8, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
||||
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 SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -2375,12 +2485,12 @@ function readKbFile(snapshot, args) {
|
|||
const target = resolveKbReadPath(snapshot, args.path);
|
||||
const st = statSync6(target.absolutePath);
|
||||
if (st.isDirectory()) {
|
||||
const entries = readdirSync4(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())
|
||||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync6(join8(target.absolutePath, entry.name)).size;
|
||||
size = statSync6(join9(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -2449,14 +2559,14 @@ function cachedText(path, mtimeMs, size) {
|
|||
function walkTextFiles(dir, out) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync4(dir, { withFileTypes: true });
|
||||
entries = readdirSync5(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join8(dir, entry.name);
|
||||
const full = join9(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -2563,7 +2673,7 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
var ENV_KEYS = [
|
||||
"KB_ROOT",
|
||||
"KB_LOCAL",
|
||||
|
|
@ -2574,17 +2684,17 @@ var ENV_KEYS = [
|
|||
"KB_PATHS",
|
||||
"KB_STATUS"
|
||||
];
|
||||
function store2() {
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -2592,7 +2702,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function publishKbContext(next, options = {}) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
if (!options.force && JSON.stringify(state.snapshot) === JSON.stringify(next))
|
||||
return;
|
||||
state.snapshot = {
|
||||
|
|
@ -2609,7 +2719,7 @@ function publishKbContext(next, options = {}) {
|
|||
}
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -2656,16 +2766,16 @@ function toLosslessJson(value) {
|
|||
}
|
||||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
||||
import { homedir as homedir3 } from "node:os";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
||||
import { homedir as homedir4 } from "node:os";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -2674,7 +2784,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -2682,22 +2792,22 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join9(process.env.DSH_HOME?.trim() || join9(homedir3(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync3(dirname5(path), { recursive: true });
|
||||
writeFileSync4(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
const path = join10(process.env.DSH_HOME?.trim() || join10(homedir4(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync4(dirname5(path), { recursive: true });
|
||||
writeFileSync5(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
function resetToolMountStatus() {
|
||||
store3().mounts.clear();
|
||||
store4().mounts.clear();
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -2894,13 +3004,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join10 } from "node:path";
|
||||
import { join as join11 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join10(snapshot.realRoot, rel);
|
||||
return join11(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -2922,7 +3032,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join10(snapshot.realRoot, rel);
|
||||
return join11(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -2941,7 +3051,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join10(root, entry);
|
||||
const full = join11(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -3160,64 +3270,10 @@ function replyReminder(setting) {
|
|||
].join(" ");
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store4();
|
||||
if (JSON.stringify(state.connection) === JSON.stringify(next))
|
||||
return;
|
||||
state.connection = next;
|
||||
for (const listener of [...state.listeners]) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error("netxops: connection subscriber failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/session-export.ts
|
||||
import { homedir as homedir4, hostname as osHostname } from "node:os";
|
||||
import { homedir as homedir5, hostname as osHostname } from "node:os";
|
||||
|
||||
// node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
||||
// ../../netxops/node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
||||
import { createRequire as createRequire3 } from "module";
|
||||
var require2 = createRequire3("/");
|
||||
var _a;
|
||||
|
|
@ -4485,8 +4541,8 @@ function mountNetxopsWebRoute(ctx, channel, handler) {
|
|||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync7 } from "node:fs";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join12 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-types.ts
|
||||
|
|
@ -4563,6 +4619,7 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
var SKILLS_URI_PREFIX = "netx://skills/";
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
|
|
@ -4594,9 +4651,46 @@ function parseTool(raw) {
|
|||
const readOnlyHint = annotations.readOnlyHint === true;
|
||||
const meta = row._meta && typeof row._meta === "object" && !Array.isArray(row._meta) ? row._meta : {};
|
||||
const netx = meta.netx && typeof meta.netx === "object" && !Array.isArray(meta.netx) ? meta.netx : {};
|
||||
if (netx.group === "meta")
|
||||
return null;
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined;
|
||||
return { name, description, inputSchema, readOnlyHint, group };
|
||||
}
|
||||
function readMetaNetx(row) {
|
||||
const meta = row._meta ?? row.meta;
|
||||
if (!meta || typeof meta !== "object" || Array.isArray(meta))
|
||||
return {};
|
||||
const netx = meta.netx;
|
||||
if (!netx || typeof netx !== "object" || Array.isArray(netx))
|
||||
return {};
|
||||
return netx;
|
||||
}
|
||||
function parseSkillResource(resource, text) {
|
||||
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
||||
return null;
|
||||
const row = resource;
|
||||
const uri = typeof row.uri === "string" ? row.uri.trim() : "";
|
||||
const netx = readMetaNetx(row);
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined;
|
||||
const skill = typeof netx.skill === "string" ? netx.skill.trim() : "";
|
||||
if (!group || !skill)
|
||||
return null;
|
||||
let relPath = typeof row.name === "string" ? row.name.trim().replace(/\\/g, "/") : "";
|
||||
if (!relPath && uri.startsWith(SKILLS_URI_PREFIX)) {
|
||||
const rest = uri.slice(SKILLS_URI_PREFIX.length);
|
||||
const parts = rest.split("/");
|
||||
if (parts.length >= 3)
|
||||
relPath = parts.slice(2).join("/");
|
||||
}
|
||||
if (!relPath || relPath.includes(".."))
|
||||
return null;
|
||||
return { group, skill, relPath, text };
|
||||
}
|
||||
function resourcesCapability(capabilities) {
|
||||
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities))
|
||||
return false;
|
||||
return "resources" in capabilities;
|
||||
}
|
||||
function parseMcpResponseBody(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").trim();
|
||||
if (!text)
|
||||
|
|
@ -4692,24 +4786,57 @@ async function postRpc(options, method, params, notification = false) {
|
|||
}
|
||||
return asRpcResult(parseMcpResponseBody(responseText));
|
||||
}
|
||||
async function listMcpTools(options) {
|
||||
async function fetchMcpCatalog(options) {
|
||||
if (!options.mcpUrl)
|
||||
throw new McpRemoteError("mcp url is empty");
|
||||
await postRpc(options, "initialize", {
|
||||
const init = await postRpc(options, "initialize", {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: CLIENT_INFO
|
||||
});
|
||||
await postRpc(options, "notifications/initialized", {}, true);
|
||||
const result = await postRpc(options, "tools/list", {});
|
||||
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
||||
const out = [];
|
||||
for (const item of tools) {
|
||||
const toolsResult = await postRpc(options, "tools/list", {});
|
||||
const toolsRaw = Array.isArray(toolsResult?.tools) ? toolsResult.tools : [];
|
||||
const tools = [];
|
||||
for (const item of toolsRaw) {
|
||||
const parsed = parseTool(item);
|
||||
if (parsed)
|
||||
out.push(parsed);
|
||||
tools.push(parsed);
|
||||
}
|
||||
if (!resourcesCapability(init?.capabilities)) {
|
||||
return { tools, skills: [] };
|
||||
}
|
||||
try {
|
||||
const listed = await postRpc(options, "resources/list", {});
|
||||
const resources = Array.isArray(listed?.resources) ? listed.resources : [];
|
||||
const skills = [];
|
||||
for (const resource of resources) {
|
||||
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
||||
continue;
|
||||
const uri = typeof resource.uri === "string" ? resource.uri.trim() : "";
|
||||
if (!uri.startsWith(SKILLS_URI_PREFIX))
|
||||
continue;
|
||||
const read = await postRpc(options, "resources/read", { uri });
|
||||
const contents = Array.isArray(read?.contents) ? read.contents : [];
|
||||
let text = "";
|
||||
for (const block of contents) {
|
||||
if (!block || typeof block !== "object" || Array.isArray(block))
|
||||
continue;
|
||||
const row = block;
|
||||
if (typeof row.text === "string") {
|
||||
text = row.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const parsed = parseSkillResource(resource, text);
|
||||
if (parsed)
|
||||
skills.push(parsed);
|
||||
}
|
||||
return { tools, skills };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { tools, skillsError: message };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
async function callMcpTool(options, name, args) {
|
||||
if (!options.mcpUrl)
|
||||
|
|
@ -4912,7 +5039,7 @@ async function refreshMcpCatalog(input) {
|
|||
fromCache: previous.tools.length > 0
|
||||
});
|
||||
try {
|
||||
const tools = await listMcpTools({
|
||||
const fetched = await fetchMcpCatalog({
|
||||
mcpUrl,
|
||||
token: input.token,
|
||||
lang: input.lang,
|
||||
|
|
@ -4922,10 +5049,15 @@ async function refreshMcpCatalog(input) {
|
|||
});
|
||||
if (gen !== store5().generation)
|
||||
return store5().state;
|
||||
if (fetched.skills !== undefined) {
|
||||
replaceSkillsCache(input.apiUrl, fetched.skills);
|
||||
} else {
|
||||
console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError);
|
||||
}
|
||||
const next = {
|
||||
phase: "connected",
|
||||
mcpUrl,
|
||||
tools: Object.freeze([...tools]),
|
||||
tools: Object.freeze([...fetched.tools]),
|
||||
error: "",
|
||||
listedAt: new Date().toISOString(),
|
||||
fromCache: false
|
||||
|
|
@ -4952,8 +5084,8 @@ async function refreshMcpCatalog(input) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync7(join12(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -5086,12 +5218,12 @@ import { createScope, scopeOf } from "@deepseek-ai/dsh-scope";
|
|||
|
||||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync8 } from "node:fs";
|
||||
import { dirname as dirname7, join as join12 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
||||
import { dirname as dirname7, join as join13 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname7(fileURLToPath4(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync8(join12(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname7(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync8(join13(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -5417,8 +5549,8 @@ function resolveApiUrlForConnection(value) {
|
|||
}
|
||||
function packageVersion3() {
|
||||
try {
|
||||
const here = dirname8(fileURLToPath5(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync9(join13(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname8(fileURLToPath4(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync9(join14(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@
|
|||
"lib/client.js",
|
||||
"presets/netxops/PERSONA.md",
|
||||
"presets/netxops/preset.yml",
|
||||
"presets/netxops/skills/",
|
||||
"cordis.patch.yml",
|
||||
"cordis.bundle.patch.yml",
|
||||
"docs/",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# 1. replaces `persona` with presets/netxops/PERSONA.md
|
||||
# 2. ensures the `netxops-tools` row below is present
|
||||
# 3. aligns workflow engine package to what the host can resolve
|
||||
# 4. copies presets/netxops/skills/
|
||||
# Group skills are pulled from netx `/mcp` resources into `$DSH_HOME/netxops-mcp-skills/`.
|
||||
# Desktop also syncs a declarative `@deepseek-ai/dsh-agent-preset` row into
|
||||
# the profile patch. DSH has no preset inheritance.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
# Mirrored from netx/skills 鈥?do not long-edit here
|
||||
|
||||
Canonical: `D:\project\chatgpt\netx\skills`
|
||||
|
||||
`powershell
|
||||
powershell -File .\scripts\sync-skills-from-netx.ps1
|
||||
`
|
||||
|
||||
Runtime prefers `NETX_SKILLS_ROOT` or sibling `../netx/skills`.
|
||||
Groups: **ops** (`netx-ops`) + **topology** (`netx-topology`). One group 鈫?one skill.
|
||||
|
||||
**Keep netx-ops pure netx** (alarms / inventory / CLI). Knowledge-base triage belongs in
|
||||
operator-subset `_skills/kb-*` packs registered by the plugin 鈥?do **not** write KB linkage
|
||||
back into `netx-ops`.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
---
|
||||
name: netx-biz-monitor
|
||||
description: >-
|
||||
Overnight cutover / biz_state monitor analysis via netxops host tools
|
||||
(netx__listBizMonitors / listBizMonitorBatches / getBizMonitor*). List
|
||||
projects/tasks then batches when no id is given; read definitions, board,
|
||||
reds/diffs with evidence (device-raw A/B, show commands), and judge tool
|
||||
false-positive vs real business fault. Does not create templates/tasks.
|
||||
---
|
||||
|
||||
# netx-biz-monitor(割接 / 业务监控只读分析)
|
||||
|
||||
人配好监控与任务;AI 当晚做归因。
|
||||
工具在 **netxops**(`netx__*`),直连 netx REST,**不是** netx-mcp。
|
||||
|
||||
## 工具(能力组 `bizMonitor`)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `netx__listBizMonitors` | **入口目录**:列出割接/监控对比项目 + 业务监控任务(**无需** project_id) |
|
||||
| `netx__listBizMonitorBatches` | **批次目录**:`project_id`→割接批次;`task_id`→采集批次;`kind=runs`→evaluate run |
|
||||
| `netx__getBizMonitorContext` | 任务定义:项目、监控/对比模板、归一化规则、端口映射、关联任务与命令项 |
|
||||
| `netx__getBizMonitorBoard` | 批次进度、evaluate run、sheet 卡、verdict 统计 |
|
||||
| `netx__listBizMonitorReds` | 红单 + **evidence** |
|
||||
| `netx__getBizMonitorDiffs` | run 明细(可滤 color=red) |
|
||||
| `netx__getBizCollectBatch` | 某次采集:commands / metrics |
|
||||
| `netx__getBizCollectCommandRaw` | 单命令全量 show 输出 |
|
||||
|
||||
交叉验证真障时再用 ops:`netx__execManagedNe` / 拓扑路径。
|
||||
|
||||
## 字段约定(必守)
|
||||
|
||||
- **`old_key` / `new_key`**:设备采集原文 **A/B**(大屏与结论用这个)
|
||||
- **`match_*` / evidence.iface.normalized|map_***:内部配对,**不要**把映射后的 BB 说成两侧端口
|
||||
- **evidence.*.command.raw_command**:实际 show 命令
|
||||
- **evidence.*.device / collect**:哪台设备、何时采的
|
||||
|
||||
## 推荐顺序
|
||||
|
||||
1. 无 id → `listBizMonitors`(`kind=projects|tasks|all`,可加 `q` / `purpose`)
|
||||
2. `listBizMonitorBatches(project_id)` 或 `(task_id)` — 拿到 **完整 batch_id**;需要 run 时再 `kind=runs` / `include_runs`
|
||||
3. `getBizMonitorContext(project_id)` — 弄清盯什么
|
||||
4. `getBizMonitorBoard(batch_id)` — 进度 / missing / skipped
|
||||
5. `listBizMonitorReds` 或 `getBizMonitorDiffs(color=red)`
|
||||
6. 看 evidence:parse 失败?current_missing?映射 miss?归一化误伤?
|
||||
7. 需要原文 → `getBizCollectCommandRaw`
|
||||
8. 像真障 → `execManagedNe` 现场核对
|
||||
|
||||
## Verdict 速查(skill 解释,工具不硬编码)
|
||||
|
||||
| verdict | 含义(窗口内) |
|
||||
|---------|----------------|
|
||||
| migrated | expect 项已迁到新侧 |
|
||||
| anomaly | 异常(多为红) |
|
||||
| unfinished | expect 未完成(验收常红) |
|
||||
| migrating / expected / ok | 过程态 / 正常 |
|
||||
| lost / not_involved | 丢失或无关 |
|
||||
|
||||
## 误报优先查
|
||||
|
||||
1. `parse_status` 非 ok / 行数为 0
|
||||
2. board:`current_missing` / `collect_skipped`
|
||||
3. 端口映射漏配、归一化前后不一致导致假 anomaly
|
||||
4. 再谈真实业务问题
|
||||
|
||||
配置整改与关红单由人做;本 skill **只读分析**。
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
---
|
||||
name: netx-ops
|
||||
description: netx 运维:告警、网元、只读 CLI。
|
||||
---
|
||||
|
||||
# netx-ops(告警 · 清单 · 纳管登录)
|
||||
|
||||
处理 netx **告警 / 网元清单 / 只读 CLI** 时遵循本手册。
|
||||
画布布图不在本 skill(见 **netx-topology**)。本手册里的「路径」仅指 `findTopologyPaths` 查两端关联。
|
||||
|
||||
**工具名**:DSH 为 `netx__` + stem(如 `netx__queryNmsAlarms`)。下文写 stem 时,调用一律加 `netx__` 前缀。
|
||||
|
||||
**两套「一批」(勿混):**
|
||||
|
||||
| 说法 | 含义 | 写法 |
|
||||
|------|------|------|
|
||||
| **多台一批** | 一次工具调用打多台设备 | `nms_ne_ids`/`ne_ids` + 共享 `commands`,或 `targets` |
|
||||
| **同台多条** | 一次工具调用、**一台**上发多条命令 | 单个 `ne_id`/`nms_ne_id` + `commands:[…]` |
|
||||
|
||||
禁止:同轮对多台各调一次 `execManagedNe`(假并行、会串行)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总原则
|
||||
|
||||
1. **先新鲜度,再结论**:`runNmsDiagnostics` 或 `aggregateNmsAlarms` → 读 `meta.last_seen_*`。过旧则按快照:时间窗落在 min~max 内,禁止默认「现在−30 分钟」。
|
||||
2. **证据优先**:有告警须有级别计数和/或 Top `host_name`;否则写「证据不足」,禁止臆造根因。
|
||||
3. **对用户**:只展示 **host_name**;**严禁**出现任何 UUID/`ne_id`。无 `host_name` 的行当垃圾丢弃(不顶替 label、不提「缺失」)。工具入参仍可用 id,但不得写入对用户可见正文。
|
||||
4. **短问**:能 ≤3 次工具调用就闭环的,先聚合/过滤,禁止无过滤翻全库。复杂关联(dying gasp、A<>B CLI)不受 3 次硬顶,但仍须有过滤与目标。
|
||||
5. **「能否登录」**:必须走 §3;禁止只查 inventory 下结论。
|
||||
|
||||
---
|
||||
|
||||
## 2. 工具选用(按需,非逐步必跑)
|
||||
|
||||
按意图选步,**不是**每次从 ① 跑到 ⑧。
|
||||
|
||||
| 意图 | 工具 |
|
||||
|------|------|
|
||||
| 新鲜度 / 态势 / Top | `runNmsDiagnostics`、`aggregateNmsAlarms` |
|
||||
| 一页现告警 | `queryNmsAlarms` |
|
||||
| 可引用明细 | `queryNmsAlarmsRaw`(`field_preset=evidence`);字段不明可先 `listNmsAlarmFields` |
|
||||
| 自定义聚合 | `aggregateNmsAlarmsRaw`(如 `group_by=alarm_host_name`) |
|
||||
| 复杂条件 | `sqlQueryNms`(只读 SELECT,设超时) |
|
||||
| 绰号→真名 / 是否在 NMS | `queryNmsNeInventory` / `getNmsNe` |
|
||||
| 两端路径 / 对端 | `findTopologyPaths` |
|
||||
| 只读登录 | `listCliTargets` 或 `listManagedNe` → **一次** `execManagedNe` |
|
||||
|
||||
`getManagedNe` **仅**接受纳管 `ne_id`。NMS 侧 id 用 `execManagedNe(nms_ne_id=…)`,禁止塞进 `getManagedNe`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 「能否登录」决策树(强制)
|
||||
|
||||
1. `listCliTargets(keyword=主机或IP)` **或** `listManagedNe(keyword=…, connect_status=pass)`
|
||||
- **命中** → `execManagedNe`,命令按厂商:`show version`(中兴/Cisco 等)或 `display version`(华为);以实测为准
|
||||
- **未命中** → `queryNmsNeInventory(keyword=…)`:说明 NMS 有无及 `connection_status`;并写明:**未进 netx CLI 通道则本通道不能登录**(不等于设备不存在)
|
||||
2. 已在 CLI 目标列表中:禁止只凭 inventory 说能/不能登。
|
||||
3. 本会话 `listCliTargets`、`listManagedNe` **各最多一次**,缓存结果;同台多条命令进同一次 `commands[]`。
|
||||
|
||||
---
|
||||
|
||||
## 4. CLI:多台一批(强制)
|
||||
|
||||
查 **≥2 台** 时:必须 **一次** `execManagedNe`。
|
||||
|
||||
- 命令相同:`nms_ne_ids` 或 `ne_ids` + 共享 `commands`
|
||||
- 命令不同(厂商/角色不同):`targets=[{nms_ne_id|ne_id, commands:[…]}, …]`,仍一次调用
|
||||
- 超时:加大 `read_timeout_sec` 或减条数;禁止同参盲重试
|
||||
- 只读:`show` / `display` / `ping` / `traceroute` 及带 `?` 的探索;配置类不做
|
||||
|
||||
**✓ 同命令多台**
|
||||
|
||||
```json
|
||||
{ "nms_ne_ids": ["uuid-a", "uuid-b"], "commands": ["show version"], "concurrency": 4 }
|
||||
```
|
||||
|
||||
**✓ 混厂商(多台一批)**
|
||||
|
||||
```json
|
||||
{
|
||||
"targets": [
|
||||
{ "nms_ne_id": "uuid-zte", "commands": ["show opticalinfo brief"] },
|
||||
{ "nms_ne_id": "uuid-hw", "commands": ["display optical-module brief"] }
|
||||
],
|
||||
"read_timeout_sec": 90
|
||||
}
|
||||
```
|
||||
|
||||
(示例中的 uuid 仅作工具入参示意,**禁止**出现在对用户回复里。)
|
||||
|
||||
记不清命令、要用 `?` 探索 → 见 **§8**(同台多条分裂,不是多台同探)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 现场短问配方
|
||||
|
||||
| 用户说法 | 做法 |
|
||||
|----------|------|
|
||||
| Critical Top / 高危排名 | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
|
||||
| 现网告警多少 / 态势 | `runNmsDiagnostics` 或 `aggregateNmsAlarms` → 级别 + 新鲜度 |
|
||||
| 断纤 / LOS / 光缆 | Raw:`keyword=LOS` 和/或 `Fiber Break` → **有 host_name 的列表 + 计数** |
|
||||
| 光功率**门限**(某区域) | Raw:`keyword=optical power`(或 Input optical power)+ 主机名前缀;**禁止**当断纤配方 |
|
||||
| 离线 / BN EMS / 失联 | Raw:`keyword=BN EMS` 或 communication failure |
|
||||
| dying gasp | 本端 dying gasp → `findTopologyPaths`/端口找对端 → 对端近时窗 BN EMS;**禁止只答一端** |
|
||||
| CRC / 拥塞 bandwidth / license / 风扇温度 | Raw 对应 keyword;区域用主机名前缀 |
|
||||
| 单机当前告警(已给 host) | 限定该 `host_name`;**禁止**跑无关日报/license 流程 |
|
||||
| 告警码 NNNN | 按 code 过滤;只列有 `host_name` 的 |
|
||||
| 时间窗(如 17:50–18:15) | 先新鲜度;时区以用户为准(未声明则沿用对话语境,现场常见 WIB/UTC+7) |
|
||||
| 能否登录 / SSH | §3 |
|
||||
| A<>B 容量 / 两端光功率 | **两端端口光模块 CLI 实读**,不是带宽利用率告警 tally(除非用户只要告警):解析两端 host → 路径/LLDP → **多台一批** optic CLI |
|
||||
| 哪段断了 + LOS 主机 | `object_name` + `findTopologyPaths` |
|
||||
| BGP/OSPF/LDP 等 | 指定 host 或双端协议类 Raw;要比时间就对齐;**默认不当断纤** |
|
||||
|
||||
### 口语约定
|
||||
|
||||
- **区域** = `host_name` 在第一个 `-` 之前的前缀(如 `ACH-`),大小写不敏感 starts-with。
|
||||
- **A<>B / capacity / 两端光功率** = 互联口 SFP/光功率 CLI;≠「带宽利用率超阈值」告警清单。
|
||||
- **断纤清单** ≠ **光功率门限清单**(后者 keyword=`optical power`)。
|
||||
- 绰号:先 `queryNmsNeInventory` 解析成 `host_name`;解析不到则说明无法解析,禁止瞎编。
|
||||
- 「继续 / YES / 确认」:接着上一未完成任务,不整段重开。
|
||||
|
||||
### 常见 cause 子串(keyword / 证据标签)
|
||||
|
||||
| 意图 | 典型子串 |
|
||||
|------|----------|
|
||||
| 断纤 / LOS | `ETPI) LOS`、`Fiber Break`、`Missing laser module` |
|
||||
| 光功率门限 | `Input optical power(dBm) threshold`、`Output optical power` |
|
||||
| 拥塞 | `bandwidth usage rate` |
|
||||
| CRC | `CRC error` |
|
||||
| 离线 | `BN EMS`、`NE communication failure` |
|
||||
| dying gasp | `Remote dying gasp` |
|
||||
| License | `Permanent license`、`No enough license` |
|
||||
| 环境 | `System Power off`、`undervoltage`、`temperature`、`Fan` |
|
||||
|
||||
Neighbour / PW / Tunnel 等控制面量多 **不当断纤**,除非用户问的就是该协议族。
|
||||
|
||||
---
|
||||
|
||||
## 6. 禁止
|
||||
|
||||
1. 无 severity / keyword / host / 区域 / 时间过滤翻全量告警。
|
||||
2. 把 NMS UUID 当作 `getManagedNe` 的 `ne_id`。
|
||||
3. 同轮对多台各调一次 `execManagedNe`。
|
||||
4. 光功率门限清单误用断纤/LOS 配方。
|
||||
5. 单机告警问句跑无关定时/license 流程。
|
||||
6. 终稿只有过程叙述、无 Result/Evidence。
|
||||
7. CLI 失败后对**同一错误命令、同一台**盲重试(应换命令或 `?` 探索)。
|
||||
8. 对用户输出 UUID,或用无 `host_name` 的行凑数。
|
||||
9. 因一台设备命令失败,就放弃该命令在其他设备上的使用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 终稿
|
||||
|
||||
`*主题 — 范围*` + Result / Evidence / Next;短、可扫读;大表只给 Top(且仅含有 `host_name` 的)。
|
||||
速查:[reference.md](reference.md)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录:厂商差异与 `?` 探索
|
||||
|
||||
仅在记不清命令、报错或需盲查时用。与 §4「多台一批」不同:此处是 **一台设备上多条探索命令**。
|
||||
|
||||
### 厂商前缀
|
||||
|
||||
| 厂商 | 只读习惯 |
|
||||
|------|----------|
|
||||
| 华为(含部分 VRP) | 多为 `display …` |
|
||||
| 中兴 / Cisco / 多数其他 | 多为 `show …` |
|
||||
|
||||
混厂商多台:用 `targets`。同厂商多台:可先共享 `commands`,但须接受下面「同厂也可能不一致」。
|
||||
|
||||
### 同厂不同设备命令也可能不一致
|
||||
|
||||
检查整网时**一定会**碰到:同为中兴/华为,A 台能敲的命令 B 台报错或参数不同(版本、牌号、角色差异)。
|
||||
|
||||
- **禁止**因一台失败就认定「这条命令全网作废、以后都不用」。
|
||||
- **应**:失败台单独换命令或走 `?` 探索;其余已成功的台继续用原命令。
|
||||
- **优先复用本会话已成功过的命令**(含同厂其他台验证过的):新台/下一批先试这些,再对失败子集另探;不要一失败就换全员命令。
|
||||
- 多台一批时:用 `targets` 给失败台换命令,或先一批共同命令 → 只对失败子集再一批探测;不要为了一台把成功台的结果丢掉重来。
|
||||
|
||||
### `?` 规则(`show` / `display` / `ping` 等同一套)
|
||||
|
||||
1. 报错中的 **`^`** 标出错位置 → **`^` 之前**已正确 → 对该前缀加 `?` 列下一级。
|
||||
2. `?` 可紧接在部分单词后(`show optical?`、`display inter?`),不必强制空格再写 `?`。
|
||||
3. 输出含 `<cr>` → 命令已完整,可回车执行。
|
||||
4. Incomplete → 未写完,继续 `?`;Unrecognized → 走错,退回上一层换词。
|
||||
5. **禁止**对同一错误整句反复重试。
|
||||
|
||||
例:
|
||||
|
||||
```text
|
||||
<r1>display interface briaf
|
||||
^
|
||||
Error: Wrong parameter found at '^' position.
|
||||
```
|
||||
|
||||
→ 改为 `display interface ?`,再选 `brief` 等合法下级。`show` 同理。
|
||||
|
||||
### 同台分裂(一台、多条、一次调用)
|
||||
|
||||
某一层 `… ?` 列出多个候选且还需下钻时:在 **同一台** 一次 `execManagedNe` 的 `commands[]` 中放入多条同级探索,综合结果再往下裂。
|
||||
**不是**多台同时探同一条 `?`。
|
||||
|
||||
例(已见 `display ip routing-table ?` 后):
|
||||
|
||||
```text
|
||||
display ip routing-table protocol ?
|
||||
display ip routing-table all-vpn-instance ?
|
||||
display ip routing-table all-routes ?
|
||||
```
|
||||
|
||||
对应一次调用形如:`execManagedNe(nms_ne_id=…, commands=["display ip routing-table protocol ?", "… all-vpn-instance ?", "… all-routes ?"])`。
|
||||
|
||||
### ZTE 光模块
|
||||
|
||||
优先 `show opticalinfo brief` → 其次 `show optical brief` → 仍不对则 `show optical?` / `show opticalinfo?`;同级候选用上面的同台分裂,勿盲猜整句。
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# netx-ops 速查
|
||||
|
||||
## 两套「一批」
|
||||
|
||||
- **多台一批**:一次调用打多台(`nms_ne_ids`/`targets`)
|
||||
- **同台多条**:一台上一次 `commands[]` 多条(含多个 `… ?` 分裂)
|
||||
- 禁止:同轮多台各调一次 `execManagedNe`
|
||||
|
||||
## 新鲜度
|
||||
|
||||
- `runNmsDiagnostics` / `aggregateNmsAlarms` → `meta.last_seen_*`
|
||||
- 快照:时间窗 ∈ min~max;勿默认「现在−30 分钟」
|
||||
|
||||
## 能否登录
|
||||
|
||||
1. `listCliTargets` / `listManagedNe` → 命中则实测 `show version` 或 `display version`
|
||||
2. 未命中 → inventory 说明有无;未进 CLI 通道则本通道不能登
|
||||
|
||||
## 短路径
|
||||
|
||||
| 意图 | 调用 |
|
||||
|------|------|
|
||||
| Critical Top | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
|
||||
| 态势 | `runNmsDiagnostics` / `aggregateNmsAlarms` |
|
||||
| 断纤 / LOS | Raw `keyword=LOS` / `Fiber Break` |
|
||||
| 光功率门限 | Raw `keyword=optical power` + 前缀(≠ 断纤) |
|
||||
| 离线 | Raw `keyword=BN EMS` |
|
||||
| 单机告警 | 限定 `host_name` |
|
||||
| 证据 | `queryNmsAlarmsRaw(field_preset=evidence)` |
|
||||
| 清单 | `queryNmsNeInventory` / `getNmsNe` |
|
||||
| 多台 CLI | `execManagedNe(nms_ne_ids=…)` 或 `targets` |
|
||||
| 长任务 / ≥4 台 | `execManagedNe(async=true)` → `getNeExecJob(job_id=…)` |
|
||||
| 设备能力 | `getManagedNe` → 读 `capability.recommended_mode` |
|
||||
| 路径 | `findTopologyPaths` |
|
||||
| A<>B 光 | 两端 host → 路径 → 多台一批 optic CLI |
|
||||
|
||||
## 展示
|
||||
|
||||
- 只展示 **host_name**;严禁 UUID
|
||||
- 无 `host_name`:丢弃
|
||||
- DSH:前缀 `netx__`
|
||||
|
||||
## 厂商与 `?`
|
||||
|
||||
- 华为 `display`;其他多 `show`;方法同一套 `?`
|
||||
- **同厂也可能命令不一**:一台失败 ≠ 全网作废;失败台另探,成功台继续原命令;**已成功过的命令优先复用**
|
||||
- `^` 前正确 → 加 `?`(可接词尾)
|
||||
- **同台分裂**:一台 `commands[]` 多条 `… ?`;**不是**多台同探
|
||||
- ZTE 光:`opticalinfo brief` → `optical brief` → `show optical?`
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
---
|
||||
name: netx-topology
|
||||
description: >-
|
||||
用 netx-topology MCP / DSH topology 组查邻接、分类打标;先 dual_unit 一次抽最大核心眼,
|
||||
再换其它算法下沉/布图(不污染 Fabric)。触发:画拓扑、布图、拖图、分类、LLDP、Fabric、netx-topology。
|
||||
---
|
||||
|
||||
# netx 拓扑(通用)
|
||||
|
||||
## Hosts(MCP + DSH)
|
||||
|
||||
| Host | Tools |
|
||||
|------|--------|
|
||||
| **MCP** `netx-topology-mcp` | 裸名(`sinkTopologyDualUnits` …);安装见 [`docs/MCP_TOPOLOGY.md`](../../../docs/MCP_TOPOLOGY.md) |
|
||||
| **DSH** `dsh-netxops` | `netx__` + 同 stem。整组工具 + skill **`netx-topology`**(能力组 **topology**,默认关)。缺布局引擎能力时用 MCP。 |
|
||||
|
||||
Canonical: `netx/skills/topology/netx-topology/`(唯一正文)。
|
||||
|
||||
**原则**:
|
||||
1. **第一步用 dual_units**:从**核心**出发,选**覆盖网元最多**的那一只眼(`prefer_top_eye` + max cover)。
|
||||
2. **`sinkTopologyDualUnits` / `layout_dual_unit` 整图流程只用一次**——只沉这一只最大眼;**禁止**再连抽下一批 dual_unit。
|
||||
3. **后续下沉换算法**:`suggestSinkHubs` → `move_nodes(park)` / `projectTopologyNeighbors`;**不要**再 `sinkTopologyDualUnits`。
|
||||
4. **眼图已定型 → 门控精修**:对该 sink 只用下表「允许」动作;禁止全局拆眼工具。
|
||||
5. 布眼目标:少交叉、眼心空旷、少重叠(椭圆弧带;长链在眼外)。对照人工金标时还要看:**紧凑度、正交边、贴边清开**(见「算法天花板」)。
|
||||
|
||||
**禁止**写临时 py 穷举坐标或直接调 HTTP;验证与压交叉**只调 MCP**(或 DSH 已实现的同名工具)。不造 Fabric 边。
|
||||
**脱敏(硬)**:勿把客户网元名、站点/区域名、具体交叉数、具体 view_id 写进本 skill。角色只用通用词:门户 / 枢纽 / 汇聚 / 接入 / 末梢。**禁止**在 skill 正文写站点缩写或设备角色缩写当专名举例。
|
||||
|
||||
---
|
||||
|
||||
## 完整处理思路(一次大区接入眼)
|
||||
|
||||
按阶段执行;每阶段只调 MCP,读返回再决定下一步。
|
||||
|
||||
```
|
||||
① 认眼
|
||||
analyze(structure) → dual_units 里取覆盖最大的核心门户对
|
||||
② 一次 sink
|
||||
sinkTopologyDualUnits(max_units=1, prefer_top_eye, layout_batch)
|
||||
→ 同一 sink 贯穿全程;眼形不对只 layout_dual_unit 重布同一对门户
|
||||
③ 换算法下沉(禁 dual)
|
||||
循环:suggestSinkHubs(pick:1) → move_nodes(park) → until_limit(crossing)
|
||||
空批次后:orphan_batch / 对根−sink−门户 分批 park;仍禁 dual
|
||||
④ 眼定型精修(门控序列,见下)
|
||||
until_limit(crossing) → until_limit(total)
|
||||
→ clear_edge_hits(portal_ids) → pull_far_chains(portal_ids)
|
||||
→ compact_bbox(portal_ids, outlier_only) → 可选再 clear / total
|
||||
⑤ 停手(算法天花板,**默认无范本**)
|
||||
overlaps=0;until_limit stall;pull/compact/clear 均 moved≈0
|
||||
→ 交付:眼可读 + ov=0 + 交叉可接受;util/正交/贴边缺口 → 手拖或改初布
|
||||
→ 勿再发明无门控全局工具硬挤
|
||||
```
|
||||
|
||||
**范本现实(硬)**:
|
||||
- **日常默认没有范本**;有人工图也几乎不是同一张网 → **禁止**把 `align_reference` / 抄坐标当主路径。
|
||||
- `align_reference` **仅**用于:同网同成员画布的 A/B 对照、调试、回放(共享 `fabric_node_id`)。跨网 ID 对不上,抄了也无意义。
|
||||
- 生产要缩短与人工观感差距 → **改初布配方**(正交/紧凑从 dual 一次就偏地铁风),不是现场找金标对齐。
|
||||
|
||||
**从零重布(无范本算力路径)**:
|
||||
- 眼已在 sink、只需甩掉坏坐标 / 金标残留:对本 sink **`layout_dual_unit`(同一门户对)→ 立刻门控精修**。这**不算**再 sink 一批 dual。
|
||||
- 初布后交叉会先飙高、贴边变差属正常;**不要**因此改走 `polish_crossings` / `fix_overlaps`。
|
||||
- 纯算力压交叉后 util/bbox 常崩(枢纽被 `until_limit` 甩远)→ **必须**接 `pull_far_chains` → `compact_bbox`;缺这两个 action 就先重启 MCP,勿半截交付。
|
||||
- 无范本时:交叉可压到可读,但仍常高于「抄同网坐标」路径;util/正交缺口靠 pull + 初布,**不是**再堆 until_limit。
|
||||
|
||||
**开局核对 catalog(硬)**:
|
||||
- 精修前先 `layoutTopologyView(catalog=true)`:须见 `pull_far_chains`、`compact_bbox`,且 `version`/`rev` 为当前包。
|
||||
- 缺 action 或 `rev` 过旧 → **重启** netx-topology MCP 再干;禁止用旧进程硬跑、禁止改走 HTTP/临时 py。
|
||||
|
||||
**精修硬门控(已写进算法,skill 须遵守)**:
|
||||
- 任何落笔:**全局交叉不得升高**;升高 → 弃该候选。
|
||||
- **最小化展开**(bundle):从小压缩步长试探;步长须清开 footprint(成员互叠也算失败)。
|
||||
- **侵入** = 展开后踏入外人框 / 留下任意 footprint 重叠 → 换 tip,勿硬塞。
|
||||
- `until_limit` 只接受 `overlaps=0`;apply 遇 `overlaps_remain` → 修算法,**勿**对眼 sink 跑 `fix_overlaps`。
|
||||
- `portal_ids` **只钉眼门户**;`freeze_layers=["core"]` 会冻住 sink 上所有核心(含旁路枢纽)——慎用。
|
||||
- 旁路枢纽也要动时:`protect_rigid=off`(仍受 `portal_ids` 约束)。
|
||||
- `clear_edge_hits`:**禁止**放宽交叉(含 `preserve_axis`);交叉升高一律拒。
|
||||
- `pull_far_chains`:相对**门户中点**缩放远走廊/孤立点/远叶(不是朝外圈枢纽);枢纽本身在外围时朝枢纽缩**收不了 bbox**。
|
||||
- `compact_bbox`:默认 **farthest-K**(`outlier_only`);全图/分位均匀缩易叠点 → 拒。
|
||||
- `until_limit`:`stop_reason=max_moves` **≠** stall → **再调**直到 `stall`(可提高 `max_moves`,但仍以 stall 为准)。
|
||||
- 大眼 `max_jump` 用 4k–8k;勿收到 <2k。大图会 background → `job_status`(可数分钟);勿中途改走拆眼工具。
|
||||
|
||||
---
|
||||
|
||||
## 算法天花板(经验固化)
|
||||
|
||||
大区接入眼跑完门控序列后,常见结果:
|
||||
|
||||
| 维度 | 算法能做到 | 再挤会怎样 | 交付策略 |
|
||||
|------|------------|------------|----------|
|
||||
| 交叉 | 从零 dual 后 `until_limit` 可大幅压下;无范本时仍常高于抄坐标路径 | 再挪极值尖端易抬交叉;`max_moves` 停了要续跑到 stall | **停**于 stall;残余当弦交叉可接受 |
|
||||
| 重叠 | 硬零 | — | 必须保持 |
|
||||
| 紧凑 util / bbox | 压交叉常先**炸开** bbox;`pull_far_chains` 多轮收远走廊 | 缺 pull 就半截交付;强缩常 `Δg>0` 拒 | **必跑** pull→compact 到 `moved=0`;其余手拖 |
|
||||
| 贴边 clearance | `clear` + `objective=total` 能降一批 | 与交叉/紧凑拉扯,清到个位数很难 | stall 后手拖擦边点 |
|
||||
| 正交 axis | `total` 略抬;远低于人工地铁风 | `preserve_axis` 清边若放宽交叉会毁掉前面成果(已禁 slack) | 手拖走廊;勿为轴牺牲交叉 |
|
||||
| 直链 / 环穿 | bundle 后仍有折角与穿环 | 全局 straighten 拆眼 | 勿清零;结构优先 |
|
||||
|
||||
**判定「算法到头」**(门控精修,无范本):
|
||||
1. `overlaps=0`
|
||||
2. `until_limit`(crossing 与 total)均 stall
|
||||
3. pull/compact/clear `moved≈0`
|
||||
4. 目视:眼心空、双门户可辨
|
||||
|
||||
**可选对照**(仅评测):对**同网**人工画布与算法 sink 各 `analyze`,只比 `summary`;**勿**因此去 `align_reference` 当交付手段。
|
||||
|
||||
**明确无效 / 禁止再试**(眼 sink):
|
||||
- 再次 `sinkTopologyDualUnits` / `until_empty` / 新块画布抽 dual
|
||||
- `polish_crossings` / `straighten_channels` / `untangle` / `fix_overlaps` / `orbit_sweep(round=true)`
|
||||
- 临时 py 穷举坐标、无门控的整图缩放/平移极值点
|
||||
- 为追 util 关掉交叉门控或给 `clear` 开交叉 slack
|
||||
- **把跨网/他网人工图当范本抄坐标**(含滥用 `align_reference`)
|
||||
|
||||
---
|
||||
|
||||
## 主路径(必循)
|
||||
|
||||
```
|
||||
analyze(structure) → 读 dual_units(认核心最大眼)
|
||||
→ 有 level:先打标;巨图勿先全量 level_bands
|
||||
→ 【仅一次】sinkTopologyDualUnits(
|
||||
max_units=1, prefer_top_eye=true, prefer_pure=false,
|
||||
sink_layers=[access], keep_layers=[core,agg], layout_batch=true,
|
||||
max_nodes/max_batch_nodes 放行大眼
|
||||
)
|
||||
→ 眼形不对:可对本 sink 重跑 layout_dual_unit(仍算同一只眼;勿再 sink 下一批 dual)
|
||||
→ 【换算法】suggestSinkHubs → move_nodes(park) 分批迁入同一 sink;每批后 until_limit;禁止再 dual
|
||||
→ 【眼定型】门控精修序列(下节);禁止全局拆眼
|
||||
→ 根图可 level_bands(只动根上残留,不动已定型的眼 sink)
|
||||
```
|
||||
|
||||
停手:最大眼可读、眼心空、`overlaps≈0`、门户清晰;其余节点用非 dual 手段靠上去即可。交叉允许来自跨走廊弦边。
|
||||
|
||||
**dual 一次用尽**:`until_empty`、再次 `sinkTopologyDualUnits`、为下一批 dual 新建块画布 —— **一律禁止**(日常)。
|
||||
|
||||
**布眼几何**(仅那一次):嵌套半椭圆;中轴只有双门户;长链/尾巴放眼外禁穿环。眼坏了重跑 `layout_dual_unit`,勿全局拆眼。
|
||||
|
||||
---
|
||||
|
||||
## 眼图定型后的精修(门控序列)
|
||||
|
||||
眼 sink 一经验收,**禁止**再对该画布跑会整图拆眼的工具。
|
||||
|
||||
| 允许 | 禁止(眼 sink 上) |
|
||||
|------|-------------------|
|
||||
| `analyze(detail=hotspots\|both)` 只读 | `polish_crossings` / `straighten_channels` |
|
||||
| `orbit_sweep`:**`until_limit=true`** 或 `node_id` preview→pick | `orbit_sweep(round=true)` 全图轮扫 |
|
||||
| `clear_edge_hits`(**须** `portal_ids`;交叉不升) | `fix_overlaps` / `untangle` 批处理整图 |
|
||||
| `pull_far_chains`(朝**门户中点**收远走廊/孤立点;同上门控) | `layout` / 再次 `sinkTopologyDualUnits` |
|
||||
| `compact_bbox`(farthest-K;交叉不升、ov=0) | 临时 py 穷举坐标 |
|
||||
| `updateTopologyViewPositions` 手拖 1~少数点 | `align_reference` 当无同网范本时的交付手段 |
|
||||
| 眼形整体崩了才 `layout_dual_unit` 重布同一门户对 | |
|
||||
|
||||
**推荐节奏**(一次序列,读返回再决定是否重复某一环):
|
||||
|
||||
```
|
||||
0) catalog=true 核对 pull_far_chains / compact_bbox(缺则重启 MCP)
|
||||
0b) 从零重布时:layout_dual_unit(同一门户对, apply) // 交叉飙高正常
|
||||
1) until_limit + objective=crossing + portal_ids + bundle
|
||||
→ stop_reason=max_moves 则续跑,直到 stall
|
||||
2) until_limit + objective=total // 交叉不升;抬贴边/正交;亦可 max_moves→续跑
|
||||
3) clear_edge_hits(portal_ids, top_n/max_moves 可加大;勿 preserve_axis 除非确认不抬交叉)
|
||||
4) pull_far_chains(portal_ids) // 可 2~3 轮直到 moved=0;压交叉后必做
|
||||
5) compact_bbox(portal_ids, outlier_only) // 常 accepted=false,正常
|
||||
6) 可选:再 clear 或 total;再 stall → 停手
|
||||
```
|
||||
|
||||
`until_limit` 调用模板:
|
||||
|
||||
```
|
||||
layoutTopologyView({
|
||||
action: "orbit_sweep", mode: "apply",
|
||||
params: {
|
||||
until_limit: true,
|
||||
objective: "crossing", // 再一轮改 total
|
||||
portal_ids: [<门户A>, <门户B>],
|
||||
protect_rigid: "off",
|
||||
max_degree: 14,
|
||||
max_jump: 8000, // 大眼用 4k–8k;勿 <2k
|
||||
max_stretch: 32,
|
||||
max_moves: 40, // 触顶只说明配额满,不是算法到头
|
||||
bundle: true
|
||||
}
|
||||
})
|
||||
→ 读 local.op:start/end_crossings、moves[]、stop_reason
|
||||
→ max_moves → 再调;stall → 进入下一步
|
||||
```
|
||||
|
||||
`round=true` ≠ `until_limit`:前者一批 top_n 死拿 pick#1;后者循环到 stall。
|
||||
勿为清零交叉拆弧带;以 `verdict.total` + 目视眼形 +「算法到头」四条为准。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 0 — 画布与成员
|
||||
|
||||
1. `getTopologyTree` → **`view_id`**(文件夹 physical 画布)。
|
||||
2. 建根/子区域只用 `createTopologyFolder`。
|
||||
3. 缺层级/区域时:`classifyTopologyFabricNodes`(`match` → `tag(level, dry_run)` → `tag`;或 `unmatched` / `apply_rules`)。**level** 越小越靠外(0 外部 / 1 核心 / 2 汇聚 / 3 接入;可用 1.1、2.1 子层)。**勿**切片建图。
|
||||
4. `analyzeTopologyViewLayout({ view_id, detail: "structure" })` 读 `dual_units` / `shape` / `layers` —— **先认核心最大眼**(`unit_count` / 各 unit `node_count`)。
|
||||
5. `addTopologyViewNodes`(可按 `role` 大档或后续 level)/ `projectTopologyNeighbors`(区域画布务必 `region_folder_id`)。
|
||||
6. 多余点用 `removeTopologyViewNodes` **移出画布**(不删 Fabric)。`region:…` 幽灵点勿当网元拖。
|
||||
|
||||
---
|
||||
|
||||
## 形状与 dual_units(只服务「第一只眼」)
|
||||
|
||||
```
|
||||
analyzeTopologyViewLayout({ view_id, detail: "structure" })
|
||||
```
|
||||
|
||||
| 字段 | 用途 |
|
||||
|------|------|
|
||||
| `shape.primary` | `chains` / `star` / `mesh` / `mixed_blocks` |
|
||||
| `dual_units` | **从核心出发**:core–core → core–agg → …,再取 **node_count 最大**的一只做唯一 sink |
|
||||
| `advice.block_plan` | 其余块怎么靠(非 dual 连抽) |
|
||||
| `gravity.type` | 链图勿当 hub 花瓣 |
|
||||
|
||||
- **第一步必选最大核心眼**:`prefer_top_eye=true`,`prefer_pure=false`,`max_units=1`;目标是**一块吃进尽量多网元**,不要求交叉=0。
|
||||
- **禁止**把 dual_units 当成「抽干源图」的循环泵;剩余网元见下方「后续下沉」。
|
||||
- **禁止**再用互斥 soft_block 把通路切开。
|
||||
- **链图 / 小图**:可不走 dual;preview `corridor`/`compact`。
|
||||
|
||||
---
|
||||
|
||||
## 根图 → 子区域:第一次(dual,仅一次)
|
||||
|
||||
```
|
||||
sinkTopologyDualUnits({
|
||||
source_view_id: <根图>,
|
||||
sink_view_id: <子区域>, // 同一张接入画布贯穿全程
|
||||
max_units: 1,
|
||||
max_nodes: 300,
|
||||
max_batch_nodes: 400,
|
||||
layout_batch: true,
|
||||
sink_layers: ["access"],
|
||||
keep_layers: ["core", "agg"],
|
||||
prefer_top_eye: true,
|
||||
prefer_pure: false,
|
||||
// 勿 until_empty;勿随后再调本工具
|
||||
})
|
||||
→ 眼形验收 / 手拖;必要时 layout_dual_unit 重布同一眼
|
||||
```
|
||||
|
||||
- **同一 sink**:后续非 dual 迁入也进**同一个** `sink_view_id`;禁止块1/块2/块3。
|
||||
- **眼定型后**:对该 sink **只门控精修**(见上节)。
|
||||
|
||||
---
|
||||
|
||||
## 后续下沉(换算法,禁用 dual)
|
||||
|
||||
源图仍有接入 / 旁路网元时,**不要**再 `sinkTopologyDualUnits`。
|
||||
|
||||
0. **先问算法要批次**(推荐)
|
||||
```
|
||||
suggestSinkHubs({
|
||||
source_view_id: <根图>,
|
||||
sink_view_id: <眼 sink>,
|
||||
pick: 1,
|
||||
})
|
||||
→ batch.fabric_node_ids / move_nodes 可直接套用
|
||||
```
|
||||
排序:剩余领地大优先、agg 优于 core;眼门户不当 batch 头。
|
||||
|
||||
**每批迁完立刻** `until_limit(crossing, portal_ids)`,再取下一批;勿攒多批再一起 polish。
|
||||
|
||||
`suggestSinkHubs` 空了但根上还有孤立末梢/旁路:清掉已在 sink 的重复点,再对 `root−sink−眼门户` 分批 `move_nodes(park)`(每批后 `until_limit`)。也会直接吐 `orphan_batch_*`。勿再 dual。
|
||||
|
||||
1. **指定迁移 + 扫角停靠**
|
||||
```
|
||||
layoutTopologyView({
|
||||
action: "move_nodes",
|
||||
source_view_id: <FROM>, view_id: <TO>,
|
||||
mode: "apply",
|
||||
params: {
|
||||
fabric_node_ids: [...],
|
||||
park: true,
|
||||
remove_from_source: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
2. **投影邻居**:`projectTopologyNeighbors`(区域画布带 `region_folder_id`),再手拖或 `orbit_sweep`。
|
||||
|
||||
3. **根图收尾**:`level_bands` 只钉**根图**上残留的 core/agg;**勿**对已定型眼 sink 跑全局带。
|
||||
|
||||
4. **小连通块**:源上剩余短链/星,可对**源图**或迁入后**单点**处理;勿对眼 sink `layout(recipe=…)`。
|
||||
|
||||
---
|
||||
|
||||
## 分层布局(特殊场景)
|
||||
|
||||
当网络需呈现层级结构(外部→接入→核心→汇聚→客户等)时:
|
||||
|
||||
### 分层通用规则
|
||||
1. **顶层:外部/对接网络**
|
||||
2. **次顶层:终端客户接入层**
|
||||
3. **中间层:核心层**
|
||||
4. **核心下层:汇聚层**
|
||||
5. **底层:接入层/孤立层**
|
||||
|
||||
### 优化流程
|
||||
1. **先手动后算法**:用户手动拖拽保证业务结构,再用算法优化
|
||||
2. **分层约束**:`updateTopologyViewPositions` 按层钉 y;`orbit_sweep` 可传 `y_min/y_max`
|
||||
3. **分步(非眼画布 / 根图)**:可贴边、分层钉 y;**已定型眼 sink 除外**——只允许门控精修 / 手拖
|
||||
|
||||
### 冲突处理
|
||||
| 冲突场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 交叉 vs 结构 | **结构优先**(最大眼 / 分层) |
|
||||
| 算法 vs 手动 | **手动优先** |
|
||||
| polish 拆眼 | **禁止**对 dual 眼走 straighten 主路径 |
|
||||
| 算法 vs 金标观感 | 交叉可赢;紧凑/正交/贴边交给手拖 |
|
||||
|
||||
---
|
||||
|
||||
## layoutTopologyView(精简)
|
||||
|
||||
| action | 用途 |
|
||||
|--------|------|
|
||||
| `layout` | 小图 / 剩余块:`compact` / `corridor` / `rings` / `unstick` |
|
||||
| `layout_dual_unit` | **仅**重布已沉的那一只眼;椭圆弧带 + 空心眼 |
|
||||
| `move_nodes` / `sink_nodes` | **后续下沉主路径**;`park` 块扫 |
|
||||
| `orbit_sweep` | **眼后精修主工具**:`until_limit` 或 `node_id`;`objective=crossing\|total` |
|
||||
| `clear_edge_hits` | 眼 sink **可**(须 `portal_ids`;交叉不升) |
|
||||
| `pull_far_chains` | 眼 sink **可**:朝门户中点收远走廊/孤立点 |
|
||||
| `align_reference` | **非主路径**:仅同网同成员 A/B 调试;跨网禁用 |
|
||||
| `compact_bbox` | 眼 sink **可**:farthest-K 收 bbox |
|
||||
| `level_bands` | 仅根图/未定型画布;巨图在 dual 一次之后钉根 |
|
||||
| `polish_crossings` | **眼 sink 禁用** |
|
||||
| `fix_overlaps` / `untangle` / `straighten_channels` | **眼 sink 禁用** |
|
||||
| `job_status` / `job_cancel` | 后台 job |
|
||||
|
||||
眼 sink 精修 = 门控序列 + 手拖补观感;不要排全局流水线,也不要在 stall 后继续发明新全局动作。
|
||||
|
||||
---
|
||||
|
||||
## 验收
|
||||
|
||||
| 块 | 看什么 |
|
||||
|----|--------|
|
||||
| `overlap` | 硬零(权重 0.24) |
|
||||
| `crossing` | crossings/cpl;`top_nodes` / `top_edges` |
|
||||
| `sparsity` / `edge_axis` / `edge_clearance` | 对照人工:紧凑、正交、贴边(算法常弱于此) |
|
||||
| `rings` / `chains` | 环被穿、走廊折角 |
|
||||
| `verdict.total` | ≈70 可交付(ov=0);结构可读优先于刷分 |
|
||||
| `score_profile` | `auto`(默认)/ `default` / `eye`;大稀疏斜边画布自动走 eye |
|
||||
|
||||
**评分修订(相对旧权)**:util 甜区下限 0.08;贴边分=节点命中∪hits/link;rings 降权;eye 再降 axis、抬 util/clearance;`compactness` 仅诊断不加权。
|
||||
|
||||
图标 25px;推荐中心距 Δx≥200、Δy≥170。
|
||||
|
||||
---
|
||||
|
||||
## 工具速查
|
||||
|
||||
| 工具 | 作用 |
|
||||
|------|------|
|
||||
| `getTopologyTree` / `getTopologyView` | 树与画布 |
|
||||
| `createTopologyFolder` | 新建根/区域 |
|
||||
| `add` / `remove` / `updateTopologyViewPositions` | 成员与手拖 |
|
||||
| `sinkTopologyDualUnits` | **仅一次**:最大核心眼 → sink |
|
||||
| `suggestSinkHubs` | **后续下沉批次**(含 orphan_batch) |
|
||||
| `move_nodes`(经 layoutTopologyView) | **后续下沉** |
|
||||
| `projectTopologyNeighbors` | 投影邻居 |
|
||||
| `copyTopologyViewNodes` | 克隆沙箱 |
|
||||
| `queryTopologyFabricNodes` | 库存 |
|
||||
| neighborhood / edges | 邻接 |
|
||||
| `layoutTopologyView` | 上表 action |
|
||||
| `analyzeTopologyViewLayout` | structure + 验收 |
|
||||
|
||||
---
|
||||
|
||||
## 代码热更
|
||||
|
||||
1. 本仓 MCP 用 `PYTHONPATH=…/src`(见用户 `~/.cursor/mcp.json`),改源码后不必为加载而 pip install。
|
||||
2. **必须重启** stdio 进程(杀旧 `python -m netx_topology_mcp` 后由 Cursor 拉起;或 MCP 面板禁用/启用)。
|
||||
3. `layoutTopologyView(catalog=true)` 核对:`version` / `rev`(`NETX_MCP_REV`)+ action 含 `pull_far_chains` / `compact_bbox`。
|
||||
4. 机器上常残留多份旧 MCP 进程 → 精修前 catalog 若仍缺 action,**杀光再启**;勿用旧进程半截交付,勿改走 HTTP/临时 py。
|
||||
|
||||
拓扑页开「实时同步」可看落笔。勿用告警/CLI MCP 写拓扑。
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Sync canonical netx/skills → dsh-netxops presets (npm embed only).
|
||||
# MCP packages do NOT carry a skills/ mirror — point Cursor/hosts at netx/skills/.
|
||||
param(
|
||||
[string]$NetxRoot = ""
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$opsRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
if (-not $NetxRoot) {
|
||||
$sibling = Join-Path $opsRoot "..\netx"
|
||||
if (Test-Path (Join-Path $sibling "skills")) {
|
||||
$NetxRoot = (Resolve-Path $sibling).Path
|
||||
} else {
|
||||
throw "Pass -NetxRoot or place netx checkout next to netxops (../netx/skills)"
|
||||
}
|
||||
}
|
||||
$src = Join-Path $NetxRoot "skills"
|
||||
$dst = Join-Path $opsRoot "presets\netxops\skills"
|
||||
if (-not (Test-Path $src)) { throw "missing $src" }
|
||||
|
||||
foreach ($group in @("ops", "topology", "biz-monitor")) {
|
||||
$from = Join-Path $src $group
|
||||
$to = Join-Path $dst $group
|
||||
if (-not (Test-Path $from)) { throw "missing group $from" }
|
||||
if (Test-Path $to) { Remove-Item $to -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $to) | Out-Null
|
||||
Copy-Item $from $to -Recurse -Force
|
||||
}
|
||||
|
||||
# Drop legacy groups if present
|
||||
foreach ($legacy in @("nms", "common", "topology-layout")) {
|
||||
$legacyPath = Join-Path $dst $legacy
|
||||
if (Test-Path $legacyPath) { Remove-Item $legacyPath -Recurse -Force }
|
||||
}
|
||||
|
||||
@"
|
||||
# Mirrored from netx/skills — do not long-edit here
|
||||
|
||||
Canonical: ``$NetxRoot\skills``
|
||||
|
||||
```powershell
|
||||
powershell -File .\scripts\sync-skills-from-netx.ps1
|
||||
```
|
||||
|
||||
Runtime prefers ``NETX_SKILLS_ROOT`` or sibling ``../netx/skills``.
|
||||
Groups: **ops** (``netx-ops``) + **topology** (``netx-topology``). One group ↔ one skill.
|
||||
|
||||
**Keep netx-ops pure netx** (alarms / inventory / CLI). Knowledge-base triage belongs in
|
||||
operator-subset ``_skills/kb-*`` packs registered by the plugin — do **not** write KB linkage
|
||||
back into ``netx-ops``.
|
||||
"@ | Set-Content -Encoding utf8 (Join-Path $dst "README.md")
|
||||
|
||||
Write-Host "Synced $src -> $dst"
|
||||
Get-ChildItem -Recurse $dst -Filter SKILL.md | ForEach-Object { $_.FullName.Substring($opsRoot.Path.Length) }
|
||||
|
|
@ -70,23 +70,15 @@ export function verifyRelease(directory = root) {
|
|||
})
|
||||
assert.equal(typeof clientExports.apply, 'function')
|
||||
assert.deepEqual(Array.from(clientExports.inject), ['slots', 'locale'])
|
||||
for (const group of ['ops/netx-ops', 'topology/netx-topology', 'biz-monitor/netx-biz-monitor']) {
|
||||
const skill = read(`presets/netxops/skills/${group}/SKILL.md`).replace(/\r\n/g, '\n')
|
||||
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(skill)
|
||||
assert.ok(frontmatter, `Missing skill frontmatter: ${group}`)
|
||||
const parsed = load(frontmatter[1])
|
||||
assert.equal(typeof parsed.name, 'string')
|
||||
assert.equal(typeof parsed.description, 'string')
|
||||
assert.ok(skill.slice(frontmatter[0].length).trim())
|
||||
}
|
||||
for (const path of ['presets/netxops/PERSONA.md', 'presets/netxops/preset.yml']) assert.ok(read(path).trim())
|
||||
assert.equal(existsSync(resolve(directory, 'presets/netxops/skills')), false, 'Group skills must not be bundled')
|
||||
return { version: pkg.version, outputs }
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
try {
|
||||
const result = verifyRelease()
|
||||
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts, bundle and skills verified.`)
|
||||
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts and bundle verified.`)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exitCode = 1
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const DEFAULT_CAPABILITY_GROUPS: NetxCapabilityGroups = Object.freeze({
|
|||
* `_meta.netx.group`. See `mcp-catalog.ts` / `filterCatalogTools`.
|
||||
*/
|
||||
|
||||
/** Skill directory name under skills root / `presets/netxops/skills/<group>/`. */
|
||||
/** Skill directory name under the MCP skills cache (`netxops-mcp-skills/<key>/<group>/`). */
|
||||
export const SKILL_DIR_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, string>> = Object.freeze({
|
||||
ops: 'ops',
|
||||
topology: 'topology',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
/**
|
||||
* Register Ops playbook skills for enabled capability groups from disk.
|
||||
* Register Ops playbook skills for enabled capability groups from the
|
||||
* on-disk MCP skills cache (`skills-cache.ts`).
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir, readFile, stat } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import {
|
||||
CAPABILITY_GROUP_IDS,
|
||||
SKILL_DIR_BY_GROUP,
|
||||
type NetxCapabilityGroupId,
|
||||
} from './capability-groups.ts'
|
||||
import { getNetxConnection } from './runtime.ts'
|
||||
import { skillsCacheDir } from './skills-cache.ts'
|
||||
|
||||
interface ParsedSkill {
|
||||
name: string
|
||||
|
|
@ -23,39 +24,6 @@ interface ParsedSkill {
|
|||
|
||||
export type { ParsedSkill }
|
||||
|
||||
/**
|
||||
* Package `presets/netxops/skills` root, or a shared netx skills checkout.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. `NETX_SKILLS_ROOT` (canonical `netx/skills`)
|
||||
* 2. Sibling `../netx/skills` when developing next to the netx repo
|
||||
* 3. Bundled `presets/netxops/skills` inside this package
|
||||
*/
|
||||
export function opsSkillsRoot(): string {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim()
|
||||
if (envRoot && existsSync(envRoot)) return envRoot
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const checkout = [join(here, '..', '.git'), join(here, '..', '..', '.git')].some(existsSync)
|
||||
const siblingCandidates = [
|
||||
// src/netx → ../../../netx/skills ; lib → ../../netx/skills
|
||||
join(here, '..', '..', '..', 'netx', 'skills'),
|
||||
join(here, '..', '..', 'netx', 'skills'),
|
||||
]
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
|
||||
const packaged = [
|
||||
join(here, '..', 'presets', 'netxops', 'skills'),
|
||||
join(here, '..', '..', 'presets', 'netxops', 'skills'),
|
||||
]
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
return packaged[0]
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw: string): { data: Record<string, unknown>; body: string } | undefined {
|
||||
// Windows CRLF (and mixed) checkouts break `$`-anchored YAML lines: a trailing
|
||||
// `\r` is a JS line terminator, so `.` and `$` both refuse it and description
|
||||
|
|
@ -200,6 +168,12 @@ export interface RegisterGroupSkillsOptions {
|
|||
*/
|
||||
disabledByGroup?: DisabledToolsByGroup
|
||||
lang?: string
|
||||
/**
|
||||
* Cache root containing `ops` / `topology` / `biz-monitor`.
|
||||
* Tests inject this; production resolves `$DSH_HOME/netxops-mcp-skills/<key>`
|
||||
* from the live connection apiUrl.
|
||||
*/
|
||||
skillsRoot?: string
|
||||
}
|
||||
|
||||
function disabledFootnote(names: readonly string[], lang?: string): string {
|
||||
|
|
@ -211,6 +185,15 @@ function disabledFootnote(names: readonly string[], lang?: string): string {
|
|||
: `\n\n## 已禁用工具\n以下工具已禁用:${list},不要调用。\n`
|
||||
}
|
||||
|
||||
function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string {
|
||||
if (typeof options.skillsRoot === 'string' && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim()
|
||||
}
|
||||
const connection = getNetxConnection()
|
||||
if (!connection?.apiUrl?.trim()) return ''
|
||||
return skillsCacheDir(connection.apiUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register playbook skills for the given capability groups into `ctx.skills`.
|
||||
* Groups that are off are skipped entirely (no skill injection).
|
||||
|
|
@ -243,10 +226,16 @@ export async function registerGroupSkills(
|
|||
return () => {}
|
||||
}
|
||||
|
||||
const root = opsSkillsRoot()
|
||||
const root = resolveSkillsRoot(options)
|
||||
const disposers: Array<() => void> = []
|
||||
const enabled = new Set(groupIds)
|
||||
const log = (ctx as { logger?: { warn: (msg: string, ...args: unknown[]) => void } }).logger
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.('%s: no MCP skills cache (missing apiUrl)', providerLabel)
|
||||
}
|
||||
return () => {}
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId)) continue
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
import type { NetxCapabilityGroupId } from './capability-groups.ts'
|
||||
import { CAPABILITY_GROUP_IDS } from './capability-groups.ts'
|
||||
import type { NetxMcpToolDescriptor } from './mcp-types.ts'
|
||||
import type { CachedSkillFile } from './skills-cache.ts'
|
||||
|
||||
const PROTOCOL_VERSION = '2025-03-26'
|
||||
const CLIENT_INFO = { name: 'dsh-netxops', version: '0.1.71' }
|
||||
const SKILLS_URI_PREFIX = 'netx://skills/'
|
||||
|
||||
export interface McpRemoteOptions {
|
||||
/** Absolute MCP endpoint, e.g. `http://127.0.0.1:8890/mcp`. */
|
||||
|
|
@ -74,10 +76,48 @@ function parseTool(raw: unknown): NetxMcpToolDescriptor | null {
|
|||
const netx = meta.netx && typeof meta.netx === 'object' && !Array.isArray(meta.netx)
|
||||
? meta.netx as Record<string, unknown>
|
||||
: {}
|
||||
// Plugin mounts skills itself — never register getNetxSkill (group=meta).
|
||||
if (netx.group === 'meta') return null
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined
|
||||
return { name, description, inputSchema, readOnlyHint, group }
|
||||
}
|
||||
|
||||
function readMetaNetx(row: Record<string, unknown>): Record<string, unknown> {
|
||||
const meta = (row._meta ?? row.meta)
|
||||
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {}
|
||||
const netx = (meta as Record<string, unknown>).netx
|
||||
if (!netx || typeof netx !== 'object' || Array.isArray(netx)) return {}
|
||||
return netx as Record<string, unknown>
|
||||
}
|
||||
|
||||
function parseSkillResource(
|
||||
resource: unknown,
|
||||
text: string,
|
||||
): CachedSkillFile | null {
|
||||
if (!resource || typeof resource !== 'object' || Array.isArray(resource)) return null
|
||||
const row = resource as Record<string, unknown>
|
||||
const uri = typeof row.uri === 'string' ? row.uri.trim() : ''
|
||||
const netx = readMetaNetx(row)
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined
|
||||
const skill = typeof netx.skill === 'string' ? netx.skill.trim() : ''
|
||||
if (!group || !skill) return null
|
||||
|
||||
let relPath = typeof row.name === 'string' ? row.name.trim().replace(/\\/g, '/') : ''
|
||||
if (!relPath && uri.startsWith(SKILLS_URI_PREFIX)) {
|
||||
const rest = uri.slice(SKILLS_URI_PREFIX.length)
|
||||
const parts = rest.split('/')
|
||||
// <groupDir>/<skill>/<rel...>
|
||||
if (parts.length >= 3) relPath = parts.slice(2).join('/')
|
||||
}
|
||||
if (!relPath || relPath.includes('..')) return null
|
||||
return { group, skill, relPath, text }
|
||||
}
|
||||
|
||||
function resourcesCapability(capabilities: unknown): boolean {
|
||||
if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) return false
|
||||
return 'resources' in (capabilities as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an MCP HTTP body: application/json, or SSE `data:` frames
|
||||
* (Streamable HTTP when not using json_response).
|
||||
|
|
@ -197,25 +237,79 @@ async function postRpc(
|
|||
return asRpcResult(parseMcpResponseBody(responseText))
|
||||
}
|
||||
|
||||
export interface McpCatalogFetchResult {
|
||||
tools: NetxMcpToolDescriptor[]
|
||||
/**
|
||||
* Set when resources were listed and read successfully (may be empty).
|
||||
* Omitted when the server has no resources capability or the pull failed —
|
||||
* callers should treat that as “no skills” while still keeping tools.
|
||||
*/
|
||||
skills?: CachedSkillFile[]
|
||||
skillsError?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize → notifications/initialized → tools/list
|
||||
* (+ resources/list + resources/read when the server advertises resources).
|
||||
*/
|
||||
export async function fetchMcpCatalog(options: McpRemoteOptions): Promise<McpCatalogFetchResult> {
|
||||
if (!options.mcpUrl) throw new McpRemoteError('mcp url is empty')
|
||||
const init = await postRpc(options, 'initialize', {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: CLIENT_INFO,
|
||||
}) as { capabilities?: unknown }
|
||||
await postRpc(options, 'notifications/initialized', {}, true)
|
||||
const toolsResult = await postRpc(options, 'tools/list', {}) as { tools?: unknown }
|
||||
const toolsRaw = Array.isArray(toolsResult?.tools) ? toolsResult.tools : []
|
||||
const tools: NetxMcpToolDescriptor[] = []
|
||||
for (const item of toolsRaw) {
|
||||
const parsed = parseTool(item)
|
||||
if (parsed) tools.push(parsed)
|
||||
}
|
||||
|
||||
if (!resourcesCapability(init?.capabilities)) {
|
||||
// No resources capability → the server publishes no skills (not an error).
|
||||
return { tools, skills: [] }
|
||||
}
|
||||
|
||||
try {
|
||||
const listed = await postRpc(options, 'resources/list', {}) as { resources?: unknown }
|
||||
const resources = Array.isArray(listed?.resources) ? listed.resources : []
|
||||
const skills: CachedSkillFile[] = []
|
||||
for (const resource of resources) {
|
||||
if (!resource || typeof resource !== 'object' || Array.isArray(resource)) continue
|
||||
const uri = typeof (resource as { uri?: unknown }).uri === 'string'
|
||||
? (resource as { uri: string }).uri.trim()
|
||||
: ''
|
||||
if (!uri.startsWith(SKILLS_URI_PREFIX)) continue
|
||||
const read = await postRpc(options, 'resources/read', { uri }) as { contents?: unknown }
|
||||
const contents = Array.isArray(read?.contents) ? read.contents : []
|
||||
let text = ''
|
||||
for (const block of contents) {
|
||||
if (!block || typeof block !== 'object' || Array.isArray(block)) continue
|
||||
const row = block as { text?: unknown }
|
||||
if (typeof row.text === 'string') {
|
||||
text = row.text
|
||||
break
|
||||
}
|
||||
}
|
||||
const parsed = parseSkillResource(resource, text)
|
||||
if (parsed) skills.push(parsed)
|
||||
}
|
||||
return { tools, skills }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { tools, skillsError: message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize → notifications/initialized → tools/list.
|
||||
*/
|
||||
export async function listMcpTools(options: McpRemoteOptions): Promise<NetxMcpToolDescriptor[]> {
|
||||
if (!options.mcpUrl) throw new McpRemoteError('mcp url is empty')
|
||||
await postRpc(options, 'initialize', {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: CLIENT_INFO,
|
||||
})
|
||||
await postRpc(options, 'notifications/initialized', {}, true)
|
||||
const result = await postRpc(options, 'tools/list', {}) as { tools?: unknown }
|
||||
const tools = Array.isArray(result?.tools) ? result.tools : []
|
||||
const out: NetxMcpToolDescriptor[] = []
|
||||
for (const item of tools) {
|
||||
const parsed = parseTool(item)
|
||||
if (parsed) out.push(parsed)
|
||||
}
|
||||
return out
|
||||
const { tools } = await fetchMcpCatalog(options)
|
||||
return tools
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
* Process-local MCP catalog cache shared by host + preset tool mounts.
|
||||
*
|
||||
* Uses Symbol.for on globalThis so host / agent-tools bundles share one store.
|
||||
* Skill files are mirrored under `$DSH_HOME/netxops-mcp-skills/<key>/`.
|
||||
*/
|
||||
|
||||
import { listMcpTools, mcpEndpointUrl, McpRemoteError } from './mcp-remote.ts'
|
||||
import { fetchMcpCatalog, mcpEndpointUrl, McpRemoteError } from './mcp-remote.ts'
|
||||
import type { NetxMcpCatalogState, NetxMcpToolDescriptor } from './mcp-types.ts'
|
||||
import { replaceSkillsCache } from './skills-cache.ts'
|
||||
|
||||
type Listener = () => void
|
||||
|
||||
|
|
@ -77,8 +79,10 @@ export interface RefreshMcpCatalogInput {
|
|||
}
|
||||
|
||||
/**
|
||||
* Pull tools/list. On failure keep the previous tool list (fromCache) and
|
||||
* surface the error for the Plugins card — never throw to the Host apply path.
|
||||
* Pull tools/list (+ skill resources). On failure keep the previous tool list
|
||||
* (fromCache) and leave the on-disk skill cache untouched. When tools succeed:
|
||||
* a server without resources → clear that address's skill cache; a failed
|
||||
* resources call → keep the last good cache.
|
||||
*/
|
||||
export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<NetxMcpCatalogState> {
|
||||
const mcpUrl = mcpEndpointUrl(input.apiUrl)
|
||||
|
|
@ -109,7 +113,7 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<
|
|||
})
|
||||
|
||||
try {
|
||||
const tools = await listMcpTools({
|
||||
const fetched = await fetchMcpCatalog({
|
||||
mcpUrl,
|
||||
token: input.token,
|
||||
lang: input.lang,
|
||||
|
|
@ -118,10 +122,19 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<
|
|||
fetchImpl: input.fetchImpl,
|
||||
})
|
||||
if (gen !== store().generation) return store().state
|
||||
|
||||
if (fetched.skills !== undefined) {
|
||||
// Includes "no resources capability" (empty list) → wipe stale files.
|
||||
replaceSkillsCache(input.apiUrl, fetched.skills)
|
||||
} else {
|
||||
// Transient resources failure: keep the last good skill cache.
|
||||
console.warn('netxops: mcp skills pull failed, keeping cache — %s', fetched.skillsError)
|
||||
}
|
||||
|
||||
const next: NetxMcpCatalogState = {
|
||||
phase: 'connected',
|
||||
mcpUrl,
|
||||
tools: Object.freeze([...tools]),
|
||||
tools: Object.freeze([...fetched.tools]),
|
||||
error: '',
|
||||
listedAt: new Date().toISOString(),
|
||||
fromCache: false,
|
||||
|
|
@ -135,6 +148,7 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<
|
|||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error)
|
||||
// Offline: keep prior tools in memory and leave disk skill cache as-is.
|
||||
const next: NetxMcpCatalogState = {
|
||||
phase: previous.tools.length > 0 ? 'offline' : 'error',
|
||||
mcpUrl,
|
||||
|
|
|
|||
107
src/netx/skills-cache.ts
Normal file
107
src/netx/skills-cache.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* On-disk cache of netx MCP skill resources, keyed by API URL.
|
||||
*
|
||||
* Layout: `$DSH_HOME/netxops-mcp-skills/<key>/<groupDir>/<skill>/<relPath>`
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import {
|
||||
SKILL_DIR_BY_GROUP,
|
||||
type NetxCapabilityGroupId,
|
||||
} from './capability-groups.ts'
|
||||
import { normalizeApiUrl } from './http.ts'
|
||||
|
||||
export interface CachedSkillFile {
|
||||
group: NetxCapabilityGroupId
|
||||
skill: string
|
||||
/** Posix-relative path inside the skill directory (e.g. `SKILL.md`). */
|
||||
relPath: string
|
||||
text: string
|
||||
}
|
||||
|
||||
function dshHome(): string {
|
||||
const fromEnv = process.env.DSH_HOME?.trim()
|
||||
return fromEnv || join(homedir(), '.dsh')
|
||||
}
|
||||
|
||||
/** Normalize apiUrl the same way connection settings do. */
|
||||
export function normalizeSkillsApiUrl(apiUrl: string): string {
|
||||
const normalized = normalizeApiUrl(apiUrl)
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
/** Stable short key so each netx address gets its own cache tree. */
|
||||
export function skillsCacheKey(apiUrl: string): string {
|
||||
const base = normalizeSkillsApiUrl(apiUrl)
|
||||
return createHash('sha256').update(base).digest('hex').slice(0, 16)
|
||||
}
|
||||
|
||||
/** Absolute cache root for one netx API URL (may not exist yet). */
|
||||
export function skillsCacheDir(apiUrl: string): string {
|
||||
return join(dshHome(), 'netxops-mcp-skills', skillsCacheKey(apiUrl))
|
||||
}
|
||||
|
||||
function isSafeSegment(value: string): boolean {
|
||||
return Boolean(value)
|
||||
&& !value.includes('..')
|
||||
&& !value.includes('/')
|
||||
&& !value.includes('\\')
|
||||
&& !value.includes('\0')
|
||||
}
|
||||
|
||||
function isSafeRelPath(relPath: string): boolean {
|
||||
if (!relPath || relPath.startsWith('/') || relPath.includes('\0')) return false
|
||||
const parts = relPath.replace(/\\/g, '/').split('/')
|
||||
return parts.every((part) => part.length > 0 && part !== '.' && part !== '..')
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the cache for `apiUrl` with `files` (empty → wipe).
|
||||
* Call only after a successful tools/list when skills were resolved
|
||||
* (including “no resources / empty list”).
|
||||
*/
|
||||
export function replaceSkillsCache(apiUrl: string, files: readonly CachedSkillFile[]): string {
|
||||
const root = skillsCacheDir(apiUrl)
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
mkdirSync(root, { recursive: true })
|
||||
let written = 0
|
||||
for (const file of files) {
|
||||
const groupDir = SKILL_DIR_BY_GROUP[file.group]
|
||||
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue
|
||||
const rel = file.relPath.replace(/\\/g, '/')
|
||||
const dest = join(root, groupDir, file.skill, ...rel.split('/'))
|
||||
mkdirSync(dirname(dest), { recursive: true })
|
||||
const text = file.text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||
writeFileSync(dest, text, 'utf8')
|
||||
written += 1
|
||||
}
|
||||
const normalized = normalizeSkillsApiUrl(apiUrl)
|
||||
const meta = {
|
||||
apiUrl: normalized,
|
||||
mcpUrl: normalized ? `${normalized}/mcp` : '',
|
||||
listedAt: new Date().toISOString(),
|
||||
fileCount: written,
|
||||
}
|
||||
writeFileSync(join(root, '.meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8')
|
||||
return root
|
||||
}
|
||||
|
||||
/** True when a previous successful skill pull left a cache directory. */
|
||||
export function hasSkillsCache(apiUrl: string): boolean {
|
||||
const root = skillsCacheDir(apiUrl)
|
||||
if (!existsSync(root)) return false
|
||||
try {
|
||||
return readdirSync(root).some((name) => name !== '.meta.json')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,38 @@
|
|||
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { registerGroupSkills } from '../src/netx/group-skills.ts'
|
||||
import { replaceSkillsCache } from '../src/netx/skills-cache.ts'
|
||||
|
||||
const OPS_SKILL = `---
|
||||
name: netx-ops
|
||||
description: Ops playbook
|
||||
---
|
||||
Body
|
||||
`
|
||||
|
||||
test('registerGroupSkills appends disabled footnote; skips disabled groups', async (t) => {
|
||||
const previous = process.env.DSH_HOME
|
||||
const home = mkdtempSync(join(tmpdir(), 'netxops-skills-disabled-'))
|
||||
process.env.DSH_HOME = home
|
||||
t.after(() => {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
})
|
||||
|
||||
const apiUrl = 'http://127.0.0.1:8890'
|
||||
const root = replaceSkillsCache(apiUrl, [{
|
||||
group: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: OPS_SKILL,
|
||||
}])
|
||||
|
||||
test('registerGroupSkills appends disabled footnote; skips disabled groups', async () => {
|
||||
/** @type {Array<{ name: string, content: string }>} */
|
||||
const registered = []
|
||||
const ctx = {
|
||||
|
|
@ -21,6 +49,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
|
|||
}
|
||||
|
||||
const dispose = await registerGroupSkills(ctx, ['ops'], 'test', {
|
||||
skillsRoot: root,
|
||||
disabledByGroup: { ops: ['netx__execManagedNe'] },
|
||||
lang: 'zh',
|
||||
})
|
||||
|
|
@ -30,6 +59,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
|
|||
|
||||
registered.length = 0
|
||||
const dispose2 = await registerGroupSkills(ctx, [], 'test', {
|
||||
skillsRoot: root,
|
||||
disabledByGroup: { ops: ['netx__execManagedNe'] },
|
||||
})
|
||||
assert.equal(registered.length, 0)
|
||||
|
|
@ -37,6 +67,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
|
|||
|
||||
registered.length = 0
|
||||
const dispose3 = await registerGroupSkills(ctx, ['ops'], 'test', {
|
||||
skillsRoot: root,
|
||||
disabledByGroup: { ops: ['netx__execManagedNe'] },
|
||||
lang: 'en',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,32 +1,350 @@
|
|||
/**
|
||||
* Group skill loader: one capability group ↔ one skill dir.
|
||||
* Group skills: MCP resources → disk cache → registerGroupSkills.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { opsSkillsRoot } from '../src/netx/group-skills.ts'
|
||||
import { SKILL_DIR_BY_GROUP } from '../src/netx/capability-groups.ts'
|
||||
import { registerGroupSkills } from '../src/netx/group-skills.ts'
|
||||
import { refreshMcpCatalog, resetMcpCatalog } from '../src/netx/mcp-store.ts'
|
||||
import { publishNetxConnection } from '../src/netx/runtime.ts'
|
||||
import {
|
||||
hasSkillsCache,
|
||||
replaceSkillsCache,
|
||||
skillsCacheDir,
|
||||
} from '../src/netx/skills-cache.ts'
|
||||
|
||||
test('opsSkillsRoot points at ops / topology playbook dirs', () => {
|
||||
const root = opsSkillsRoot()
|
||||
assert.ok(existsSync(root), root)
|
||||
for (const dir of Object.values(SKILL_DIR_BY_GROUP)) {
|
||||
assert.ok(existsSync(join(root, dir)), join(root, dir))
|
||||
const API = 'http://127.0.0.1:18890'
|
||||
const OPS_SKILL = `---
|
||||
name: netx-ops
|
||||
description: Ops playbook for tests
|
||||
---
|
||||
# Ops
|
||||
|
||||
Use queryNmsAlarms.
|
||||
`
|
||||
|
||||
const OPS_REF = '# Reference\n\nExtra detail.\n'
|
||||
const TOPO_SKILL = `---
|
||||
name: netx-topology
|
||||
description: Topology playbook
|
||||
---
|
||||
# Topology
|
||||
`
|
||||
|
||||
function skillResources(files) {
|
||||
return files.map((f) => ({
|
||||
uri: `netx://skills/${f.groupDir}/${f.skill}/${f.relPath}`,
|
||||
name: f.relPath,
|
||||
mimeType: 'text/markdown',
|
||||
...(f.relPath === 'SKILL.md' ? { description: f.description || f.skill } : {}),
|
||||
_meta: {
|
||||
netx: {
|
||||
group: f.group,
|
||||
skill: f.skill,
|
||||
entry: f.relPath === 'SKILL.md',
|
||||
},
|
||||
},
|
||||
text: f.text,
|
||||
}))
|
||||
}
|
||||
|
||||
function startMockMcp({
|
||||
tools = [{ name: 'queryNmsAlarms', description: 'alarms', readOnlyHint: true, group: 'ops' }],
|
||||
resources = null,
|
||||
failResources = false,
|
||||
token = 'secret',
|
||||
} = {}) {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const chunks = []
|
||||
for await (const chunk of req) chunks.push(chunk)
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
const auth = req.headers.authorization ?? ''
|
||||
if (token && auth !== `Bearer ${token}`) {
|
||||
res.writeHead(401)
|
||||
res.end('unauthorized')
|
||||
return
|
||||
}
|
||||
const reply = (result) => {
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ jsonrpc: '2.0', id: body.id ?? 1, result }))
|
||||
}
|
||||
if (body.method === 'initialize') {
|
||||
reply({
|
||||
protocolVersion: '2025-03-26',
|
||||
capabilities: resources === null && !failResources
|
||||
? { tools: {} }
|
||||
: { tools: {}, resources: {} },
|
||||
serverInfo: { name: 'mock-netx', version: '0.0.0' },
|
||||
})
|
||||
return
|
||||
}
|
||||
if (body.method === 'notifications/initialized') {
|
||||
res.writeHead(202)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
if (body.method === 'tools/list') {
|
||||
reply({
|
||||
tools: tools.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
annotations: { readOnlyHint: t.readOnlyHint !== false },
|
||||
_meta: { netx: { group: t.group } },
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (body.method === 'resources/list') {
|
||||
if (failResources) {
|
||||
res.writeHead(503)
|
||||
res.end('resources down')
|
||||
return
|
||||
}
|
||||
const list = Array.isArray(resources) ? resources : []
|
||||
reply({
|
||||
resources: list.map(({ text: _t, ...rest }) => rest),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (body.method === 'resources/read') {
|
||||
if (failResources) {
|
||||
res.writeHead(503)
|
||||
res.end('resources down')
|
||||
return
|
||||
}
|
||||
const list = Array.isArray(resources) ? resources : []
|
||||
const hit = list.find((r) => r.uri === body.params?.uri)
|
||||
if (!hit) {
|
||||
res.writeHead(400)
|
||||
res.end('missing')
|
||||
return
|
||||
}
|
||||
reply({
|
||||
contents: [{ uri: hit.uri, mimeType: 'text/markdown', text: hit.text }],
|
||||
})
|
||||
return
|
||||
}
|
||||
res.writeHead(400)
|
||||
res.end('unknown method')
|
||||
})
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const { port } = server.address()
|
||||
resolve({
|
||||
server,
|
||||
apiUrl: `http://127.0.0.1:${port}`,
|
||||
url: `http://127.0.0.1:${port}/mcp`,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function fakeSkillsCtx() {
|
||||
/** @type {Array<{ name: string, content: string, path?: string, resourceBase?: { path: string } }>} */
|
||||
const registered = []
|
||||
const ctx = {
|
||||
skills: {
|
||||
register(skill) {
|
||||
registered.push(skill)
|
||||
return () => {}
|
||||
},
|
||||
},
|
||||
logger: { warn() {}, info() {} },
|
||||
}
|
||||
return { ctx, registered }
|
||||
}
|
||||
|
||||
function withTempHome(t) {
|
||||
const previous = process.env.DSH_HOME
|
||||
const home = mkdtempSync(join(tmpdir(), 'netxops-skills-'))
|
||||
process.env.DSH_HOME = home
|
||||
t.after(() => {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
resetMcpCatalog()
|
||||
})
|
||||
return home
|
||||
}
|
||||
|
||||
const FULL_RESOURCES = skillResources([
|
||||
{
|
||||
group: 'ops',
|
||||
groupDir: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: OPS_SKILL,
|
||||
description: 'Ops playbook for tests',
|
||||
},
|
||||
{
|
||||
group: 'ops',
|
||||
groupDir: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'reference.md',
|
||||
text: OPS_REF,
|
||||
},
|
||||
{
|
||||
group: 'topology',
|
||||
groupDir: 'topology',
|
||||
skill: 'netx-topology',
|
||||
relPath: 'SKILL.md',
|
||||
text: TOPO_SKILL,
|
||||
description: 'Topology playbook',
|
||||
},
|
||||
])
|
||||
|
||||
test('refresh + register: normal skill with reference.md', async (t) => {
|
||||
withTempHome(t)
|
||||
const mock = await startMockMcp({ resources: FULL_RESOURCES })
|
||||
t.after(() => mock.server.close())
|
||||
publishNetxConnection({
|
||||
apiUrl: mock.apiUrl,
|
||||
token: 'secret',
|
||||
lang: 'zh',
|
||||
toolCallTimeoutMs: 5_000,
|
||||
toolsEnabled: true,
|
||||
})
|
||||
const catalog = await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
assert.equal(catalog.phase, 'connected')
|
||||
assert.equal(catalog.tools.length, 1)
|
||||
const cache = skillsCacheDir(mock.apiUrl)
|
||||
assert.ok(existsSync(join(cache, 'ops', 'netx-ops', 'SKILL.md')))
|
||||
assert.ok(existsSync(join(cache, 'ops', 'netx-ops', 'reference.md')))
|
||||
assert.equal(readFileSync(join(cache, 'ops', 'netx-ops', 'reference.md'), 'utf8'), OPS_REF)
|
||||
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
const dispose = await registerGroupSkills(ctx, ['ops'], 'test')
|
||||
assert.equal(registered.length, 1)
|
||||
assert.equal(registered[0].name, 'netx-ops')
|
||||
assert.match(registered[0].content, /queryNmsAlarms/)
|
||||
assert.equal(registered[0].resourceBase?.path, join(cache, 'ops', 'netx-ops'))
|
||||
assert.ok(registered[0].path?.endsWith('SKILL.md'))
|
||||
dispose()
|
||||
})
|
||||
|
||||
test('each group dir has a playbook with name + description', () => {
|
||||
const root = opsSkillsRoot()
|
||||
for (const group of Object.keys(SKILL_DIR_BY_GROUP)) {
|
||||
const groupDir = join(root, SKILL_DIR_BY_GROUP[group])
|
||||
const bundles = readdirSync(groupDir).filter((name) => existsSync(join(groupDir, name, 'SKILL.md')))
|
||||
assert.ok(bundles.length >= 1, group)
|
||||
const raw = readFileSync(join(groupDir, bundles[0], 'SKILL.md'), 'utf8')
|
||||
assert.match(raw, /^---\r?\n/)
|
||||
assert.match(raw, /\nname:\s+\S+/)
|
||||
assert.match(raw, /\ndescription:\s/)
|
||||
}
|
||||
test('server without resources capability → no skills, tools still ok', async (t) => {
|
||||
withTempHome(t)
|
||||
// Pre-seed stale cache that must be cleared on successful tools refresh.
|
||||
replaceSkillsCache(API, [{
|
||||
group: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: OPS_SKILL,
|
||||
}])
|
||||
assert.equal(hasSkillsCache(API), true)
|
||||
|
||||
const mock = await startMockMcp({ resources: null })
|
||||
t.after(() => mock.server.close())
|
||||
const catalog = await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
assert.equal(catalog.phase, 'connected')
|
||||
assert.equal(catalog.tools.length, 1)
|
||||
assert.equal(hasSkillsCache(mock.apiUrl), false)
|
||||
|
||||
publishNetxConnection({
|
||||
apiUrl: mock.apiUrl, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000,
|
||||
})
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
await registerGroupSkills(ctx, ['ops'], 'test')
|
||||
assert.equal(registered.length, 0)
|
||||
})
|
||||
|
||||
test('resources call failure keeps prior skill cache, tools still ok', async (t) => {
|
||||
withTempHome(t)
|
||||
const mock = await startMockMcp({ failResources: true })
|
||||
t.after(() => mock.server.close())
|
||||
replaceSkillsCache(mock.apiUrl, [{
|
||||
group: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: OPS_SKILL,
|
||||
}])
|
||||
const catalog = await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
assert.equal(catalog.phase, 'connected')
|
||||
assert.equal(catalog.tools.length, 1)
|
||||
assert.equal(hasSkillsCache(mock.apiUrl), true)
|
||||
})
|
||||
|
||||
test('offline refresh keeps prior skill cache', async (t) => {
|
||||
withTempHome(t)
|
||||
const mock = await startMockMcp({ resources: FULL_RESOURCES })
|
||||
const ok = await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
assert.equal(ok.phase, 'connected')
|
||||
mock.server.close()
|
||||
|
||||
const offline = await startMockMcp({ failResources: true, tools: [] })
|
||||
// Fail tools/list entirely
|
||||
offline.server.close()
|
||||
const down = http.createServer((_req, res) => {
|
||||
res.writeHead(503)
|
||||
res.end('down')
|
||||
})
|
||||
await new Promise((resolve) => down.listen(0, '127.0.0.1', resolve))
|
||||
const { port } = down.address()
|
||||
t.after(() => down.close())
|
||||
// Same apiUrl as successful pull so cache key matches — use mock.apiUrl host
|
||||
// but port changed; instead keep using the first mock's cache by refreshing
|
||||
// the same apiUrl against a dead server.
|
||||
const deadUrl = mock.apiUrl
|
||||
const next = await refreshMcpCatalog({ apiUrl: deadUrl, token: 'secret', timeoutMs: 500 })
|
||||
assert.ok(next.phase === 'offline' || next.phase === 'error')
|
||||
assert.equal(hasSkillsCache(deadUrl), true)
|
||||
publishNetxConnection({
|
||||
apiUrl: deadUrl, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000,
|
||||
})
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
await registerGroupSkills(ctx, ['ops'], 'test')
|
||||
assert.equal(registered[0]?.name, 'netx-ops')
|
||||
})
|
||||
|
||||
test('refresh clears deleted skills from cache', async (t) => {
|
||||
withTempHome(t)
|
||||
const slim = skillResources([
|
||||
{
|
||||
group: 'ops',
|
||||
groupDir: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: OPS_SKILL,
|
||||
description: 'Ops playbook for tests',
|
||||
},
|
||||
])
|
||||
const mock = await startMockMcp({ resources: slim })
|
||||
t.after(() => mock.server.close())
|
||||
replaceSkillsCache(mock.apiUrl, [
|
||||
{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL },
|
||||
{ group: 'topology', skill: 'netx-topology', relPath: 'SKILL.md', text: TOPO_SKILL },
|
||||
])
|
||||
assert.ok(existsSync(join(skillsCacheDir(mock.apiUrl), 'topology', 'netx-topology', 'SKILL.md')))
|
||||
await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
assert.ok(existsSync(join(skillsCacheDir(mock.apiUrl), 'ops', 'netx-ops', 'SKILL.md')))
|
||||
assert.equal(existsSync(join(skillsCacheDir(mock.apiUrl), 'topology', 'netx-topology', 'SKILL.md')), false)
|
||||
})
|
||||
|
||||
test('group off → skill not registered', async (t) => {
|
||||
withTempHome(t)
|
||||
replaceSkillsCache(API, [
|
||||
{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL },
|
||||
{ group: 'topology', skill: 'netx-topology', relPath: 'SKILL.md', text: TOPO_SKILL },
|
||||
])
|
||||
publishNetxConnection({
|
||||
apiUrl: API, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000,
|
||||
})
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
await registerGroupSkills(ctx, ['topology'], 'test')
|
||||
assert.equal(registered.length, 1)
|
||||
assert.equal(registered[0].name, 'netx-topology')
|
||||
registered.length = 0
|
||||
await registerGroupSkills(ctx, [], 'test')
|
||||
assert.equal(registered.length, 0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import http from 'node:http'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import {
|
||||
filterCatalogTools,
|
||||
|
|
@ -16,6 +19,17 @@ import { listMcpTools, callMcpTool, mcpEndpointUrl } from '../src/netx/mcp-remot
|
|||
import { refreshMcpCatalog, getMcpCatalogState, resetMcpCatalog } from '../src/netx/mcp-store.ts'
|
||||
import { DEFAULT_CAPABILITY_GROUPS } from '../src/netx/capability-groups.ts'
|
||||
|
||||
function withTempDshHome(t) {
|
||||
const previous = process.env.DSH_HOME
|
||||
const home = mkdtempSync(join(tmpdir(), 'netxops-mcp-catalog-'))
|
||||
process.env.DSH_HOME = home
|
||||
t.after(() => {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
if (previous === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previous
|
||||
})
|
||||
}
|
||||
|
||||
const SAMPLE = Object.freeze([
|
||||
{
|
||||
name: 'queryNmsAlarms',
|
||||
|
|
@ -196,7 +210,29 @@ test('listMcpTools + callMcpTool against mock Streamable HTTP server', async ()
|
|||
}
|
||||
})
|
||||
|
||||
test('refreshMcpCatalog keeps cache on disconnect', async () => {
|
||||
test('listMcpTools drops group=meta tools (getNetxSkill)', async () => {
|
||||
const withMeta = [
|
||||
...SAMPLE,
|
||||
{
|
||||
name: 'getNetxSkill',
|
||||
description: 'read skill',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
readOnlyHint: true,
|
||||
group: 'meta',
|
||||
},
|
||||
]
|
||||
const mock = await startMockMcp(withMeta)
|
||||
try {
|
||||
const tools = await listMcpTools({ mcpUrl: mock.url, token: 'secret' })
|
||||
assert.equal(tools.length, SAMPLE.length)
|
||||
assert.equal(tools.some((t) => t.name === 'getNetxSkill'), false)
|
||||
} finally {
|
||||
mock.server.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('refreshMcpCatalog keeps cache on disconnect', async (t) => {
|
||||
withTempDshHome(t)
|
||||
resetMcpCatalog()
|
||||
const mock = await startMockMcp(SAMPLE)
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { publishNetxConnection } from '../src/netx/runtime.ts'
|
||||
import { replaceSkillsCache } from '../src/netx/skills-cache.ts'
|
||||
|
||||
const STORE_KEY = Symbol.for('dsh-netxops.mcp-catalog-store')
|
||||
|
||||
|
|
@ -86,13 +87,41 @@ export function fullCatalog() {
|
|||
]
|
||||
}
|
||||
|
||||
const DEFAULT_API = 'http://127.0.0.1:8890'
|
||||
|
||||
const MINIMAL_SKILLS = Object.freeze([
|
||||
{
|
||||
group: 'ops',
|
||||
skill: 'netx-ops',
|
||||
relPath: 'SKILL.md',
|
||||
text: '---\nname: netx-ops\ndescription: Ops playbook\n---\nOps body\n',
|
||||
},
|
||||
{
|
||||
group: 'topology',
|
||||
skill: 'netx-topology',
|
||||
relPath: 'SKILL.md',
|
||||
text: '---\nname: netx-topology\ndescription: Topology playbook\n---\nTopology body\n',
|
||||
},
|
||||
{
|
||||
group: 'bizMonitor',
|
||||
skill: 'netx-biz-monitor',
|
||||
relPath: 'SKILL.md',
|
||||
text: '---\nname: netx-biz-monitor\ndescription: Biz monitor playbook\n---\nBiz body\n',
|
||||
},
|
||||
])
|
||||
|
||||
/** Write a minimal MCP skills cache for `apiUrl` (default test netx). */
|
||||
export function seedSkillsCache(apiUrl = DEFAULT_API) {
|
||||
return replaceSkillsCache(apiUrl, MINIMAL_SKILLS)
|
||||
}
|
||||
|
||||
/** Install a frozen catalog into the shared Symbol.for store (no network). */
|
||||
export function seedMcpCatalog(tools = fullCatalog()) {
|
||||
const root = globalThis
|
||||
const store = {
|
||||
state: {
|
||||
phase: 'connected',
|
||||
mcpUrl: 'http://127.0.0.1:8890/mcp',
|
||||
mcpUrl: `${DEFAULT_API}/mcp`,
|
||||
tools: Object.freeze([...tools]),
|
||||
error: '',
|
||||
listedAt: new Date().toISOString(),
|
||||
|
|
@ -102,6 +131,7 @@ export function seedMcpCatalog(tools = fullCatalog()) {
|
|||
generation: 1,
|
||||
}
|
||||
root[STORE_KEY] = store
|
||||
seedSkillsCache(DEFAULT_API)
|
||||
return store.state
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ test('the npm tarball installs offline and supplies every exported artifact and
|
|||
for (const path of Object.values(pkg.exports).filter(path => !path.includes('*'))) assert.ok(names.has(path.slice(2)), `Missing packed export ${path}`)
|
||||
for (const path of ['icon.svg', 'locale/en.json', 'locale/zh.json', pkg.dsh.bundle.patch.slice(2),
|
||||
'presets/netxops/PERSONA.md', 'presets/netxops/preset.yml',
|
||||
'presets/netxops/skills/ops/netx-ops/SKILL.md',
|
||||
'presets/netxops/skills/topology/netx-topology/SKILL.md',
|
||||
'presets/netxops/skills/biz-monitor/netx-biz-monitor/SKILL.md',
|
||||
]) assert.ok(names.has(path), `Missing packed resource ${path}`)
|
||||
for (const path of names) assert.doesNotMatch(path, /^(?:src|test|tests|examples|scripts|node_modules)\/|\.env|\.npmrc/, `Private/developer file included: ${path}`)
|
||||
for (const path of names) {
|
||||
assert.doesNotMatch(path, /^presets\/netxops\/skills\//, `Bundled group skill must not ship: ${path}`)
|
||||
assert.doesNotMatch(path, /^(?:src|test|tests|examples|scripts|node_modules)\/|\.env|\.npmrc/, `Private/developer file included: ${path}`)
|
||||
}
|
||||
const profile = join(directory, '全新安装 profile with spaces')
|
||||
mkdirSync(profile)
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify({ name: 'isolated-netxops-profile', private: true }))
|
||||
|
|
@ -78,42 +78,46 @@ test('the npm tarball installs offline and supplies every exported artifact and
|
|||
} catch { /* Try a complete Host generation, never mix peers. */ }
|
||||
}
|
||||
if (!peers) { console.log('No installed Harness runtime: offline installation verified; Host activation requires a local Harness.'); return }
|
||||
// A neighboring package must never override the skills shipped to users.
|
||||
const trap = join(profile, 'node_modules', 'netx', 'skills', 'ops', 'netx-ops')
|
||||
mkdirSync(trap, { recursive: true })
|
||||
writeFileSync(join(trap, 'SKILL.md'), '---\nname: unexpected-neighbor\ndescription: Must not load\n---\nwrong package')
|
||||
const loader = join(directory, 'host-loader.mjs')
|
||||
writeFileSync(loader, `let peers; export function initialize(data) { peers = data }
|
||||
export async function resolve(name, ctx, next) { return peers[name] ? { url: peers[name], shortCircuit: true } : next(name, ctx) }`)
|
||||
const smoke = join(directory, 'activate-installed.mjs')
|
||||
writeFileSync(smoke, `import assert from 'node:assert/strict'; import { register } from 'node:module';
|
||||
writeFileSync(smoke, `import assert from 'node:assert/strict'; import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path';
|
||||
import { register } from 'node:module';
|
||||
register(${JSON.stringify(pathToFileURL(loader).href)}, { data: ${JSON.stringify(peers)} });
|
||||
const { Context, Service } = await import('@deepseek-ai/cordis');
|
||||
const host = await import(${JSON.stringify(pathToFileURL(join(installed, 'lib/index.js')).href)});
|
||||
const root = new Context(), tools = new Map(), skills = new Map(), owners = [];
|
||||
class Tools extends Service { constructor(ctx) { super(ctx, 'tools') } register(entry) { assert.ok(!tools.has(entry.name)); tools.set(entry.name, entry); return () => tools.delete(entry.name) } }
|
||||
class Skills extends Service { constructor(ctx) { super(ctx, 'skills') } register(entry) { assert.ok(!skills.has(entry.name)); skills.set(entry.name, entry); return () => skills.delete(entry.name) } }
|
||||
// Seed a cached /mcp catalog so host registration does not need a live netx.
|
||||
// Seed a cached /mcp catalog + on-disk skills cache (no live netx).
|
||||
const STORE = Symbol.for('dsh-netxops.mcp-catalog-store');
|
||||
const ops = ${JSON.stringify([...OPS_TOOL_NAMES_INLINE])};
|
||||
const apiUrl = 'http://127.0.0.1:8890';
|
||||
globalThis[STORE] = { generation: 1, listeners: new Set(), state: {
|
||||
phase: 'connected', mcpUrl: 'http://127.0.0.1:8890/mcp', error: '', listedAt: new Date().toISOString(), fromCache: false,
|
||||
phase: 'connected', mcpUrl: apiUrl + '/mcp', error: '', listedAt: new Date().toISOString(), fromCache: false,
|
||||
tools: ops.map(name => ({ name, description: name, inputSchema: { type: 'object', properties: {} },
|
||||
readOnlyHint: name !== 'execManagedNe', group: 'ops' })),
|
||||
} };
|
||||
const dshHome = process.env.DSH_HOME;
|
||||
const key = createHash('sha256').update(apiUrl).digest('hex').slice(0, 16);
|
||||
const skillDir = join(dshHome, 'netxops-mcp-skills', key, 'ops', 'netx-ops');
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(join(skillDir, 'SKILL.md'), '---\\nname: netx-ops\\ndescription: Ops playbook\\n---\\nOps body\\n');
|
||||
owners.push(await root.plugin(Tools), await root.plugin(Skills));
|
||||
const owner = await root.plugin(host, { installAgentPreset: false, alarmPushEnabled: false, groupOpsPublic: true }); owners.push(owner);
|
||||
assert.equal(owner.state, 2); assert.equal(tools.size, 15);
|
||||
await new Promise(resolve => setTimeout(resolve, 80));
|
||||
assert.ok(skills.has('netx-ops')); assert.equal(skills.has('unexpected-neighbor'), false);
|
||||
assert.ok(skills.get('netx-ops').path.startsWith(${JSON.stringify(installed)}));
|
||||
assert.ok(skills.has('netx-ops'));
|
||||
assert.ok(skills.get('netx-ops').path.startsWith(dshHome));
|
||||
for (const path of ${JSON.stringify(Object.values(pkg.exports).filter(path => path.endsWith('.js') && path !== './lib/client.js'))}) {
|
||||
const api = await import(new URL(path, ${JSON.stringify(pathToFileURL(join(installed, 'package.json')).href)})); assert.equal(typeof api.apply, 'function');
|
||||
}
|
||||
for (const owner of owners.reverse()) await owner.dispose();
|
||||
await new Promise(resolve => setTimeout(resolve, 40)); assert.equal(tools.size, 0); assert.equal(skills.size, 0);
|
||||
console.log('Installed Host active: 15 tools, bundled ops skill, clean unload.');`)
|
||||
run([smoke], profile, { DSH_HOME: join(directory, 'isolated-dsh-home'), NETX_SKILLS_ROOT: '' })
|
||||
console.log('Installed Host active: 15 tools, cached ops skill, clean unload.');`)
|
||||
run([smoke], profile, { DSH_HOME: join(directory, 'isolated-dsh-home') })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue