diff --git a/.release-build.json b/.release-build.json index c0a0772..2191440 100644 --- a/.release-build.json +++ b/.release-build.json @@ -2,7 +2,7 @@ "format": 1, "builds": { "lib/agent-tools-biz-monitor.js": { - "output": "11fd312eeb902daab190770bf9465fcbd18ff97e48ca2d4e86144cffa6691790", + "output": "b9ab81312e2443d43b283116ac7f981666f5673d9d550141f264a99702ea7398", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -11,9 +11,9 @@ "src/agent-tools-biz-monitor.ts": "201bd447198d5ee7cb1aa25031a3f6054fe12a8a221edd4915752696cca1537d", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -24,18 +24,18 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-common.js": { - "output": "9e40a55c9c823b76c8060aa5f328e89b1411091f1a23841e63505deb2de3e316", + "output": "6cf374696df26b8519e4e43b33dc00991e6ad5dbb1eae442c446d68849a1d440", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -44,9 +44,9 @@ "src/agent-tools-common.ts": "cf463e940bc2042d7462502de93d45380550da54f1965fa684d72f71b16bc41b", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -57,18 +57,18 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-nms.js": { - "output": "126ef9bc22cbe2814442efd68a0ff83ce29fdac7f9baed9602a7a186ef29db0f", + "output": "f7fd09a4fb4fe79218c22567ffb806319271c47ad9de5fbcac3748562adbda60", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -77,9 +77,9 @@ "src/agent-tools-nms.ts": "6505e51bfb2948756440b00724b19d907d410d34ef1dcbf08019774c0f9d7ef5", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -90,18 +90,18 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-ops.js": { - "output": "56fd1c68147c6d48cca50535f67ce3807dab5f35db4ca9d675fe8db731d9f1d2", + "output": "59218d9c32d97a92cf90c4a6c6d5f184f04162a93aada26d408e7d90debc93ca", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -110,9 +110,9 @@ "src/agent-tools-ops.ts": "2f0331106de53f3dcdd8d61ce2d9da363f0a9b854f1dd7d98e27997c82d98a77", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -123,18 +123,18 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-topology.js": { - "output": "7b683d38bbab4764684d88e6f0989177e60789e6b8923438b1c860c75d54fc26", + "output": "b04b4ab6ed44cbb233f0d8c37d26d24d5336432d1ca8b950970a69aafdefdd5b", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -143,9 +143,9 @@ "src/agent-tools-topology.ts": "22bf5f1c71a5b1d33bd3a894bca2c6e4c2fc72072c8a9d7bbc00e214dfafa9f8", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -156,18 +156,18 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools.js": { - "output": "e831d2c6a671a9b4bcc2449a0e84772446848372925567704cdb0590a2bb7071", + "output": "89f59175ba51d39a0fbd48e825e83470eadd84ef5378c8c29c51ab0cde3845d6", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -176,9 +176,9 @@ "src/agent-tools.ts": "b7e4f8476ba9c5f2754d859074c94129642d18883da293b80e310c09336bc5c5", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", @@ -189,31 +189,31 @@ "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", - "src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", - "src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/client.js": { - "output": "76d73a9e8354e55c27d60c90d17206b7d9eb096829a64cbfc15ab1152c7db49b", + "output": "bec095ff7f28d3eb97152486275c93badc43fe687377c5d0e3267b355add2e2d", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", - "src/client/NetxopsCard.tsx": "51d85f150fd13fc5377de29a4e80b992e860514e0700884218f9f9c7bdcd4cc1", + "src/client/NetxopsCard.tsx": "d4c4e795949496f5f913914c23c338bdbc81ea2b721a55d835815f2eae13f667", "src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31", "src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9", "src/client/controller.ts": "d3353feda6f6b004dd094b1fc90d743003b55ce5a9d0b46c8fb9c1eb33925cc1", "src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117", "src/client/index.ts": "1f73ae8e8c02e13b47c5679869359444485b4621eca20a15c632289f22e5d760", "src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10", - "src/client/locales.ts": "b9aa9cac558119c3a81e773dc3b49378f0c37fb2220696a484a92bf19aabb5d6", + "src/client/locales.ts": "35106f934baaad22cf5e7afdebe415ef198af29f4831ad9dfa7ff2d0235cf4d9", "src/client/mcp-catalog-view.ts": "3066ba1fd56b19e099e1f58ba9699a4d17de9f5733e27a4f4da0c4f58809a545", "src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd", "src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb", @@ -222,7 +222,7 @@ "src/client/tools-status-view.ts": "4ff296641164124748356a24e0d38ec88f09ee9826bf1a2ab51159026a2b2ca8", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917", "src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", @@ -231,14 +231,14 @@ } }, "lib/index.js": { - "output": "515aafc5a26d8680cedbe970751b5f565e098c8f691014fb0f9a1fe9edd6f82f", + "output": "ffdddb5bd035a1ef3f268efd6590e472f32d99ad9105da056d9d00852c6adf17", "inputs": { "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", "src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc", - "src/index.ts": "f583927e67045d83362930d8c416d540c1d8b08b18b2ad78e2a3d72ea03df559", + "src/index.ts": "86d6ab02fb50b59de89e8ae84bb8c65978d715968f4c721f779e86b60d23a13e", "src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62", "src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf", "src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f", @@ -246,10 +246,10 @@ "src/netx/alarm-session.ts": "0df97efc3debb262573c19ade84d9d50584d58b21fa2c5ae5badd382915f18e3", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", - "src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", + "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/heal-profile-entry.ts": "18c2b635f727bf41d38c7c38fcc0ce0c5391c9d98b2d662db354f1908defb6be", - "src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", + "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd", "src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082", "src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", @@ -262,16 +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": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", + "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", - "src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", + "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff", "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": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", + "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139", "src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5" diff --git a/lib/agent-tools-biz-monitor.js b/lib/agent-tools-biz-monitor.js index bdc5713..c7c5e6d 100644 --- a/lib/agent-tools-biz-monitor.js +++ b/lib/agent-tools-biz-monitor.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/agent-tools-common.js b/lib/agent-tools-common.js index 1f667ab..44af07c 100644 --- a/lib/agent-tools-common.js +++ b/lib/agent-tools-common.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/agent-tools-nms.js b/lib/agent-tools-nms.js index 98c2bbf..18a0297 100644 --- a/lib/agent-tools-nms.js +++ b/lib/agent-tools-nms.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/agent-tools-ops.js b/lib/agent-tools-ops.js index 3d592b9..aa6c954 100644 --- a/lib/agent-tools-ops.js +++ b/lib/agent-tools-ops.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/agent-tools-topology.js b/lib/agent-tools-topology.js index d6101ed..c640d95 100644 --- a/lib/agent-tools-topology.js +++ b/lib/agent-tools-topology.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/agent-tools.js b/lib/agent-tools.js index 0741c2c..39260b2 100644 --- a/lib/agent-tools.js +++ b/lib/agent-tools.js @@ -1,5 +1,5 @@ // src/netx/group-tools-plugin.ts -import { readFileSync as readFileSync6 } from "node:fs"; +import { readFileSync as readFileSync7 } from "node:fs"; import { dirname as dirname6, join as join11 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; @@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") { // src/netx/skills-cache.ts import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -293,6 +302,7 @@ function normalizeApiUrl(raw) { } // src/netx/skills-cache.ts +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join(homedir(), ".dsh"); @@ -305,9 +315,46 @@ 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)); +function cacheParent() { + return join(dshHome(), "netxops-mcp-skills"); } +function skillsCacheDir(apiUrl) { + return join(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +var summaryMemo = new Map; // src/netx/mcp-store.ts var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); @@ -512,12 +559,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -527,7 +574,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -593,11 +645,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 { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs"; import { join as join5 } from "node:path"; // src/netx/kb-manifest.ts -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, @@ -624,7 +676,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: existsSync(absolute) }; + return { key, relative: rel, absolute, exists: existsSync2(absolute) }; }); } function renderKbPathTable(snapshot) { @@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; +import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs"; import { join as join4, relative, resolve as resolve2, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", @@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync2(checked.absolutePath)) { + if (existsSync3(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { throw new Error(`path not under writable roots: ${realRel}`); } - if (!statSync2(realFile).isFile()) { + if (!statSync3(realFile).isFile()) { throw new Error(`not a file: ${realFile}`); } return { @@ -813,7 +865,7 @@ function readKbIdentity(snapshot) { const dir = join5(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 []; } @@ -825,9 +877,9 @@ function readKbIdentity(snapshot) { const full = join5(dir, name); let text; try { - if (!statSync3(full).isFile()) + if (!statSync4(full).isFile()) continue; - text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); + text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim(); } catch { continue; } @@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync3, - mkdirSync, - readdirSync as readdirSync3, - readFileSync as readFileSync3, - rmSync, - statSync as statSync4, - writeFileSync + existsSync as existsSync4, + mkdirSync as mkdirSync2, + readdirSync as readdirSync4, + readFileSync as readFileSync4, + rmSync as rmSync2, + statSync as statSync5, + writeFileSync as writeFileSync2 } from "node:fs"; import { dirname as dirname3, join as join6 } from "node:path"; function requireLocalRoot(snapshot) { @@ -1004,12 +1056,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1023,12 +1075,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1040,12 +1092,12 @@ 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 (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1059,12 +1111,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync3(target.absolutePath) && !args.overwrite) { + if (existsSync4(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 = existsSync3(target.absolutePath); - writeFileSync(target.absolutePath, args.body.endsWith(` + mkdirSync2(dirname3(target.absolutePath), { recursive: true }); + const existed = existsSync4(target.absolutePath); + writeFileSync2(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1077,13 +1129,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync(target.absolutePath, body, "utf8"); + writeFileSync2(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync3(target.absolutePath)) { + if (!existsSync4(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", @@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) { function walkFiles(dir, baseLocal, out) { let entries; try { - entries = readdirSync3(dir); + entries = readdirSync4(dir); } catch { return; } @@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) { const full = join6(dir, name); let st; try { - st = statSync4(full); + st = statSync5(full); } catch { continue; } @@ -1152,7 +1204,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 { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs"; 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"]); @@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) { } function readKbFile(snapshot, args) { const target = resolveKbReadPath(snapshot, args.path); - const st = statSync5(target.absolutePath); + 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 = statSync5(join7(target.absolutePath, entry.name)).size; + size = statSync6(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); @@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { throw new Error(`not a text KB file: ${target.relativePath}`); } - const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` + const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` `).split(` `); const start = Math.max(1, Math.floor(args.offset ?? 1)); @@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) { const hit = cache.files.get(path); if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit; - const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); + const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, ""); if (hit) cache.chars -= hit.text.length; if (cache.chars + text.length > CACHE_MAX_CHARS) { @@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) { function walkTextFiles(dir, out) { let entries; try { - entries = readdirSync4(dir, { withFileTypes: true }); + entries = readdirSync5(dir, { withFileTypes: true }); } catch { return; } @@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) { if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; try { - const st = statSync5(full); + const st = statSync6(full); if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); } catch {} @@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) { const files = []; const seen = new Set; for (const dir of dirs) { - const st = statSync5(dir); + const st = statSync6(dir); const batch = []; if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); @@ -1432,7 +1484,7 @@ function toLosslessJson(value) { } // src/netx/tool-mount-status.ts -import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs"; 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"); @@ -1459,8 +1511,8 @@ function getToolMountStatus() { function persist() { try { 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)} + mkdirSync3(dirname4(path), { recursive: true }); + writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} `, "utf8"); } catch {} } @@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) { } // src/netx/tools.ts -import { readFileSync as readFileSync5 } from "node:fs"; +import { readFileSync as readFileSync6 } from "node:fs"; import { dirname as dirname5, join as join10 } from "node:path"; import { fileURLToPath } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; @@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) { function packageVersion() { try { const here = dirname5(fileURLToPath(import.meta.url)); - const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; @@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) { function packageVersion2() { try { const here = dirname6(fileURLToPath2(import.meta.url)); - const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); + const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8")); return typeof pkg.version === "string" ? pkg.version : "0"; } catch { return "0"; diff --git a/lib/client.js b/lib/client.js index 48d9746..a3f13ab 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1070,6 +1070,40 @@ function serializeDisabledTools(names) { return JSON.stringify([...new Set(names.map((n) => n.trim()).filter(Boolean))].sort()); } +// 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}` }; +} +function isInsecureRemoteApiUrl(raw) { + let url; + try { + url = new URL(raw.trim()); + } catch { + return false; + } + if (url.protocol !== "http:") + return false; + const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase(); + return !(host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\./.test(host)); +} + // src/client/NetxopsCard.tsx var jsx_runtime = require("react/jsx-runtime"); function phaseLocaleKey(phase) { @@ -1641,6 +1675,10 @@ function NetxopsCard(props) { props.resetField("apiUrl"); } }), + isInsecureRemoteApiUrl(state.apiUrl.text) ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-invalid", + children: t("apiUrlInsecure") + }) : null, /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldGrid", children: [ @@ -2161,28 +2199,6 @@ function NetxopsCard(props) { }); } -// 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/client/snapshot-store.ts function createSnapshotStore(init) { let state = init; @@ -3228,6 +3244,7 @@ var en = { sectionExport: "Session export", apiUrl: "API URL", apiUrlHint: "http://127.0.0.1:8890", + apiUrlInsecure: "Plain HTTP to a remote host: the token and the skills sent to the model are not encrypted. Use https://.", apiUrlInvalid: "Enter an HTTP or HTTPS URL without credentials.", lang: "API / alarm language", thinkingLanguage: "Thinking language", @@ -3324,6 +3341,7 @@ var zh = { sectionExport: "会话导出", apiUrl: "API 地址", apiUrlHint: "http://127.0.0.1:8890", + apiUrlInsecure: "连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。", apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。", lang: "API / 告警语言", thinkingLanguage: "思考语言", diff --git a/lib/index.js b/lib/index.js index 361e3c1..209efe0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1398,6 +1398,7 @@ import { } from "node:fs"; import { homedir as homedir3 } from "node:os"; import { dirname as dirname2, join as join3 } from "node:path"; +var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000; function dshHome() { const fromEnv = process.env.DSH_HOME?.trim(); return fromEnv || join3(homedir3(), ".dsh"); @@ -1410,8 +1411,65 @@ function skillsCacheKey(apiUrl) { const base = normalizeSkillsApiUrl(apiUrl); return createHash2("sha256").update(base).digest("hex").slice(0, 16); } +function cacheParent() { + return join3(dshHome(), "netxops-mcp-skills"); +} function skillsCacheDir(apiUrl) { - return join3(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); + return join3(cacheParent(), skillsCacheKey(apiUrl)); +} +function tokenFingerprint(token) { + const raw = token.trim(); + return raw ? createHash2("sha256").update(raw).digest("hex").slice(0, 16) : ""; +} +function readMeta(root) { + try { + const meta = JSON.parse(readFileSync3(join3(root, ".meta.json"), "utf8")); + return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null; + } catch { + return null; + } +} +function usableSkillsCacheDir(apiUrl, token) { + if (!apiUrl.trim()) + return ""; + const root = skillsCacheDir(apiUrl); + const pulledWith = readMeta(root)?.tokenFp; + if (pulledWith && pulledWith !== tokenFingerprint(token)) + return ""; + return root; +} +function cachedUnavailableTools(root) { + const raw = readMeta(root)?.unavailableTools; + const out = {}; + if (!raw || typeof raw !== "object") + return out; + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group]; + if (Array.isArray(names)) + out[group] = names.filter((n) => typeof n === "string"); + } + return out; +} +function pruneStaleCaches(keep) { + const parent = cacheParent(); + let entries; + try { + entries = readdirSync(parent); + } catch { + return; + } + const now = Date.now(); + for (const name of entries) { + const dir = join3(parent, name); + if (dir === keep) + continue; + try { + const listedAt = Date.parse(readMeta(dir)?.listedAt ?? ""); + const age = now - (Number.isFinite(listedAt) ? listedAt : statSync2(dir).mtimeMs); + if (age > STALE_CACHE_MS) + rmSync(dir, { recursive: true, force: true }); + } catch {} + } } function isSafeSegment(value) { return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00"); @@ -1422,12 +1480,17 @@ function isSafeRelPath(relPath) { const parts = relPath.replace(/\\/g, "/").split("/"); return parts.every((part) => part.length > 0 && part !== "." && part !== ".."); } -function replaceSkillsCache(apiUrl, files) { +function replaceSkillsCache(apiUrl, files, options = {}) { const root = skillsCacheDir(apiUrl); rmSync(root, { recursive: true, force: true }); mkdirSync2(root, { recursive: true }); let written = 0; + const unavailableTools = {}; for (const file of files) { + if (file.unavailableTools?.length && SKILL_DIR_BY_GROUP[file.group]) { + const merged = new Set([...unavailableTools[file.group] ?? [], ...file.unavailableTools]); + unavailableTools[file.group] = [...merged].sort(); + } const groupDir = SKILL_DIR_BY_GROUP[file.group]; if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue; @@ -1445,10 +1508,14 @@ function replaceSkillsCache(apiUrl, files) { apiUrl: normalized, mcpUrl: normalized ? `${normalized}/mcp` : "", listedAt: new Date().toISOString(), - fileCount: written + fileCount: written, + ...options.token !== undefined ? { tokenFp: tokenFingerprint(options.token) } : {}, + unavailableTools }; writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)} `, "utf8"); + summaryMemo.delete(root); + pruneStaleCaches(root); return root; } function countFiles(dir) { @@ -1459,17 +1526,17 @@ function countFiles(dir) { } return n; } -function listCachedSkills(apiUrl) { - const root = skillsCacheDir(apiUrl); +var summaryMemo = new Map; +function listCachedSkills(apiUrl, token = "") { + const root = usableSkillsCacheDir(apiUrl, token); + if (!root || !existsSync3(root)) + return { listedAt: null, skills: [] }; + const metaListedAt = readMeta(root)?.listedAt; + const listedAt = typeof metaListedAt === "string" ? metaListedAt : null; + const memo = summaryMemo.get(root); + if (memo && memo.listedAt === listedAt) + return memo; const skills = []; - let listedAt = null; - if (!apiUrl.trim() || !existsSync3(root)) - return { listedAt, skills }; - try { - const meta = JSON.parse(readFileSync3(join3(root, ".meta.json"), "utf8")); - if (typeof meta.listedAt === "string") - listedAt = meta.listedAt; - } catch {} for (const group of CAPABILITY_GROUP_IDS) { const groupDir = join3(root, SKILL_DIR_BY_GROUP[group]); if (!existsSync3(groupDir)) @@ -1483,7 +1550,15 @@ function listCachedSkills(apiUrl) { } } catch {} } - return { listedAt, skills }; + const listing = { listedAt, skills }; + summaryMemo.set(root, listing); + return listing; +} + +// src/netx/mcp-types.ts +var NETX_TOOL_PREFIX = "netx__"; +function publicToolName(serverName) { + return `${NETX_TOOL_PREFIX}${serverName}`; } // src/netx/group-skills.ts @@ -1614,12 +1689,12 @@ function disabledFootnote(names, lang) { const en = (lang ?? "").trim().toLowerCase().startsWith("en"); return en ? ` -## Disabled tools -The following tools are disabled: ${list}. Do not call them. +## Unavailable tools +The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them. ` : ` -## 已禁用工具 -以下工具已禁用:${list},不要调用。 +## 不可用工具 +以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。 `; } function resolveSkillsRoot(options) { @@ -1629,7 +1704,7 @@ function resolveSkillsRoot(options) { const connection = getNetxConnection(); if (!connection?.apiUrl?.trim()) return ""; - return skillsCacheDir(connection.apiUrl); + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? ""); } async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; @@ -1648,6 +1723,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions } return () => {}; } + const unavailable = cachedUnavailableTools(root); try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) @@ -1661,7 +1737,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions if (skills.length === 0) { 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); + const offNames = new Set([ + ...options.disabledByGroup?.[groupId] ?? [], + ...(unavailable[groupId] ?? []).map(publicToolName) + ]); + const footnote = disabledFootnote([...offNames].sort(), options.lang); for (const skill of skills) { const base = groupId === "topology" ? `${skill.content} @@ -4581,12 +4661,6 @@ 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 -var NETX_TOOL_PREFIX = "netx__"; -function publicToolName(serverName) { - return `${NETX_TOOL_PREFIX}${serverName}`; -} - // src/netx/mcp-catalog.ts function disabledSet(disabledTools) { const out = new Set; @@ -4720,7 +4794,14 @@ function parseSkillResource(resource, text) { } if (!relPath || relPath.includes("..")) return null; - return { group, skill, relPath, text }; + const unavailableTools = Array.isArray(netx.unavailableTools) ? netx.unavailableTools.filter((n) => typeof n === "string" && n.length > 0) : []; + return { + group, + skill, + relPath, + text, + ...netx.entry === true && unavailableTools.length > 0 ? { unavailableTools } : {} + }; } function resourcesCapability(capabilities) { if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities)) @@ -5086,7 +5167,7 @@ async function refreshMcpCatalog(input) { if (gen !== store5().generation) return store5().state; if (fetched.skills !== undefined) { - replaceSkillsCache(input.apiUrl, fetched.skills); + replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token }); } else { console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError); } @@ -5513,7 +5594,8 @@ function installPresetPublication(ctx) { // src/index.ts function mcpCatalogPayload(catalog) { - const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? ""); + const connection = getNetxConnection(); + const cached = listCachedSkills(connection?.apiUrl ?? "", connection?.token ?? ""); return { phase: catalog.phase, mcpUrl: catalog.mcpUrl, diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index 40952b6..5e730c9 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -23,6 +23,7 @@ import { ensureStyles } from './styles.ts' import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts' import { parsePresetIds } from '../netx/capability-groups.ts' import { parseDisabledTools } from '../netx/mcp-catalog.ts' +import { isInsecureRemoteApiUrl } from '../netx/http.ts' import type { McpSkillRow, McpToolRow } from './mcp-catalog-view.ts' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client' @@ -559,6 +560,9 @@ export function NetxopsCard(props: NetxopsCardProps) { onEdit={(text) => { props.edit('apiUrl', text) }} onReset={() => { props.resetField('apiUrl') }} /> + {isInsecureRemoteApiUrl(state.apiUrl.text) + ?

{t('apiUrlInsecure')}

+ : null}
= { sectionExport: 'Session export', apiUrl: 'API URL', apiUrlHint: 'http://127.0.0.1:8890', + apiUrlInsecure: 'Plain HTTP to a remote host: the token and the skills sent to the model are not encrypted. Use https://.', apiUrlInvalid: 'Enter an HTTP or HTTPS URL without credentials.', lang: 'API / alarm language', thinkingLanguage: 'Thinking language', @@ -217,6 +219,7 @@ export const zh: Record = { sectionExport: '会话导出', apiUrl: 'API 地址', apiUrlHint: 'http://127.0.0.1:8890', + apiUrlInsecure: '连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。', apiUrlInvalid: '请输入不含账号密码的 HTTP 或 HTTPS 地址。', lang: 'API / 告警语言', thinkingLanguage: '思考语言', diff --git a/src/index.ts b/src/index.ts index 9e8cc34..6ae36fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,7 +108,8 @@ import { /** Plugins-card catalog payload: tools from the MCP store + skills from the disk cache. */ function mcpCatalogPayload(catalog: ReturnType) { - const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? '') + const connection = getNetxConnection() + const cached = listCachedSkills(connection?.apiUrl ?? '', connection?.token ?? '') return { phase: catalog.phase, mcpUrl: catalog.mcpUrl, diff --git a/src/netx/group-skills.ts b/src/netx/group-skills.ts index 096be35..d3d924f 100644 --- a/src/netx/group-skills.ts +++ b/src/netx/group-skills.ts @@ -12,7 +12,8 @@ import { type NetxCapabilityGroupId, } from './capability-groups.ts' import { getNetxConnection } from './runtime.ts' -import { skillsCacheDir } from './skills-cache.ts' +import { cachedUnavailableTools, usableSkillsCacheDir } from './skills-cache.ts' +import { publicToolName } from './mcp-types.ts' interface ParsedSkill { name: string @@ -181,8 +182,8 @@ function disabledFootnote(names: readonly string[], lang?: string): string { const list = names.join(', ') const en = (lang ?? '').trim().toLowerCase().startsWith('en') return en - ? `\n\n## Disabled tools\nThe following tools are disabled: ${list}. Do not call them.\n` - : `\n\n## 已禁用工具\n以下工具已禁用:${list},不要调用。\n` + ? `\n\n## Unavailable tools\nThe following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.\n` + : `\n\n## 不可用工具\n以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。\n` } function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string { @@ -191,7 +192,7 @@ function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string { } const connection = getNetxConnection() if (!connection?.apiUrl?.trim()) return '' - return skillsCacheDir(connection.apiUrl) + return usableSkillsCacheDir(connection.apiUrl, connection.token ?? '') } /** @@ -236,6 +237,7 @@ export async function registerGroupSkills( } return () => {} } + const unavailable = cachedUnavailableTools(root) try { for (const groupId of CAPABILITY_GROUP_IDS) { if (!enabled.has(groupId)) continue @@ -252,7 +254,11 @@ export async function registerGroupSkills( join(root, SKILL_DIR_BY_GROUP[groupId]), ) } - const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang) + const offNames = new Set([ + ...(options.disabledByGroup?.[groupId] ?? []), + ...(unavailable[groupId] ?? []).map(publicToolName), + ]) + const footnote = disabledFootnote([...offNames].sort(), options.lang) for (const skill of skills) { const base = groupId === 'topology' ? `${skill.content}\n\n## DeepSeek Harness REST capabilities\nUse only tools present in this session. Dual-unit/orbit/polish recipes require server-side topology tools. Use only tools present in this session.\n` diff --git a/src/netx/http.ts b/src/netx/http.ts index c5637f5..b8249fd 100644 --- a/src/netx/http.ts +++ b/src/netx/http.ts @@ -32,3 +32,19 @@ export function normalizeApiUrl(raw: string): NormalizeApiUrlResult { const search = parsed.search && parsed.search !== '?' ? parsed.search : '' return { ok: true, url: `${parsed.origin}${path}${search}` } } + +/** + * True for plain `http://` to a non-loopback host: the token and the skills + * injected into the model's prompt would travel unencrypted. + */ +export function isInsecureRemoteApiUrl(raw: string): boolean { + let url: URL + try { + url = new URL(raw.trim()) + } catch { + return false + } + if (url.protocol !== 'http:') return false + const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase() + return !(host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127\./.test(host)) +} diff --git a/src/netx/mcp-remote.ts b/src/netx/mcp-remote.ts index dd3594c..b5946a1 100644 --- a/src/netx/mcp-remote.ts +++ b/src/netx/mcp-remote.ts @@ -110,7 +110,16 @@ function parseSkillResource( if (parts.length >= 3) relPath = parts.slice(2).join('/') } if (!relPath || relPath.includes('..')) return null - return { group, skill, relPath, text } + const unavailableTools = Array.isArray(netx.unavailableTools) + ? netx.unavailableTools.filter((n): n is string => typeof n === 'string' && n.length > 0) + : [] + return { + group, + skill, + relPath, + text, + ...(netx.entry === true && unavailableTools.length > 0 ? { unavailableTools } : {}), + } } function resourcesCapability(capabilities: unknown): boolean { diff --git a/src/netx/mcp-store.ts b/src/netx/mcp-store.ts index bb4ba34..292227f 100644 --- a/src/netx/mcp-store.ts +++ b/src/netx/mcp-store.ts @@ -125,7 +125,7 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise< if (fetched.skills !== undefined) { // Includes "no resources capability" (empty list) → wipe stale files. - replaceSkillsCache(input.apiUrl, fetched.skills) + replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token }) } else { // Transient resources failure: keep the last good skill cache. console.warn('netxops: mcp skills pull failed, keeping cache — %s', fetched.skillsError) diff --git a/src/netx/skills-cache.ts b/src/netx/skills-cache.ts index 9e606e0..415b9cd 100644 --- a/src/netx/skills-cache.ts +++ b/src/netx/skills-cache.ts @@ -2,6 +2,7 @@ * On-disk cache of netx MCP skill resources, keyed by API URL. * * Layout: `$DSH_HOME/netxops-mcp-skills////` + * plus `.meta.json` (listedAt, token fingerprint, per-group unavailable tools). */ import { createHash } from 'node:crypto' @@ -29,8 +30,23 @@ export interface CachedSkillFile { /** Posix-relative path inside the skill directory (e.g. `SKILL.md`). */ relPath: string text: string + /** SKILL.md only: server tools of this group the token cannot call. */ + unavailableTools?: readonly string[] } +interface SkillsCacheMeta { + apiUrl?: string + mcpUrl?: string + listedAt?: string + fileCount?: number + /** sha256 prefix of the token that pulled this cache. */ + tokenFp?: string + unavailableTools?: Partial> +} + +/** Caches untouched for this long (other netx addresses) are pruned on refresh. */ +const STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000 + function dshHome(): string { const fromEnv = process.env.DSH_HOME?.trim() return fromEnv || join(homedir(), '.dsh') @@ -48,9 +64,75 @@ export function skillsCacheKey(apiUrl: string): string { return createHash('sha256').update(base).digest('hex').slice(0, 16) } +function cacheParent(): string { + return join(dshHome(), 'netxops-mcp-skills') +} + /** 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)) + return join(cacheParent(), skillsCacheKey(apiUrl)) +} + +/** Short fingerprint of a token (never the token itself). */ +export function tokenFingerprint(token: string): string { + const raw = token.trim() + return raw ? createHash('sha256').update(raw).digest('hex').slice(0, 16) : '' +} + +function readMeta(root: string): SkillsCacheMeta | null { + try { + const meta = JSON.parse(readFileSync(join(root, '.meta.json'), 'utf8')) as unknown + return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta as SkillsCacheMeta : null + } catch { + return null + } +} + +/** + * Cache root usable for this connection, or '' when the cache was pulled with + * a different token (switching to a narrower token must not keep mounting the + * broader token's skills while netx is offline). + */ +export function usableSkillsCacheDir(apiUrl: string, token: string): string { + if (!apiUrl.trim()) return '' + const root = skillsCacheDir(apiUrl) + const pulledWith = readMeta(root)?.tokenFp + if (pulledWith && pulledWith !== tokenFingerprint(token)) return '' + return root +} + +/** Per-group tools the pulling token could not call (from `.meta.json`). */ +export function cachedUnavailableTools(root: string): Partial> { + const raw = readMeta(root)?.unavailableTools + const out: Partial> = {} + if (!raw || typeof raw !== 'object') return out + for (const group of CAPABILITY_GROUP_IDS) { + const names = raw[group] + if (Array.isArray(names)) out[group] = names.filter((n): n is string => typeof n === 'string') + } + return out +} + +function pruneStaleCaches(keep: string): void { + const parent = cacheParent() + let entries: string[] + try { + entries = readdirSync(parent) + } catch { + return + } + const now = Date.now() + for (const name of entries) { + const dir = join(parent, name) + if (dir === keep) continue + try { + const listedAt = Date.parse(readMeta(dir)?.listedAt ?? '') + const age = now - (Number.isFinite(listedAt) ? listedAt : statSync(dir).mtimeMs) + if (age > STALE_CACHE_MS) rmSync(dir, { recursive: true, force: true }) + } catch { + // best effort + } + } } function isSafeSegment(value: string): boolean { @@ -72,12 +154,21 @@ function isSafeRelPath(relPath: string): boolean { * 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 { +export function replaceSkillsCache( + apiUrl: string, + files: readonly CachedSkillFile[], + options: { token?: string } = {}, +): string { const root = skillsCacheDir(apiUrl) rmSync(root, { recursive: true, force: true }) mkdirSync(root, { recursive: true }) let written = 0 + const unavailableTools: Partial> = {} for (const file of files) { + if (file.unavailableTools?.length && SKILL_DIR_BY_GROUP[file.group]) { + const merged = new Set([...(unavailableTools[file.group] ?? []), ...file.unavailableTools]) + unavailableTools[file.group] = [...merged].sort() + } const groupDir = SKILL_DIR_BY_GROUP[file.group] if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue const rel = file.relPath.replace(/\\/g, '/') @@ -88,13 +179,17 @@ export function replaceSkillsCache(apiUrl: string, files: readonly CachedSkillFi written += 1 } const normalized = normalizeSkillsApiUrl(apiUrl) - const meta = { + const meta: SkillsCacheMeta = { apiUrl: normalized, mcpUrl: normalized ? `${normalized}/mcp` : '', listedAt: new Date().toISOString(), fileCount: written, + ...(options.token !== undefined ? { tokenFp: tokenFingerprint(options.token) } : {}), + unavailableTools, } writeFileSync(join(root, '.meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8') + summaryMemo.delete(root) + pruneStaleCaches(root) return root } @@ -125,18 +220,20 @@ function countFiles(dir: string): number { return n } -/** Skills currently in the cache for `apiUrl` (what the plugin will mount). */ -export function listCachedSkills(apiUrl: string): { listedAt: string | null; skills: CachedSkillSummary[] } { - const root = skillsCacheDir(apiUrl) +type CachedSkillsListing = { listedAt: string | null; skills: CachedSkillSummary[] } + +/** root → listing; invalidated by replaceSkillsCache, keyed by .meta.json listedAt. */ +const summaryMemo = new Map() + +/** Skills the plugin will mount for this connection (Plugins card polls this). */ +export function listCachedSkills(apiUrl: string, token = ''): CachedSkillsListing { + const root = usableSkillsCacheDir(apiUrl, token) + if (!root || !existsSync(root)) return { listedAt: null, skills: [] } + const metaListedAt = readMeta(root)?.listedAt + const listedAt = typeof metaListedAt === 'string' ? metaListedAt : null + const memo = summaryMemo.get(root) + if (memo && memo.listedAt === listedAt) return memo const skills: CachedSkillSummary[] = [] - let listedAt: string | null = null - if (!apiUrl.trim() || !existsSync(root)) return { listedAt, skills } - try { - const meta = JSON.parse(readFileSync(join(root, '.meta.json'), 'utf8')) as { listedAt?: unknown } - if (typeof meta.listedAt === 'string') listedAt = meta.listedAt - } catch { - // no meta yet - } for (const group of CAPABILITY_GROUP_IDS) { const groupDir = join(root, SKILL_DIR_BY_GROUP[group]) if (!existsSync(groupDir)) continue @@ -150,5 +247,7 @@ export function listCachedSkills(apiUrl: string): { listedAt: string | null; ski // unreadable group dir → skip } } - return { listedAt, skills } + const listing = { listedAt, skills } + summaryMemo.set(root, listing) + return listing } diff --git a/test/group-skills-disabled.test.mjs b/test/group-skills-disabled.test.mjs index 2982f83..13929b6 100644 --- a/test/group-skills-disabled.test.mjs +++ b/test/group-skills-disabled.test.mjs @@ -54,7 +54,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy lang: 'zh', }) assert.ok(registered.length >= 1) - assert.match(registered[0].content, /以下工具已禁用:netx__execManagedNe,不要调用/) + assert.match(registered[0].content, /以下工具已禁用或当前 token 无权使用:netx__execManagedNe,不要调用/) dispose() registered.length = 0 @@ -71,6 +71,6 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy disabledByGroup: { ops: ['netx__execManagedNe'] }, lang: 'en', }) - assert.match(registered[0].content, /The following tools are disabled: netx__execManagedNe/) + assert.match(registered[0].content, /disabled or not granted to this token: netx__execManagedNe/) dispose3() }) diff --git a/test/group-skills.test.mjs b/test/group-skills.test.mjs index 7532b1c..9d77a42 100644 --- a/test/group-skills.test.mjs +++ b/test/group-skills.test.mjs @@ -5,7 +5,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import http from 'node:http' -import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -14,6 +14,7 @@ import { refreshMcpCatalog, resetMcpCatalog } from '../src/netx/mcp-store.ts' import { publishNetxConnection } from '../src/netx/runtime.ts' import { hasSkillsCache, + usableSkillsCacheDir, listCachedSkills, replaceSkillsCache, skillsCacheDir, @@ -48,6 +49,7 @@ function skillResources(files) { group: f.group, skill: f.skill, entry: f.relPath === 'SKILL.md', + ...(f.unavailableTools ? { unavailableTools: f.unavailableTools } : {}), }, }, text: f.text, @@ -365,3 +367,55 @@ test('listCachedSkills summarizes cached skills per group for the Plugins card', ]) assert.deepEqual(listCachedSkills('').skills, []) }) + +test('tools the token cannot call are merged into the skill footnote', async (t) => { + withTempHome(t) + const resources = skillResources([{ + group: 'ops', groupDir: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', + text: OPS_SKILL, unavailableTools: ['execManagedNe'], + }]) + const mock = await startMockMcp({ resources }) + t.after(() => mock.server.close()) + await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' }) + publishNetxConnection({ apiUrl: mock.apiUrl, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000 }) + const { ctx, registered } = fakeSkillsCtx() + await registerGroupSkills(ctx, ['ops'], 'test', { disabledByGroup: { ops: ['netx__queryNmsAlarms'] } }) + assert.match(registered[0].content, /netx__execManagedNe/) + assert.match(registered[0].content, /netx__queryNmsAlarms/) + assert.match(registered[0].content, /当前 token 无权使用/) +}) + +test('cache pulled with another token is not mounted', async (t) => { + withTempHome(t) + replaceSkillsCache(API, [{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL }], { token: 'wide' }) + assert.ok(usableSkillsCacheDir(API, 'wide')) + assert.equal(usableSkillsCacheDir(API, 'narrow'), '') + assert.deepEqual(listCachedSkills(API, 'narrow').skills, []) + publishNetxConnection({ apiUrl: API, token: 'narrow', lang: 'zh', toolCallTimeoutMs: 5_000 }) + const { ctx, registered } = fakeSkillsCtx() + await registerGroupSkills(ctx, ['ops'], 'test') + assert.equal(registered.length, 0) +}) + +test('refresh prunes caches of other netx addresses unused for 30 days', (t) => { + const home = withTempHome(t) + const stale = join(home, 'netxops-mcp-skills', 'deadbeefdeadbeef') + mkdirSync(stale, { recursive: true }) + writeFileSync(join(stale, '.meta.json'), JSON.stringify({ listedAt: '2020-01-01T00:00:00.000Z' })) + const fresh = join(home, 'netxops-mcp-skills', 'feedfacefeedface') + mkdirSync(fresh, { recursive: true }) + writeFileSync(join(fresh, '.meta.json'), JSON.stringify({ listedAt: new Date().toISOString() })) + replaceSkillsCache(API, [], { token: 'secret' }) + assert.equal(existsSync(stale), false) + assert.equal(existsSync(fresh), true) +}) + +test('isInsecureRemoteApiUrl flags plain http only for non-loopback hosts', async () => { + const { isInsecureRemoteApiUrl } = await import('../src/netx/http.ts') + assert.equal(isInsecureRemoteApiUrl('http://127.0.0.1:8890'), false) + assert.equal(isInsecureRemoteApiUrl('http://localhost:8890'), false) + assert.equal(isInsecureRemoteApiUrl('http://[::1]:8890'), false) + assert.equal(isInsecureRemoteApiUrl('https://netx.example.com'), false) + assert.equal(isInsecureRemoteApiUrl('http://192.168.1.10:8890'), true) + assert.equal(isInsecureRemoteApiUrl('not a url'), false) +})