feat(skills): merge unavailable tools into footnote, bind cache to token, prune stale caches, warn on remote plain HTTP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 23:37:16 +08:00
parent 056f2d4ea3
commit 05bfd97c6e
19 changed files with 1100 additions and 496 deletions

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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: "思考语言",

View file

@ -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,