mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 02:43:21 +08:00
feat(skills): pull group skills from netx /mcp resources, drop bundled copy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
277a61894d
commit
fa82f530e9
31 changed files with 2074 additions and 2027 deletions
482
lib/index.js
482
lib/index.js
|
|
@ -3,8 +3,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|||
|
||||
// src/index.ts
|
||||
import { readFileSync as readFileSync9 } from "node:fs";
|
||||
import { dirname as dirname8, join as join13 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
||||
import { dirname as dirname8, join as join14 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
||||
import * as DshSettings from "@deepseek-ai/dsh-settings";
|
||||
|
|
@ -1328,34 +1328,129 @@ function groupsForPreset(connection, presetId, only) {
|
|||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
import { existsSync as existsSync3 } from "node:fs";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { dirname as dirname2, join as join3 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
function opsSkillsRoot() {
|
||||
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
|
||||
if (envRoot && existsSync3(envRoot))
|
||||
return envRoot;
|
||||
const here = dirname2(fileURLToPath2(import.meta.url));
|
||||
const checkout = [join3(here, "..", ".git"), join3(here, "..", "..", ".git")].some(existsSync3);
|
||||
const siblingCandidates = [
|
||||
join3(here, "..", "..", "..", "netx", "skills"),
|
||||
join3(here, "..", "..", "netx", "skills")
|
||||
];
|
||||
for (const candidate of checkout ? siblingCandidates : []) {
|
||||
if (existsSync3(candidate))
|
||||
return candidate;
|
||||
import { join as join4 } from "node:path";
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
const packaged = [
|
||||
join3(here, "..", "presets", "netxops", "skills"),
|
||||
join3(here, "..", "..", "presets", "netxops", "skills")
|
||||
];
|
||||
for (const candidate of packaged) {
|
||||
if (existsSync3(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return packaged[0];
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store2();
|
||||
if (JSON.stringify(state.connection) === JSON.stringify(next))
|
||||
return;
|
||||
state.connection = next;
|
||||
for (const listener of [...state.listeners]) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error("netxops: connection subscriber failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/skills-cache.ts
|
||||
import { createHash as createHash2 } from "node:crypto";
|
||||
import {
|
||||
existsSync as existsSync3,
|
||||
mkdirSync as mkdirSync2,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync as writeFileSync3
|
||||
} from "node:fs";
|
||||
import { homedir as homedir3 } from "node:os";
|
||||
import { dirname as dirname2, join as join3 } from "node:path";
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
return fromEnv || join3(homedir3(), ".dsh");
|
||||
}
|
||||
function normalizeSkillsApiUrl(apiUrl) {
|
||||
const normalized = normalizeApiUrl(apiUrl);
|
||||
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
|
||||
}
|
||||
function skillsCacheKey(apiUrl) {
|
||||
const base = normalizeSkillsApiUrl(apiUrl);
|
||||
return createHash2("sha256").update(base).digest("hex").slice(0, 16);
|
||||
}
|
||||
function skillsCacheDir(apiUrl) {
|
||||
return join3(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
|
||||
}
|
||||
function isSafeSegment(value) {
|
||||
return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00");
|
||||
}
|
||||
function isSafeRelPath(relPath) {
|
||||
if (!relPath || relPath.startsWith("/") || relPath.includes("\x00"))
|
||||
return false;
|
||||
const parts = relPath.replace(/\\/g, "/").split("/");
|
||||
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
||||
}
|
||||
function replaceSkillsCache(apiUrl, files) {
|
||||
const root = skillsCacheDir(apiUrl);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
mkdirSync2(root, { recursive: true });
|
||||
let written = 0;
|
||||
for (const file of files) {
|
||||
const groupDir = SKILL_DIR_BY_GROUP[file.group];
|
||||
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath))
|
||||
continue;
|
||||
const rel = file.relPath.replace(/\\/g, "/");
|
||||
const dest = join3(root, groupDir, file.skill, ...rel.split("/"));
|
||||
mkdirSync2(dirname2(dest), { recursive: true });
|
||||
const text = file.text.replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
writeFileSync3(dest, text, "utf8");
|
||||
written += 1;
|
||||
}
|
||||
const normalized = normalizeSkillsApiUrl(apiUrl);
|
||||
const meta = {
|
||||
apiUrl: normalized,
|
||||
mcpUrl: normalized ? `${normalized}/mcp` : "",
|
||||
listedAt: new Date().toISOString(),
|
||||
fileCount: written
|
||||
};
|
||||
writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)}
|
||||
`, "utf8");
|
||||
return root;
|
||||
}
|
||||
|
||||
// src/netx/group-skills.ts
|
||||
function parseFrontmatter(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
|
|
@ -1402,7 +1497,7 @@ function stripQuotes(value) {
|
|||
return value;
|
||||
}
|
||||
async function loadSkillBundle(dir) {
|
||||
const skillPath = join3(dir, "SKILL.md");
|
||||
const skillPath = join4(dir, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -1425,7 +1520,7 @@ async function loadSkillBundle(dir) {
|
|||
};
|
||||
}
|
||||
async function loadGroupSkills(skillsRoot, groupId) {
|
||||
const groupDir = join3(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
const groupDir = join4(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(groupDir);
|
||||
|
|
@ -1435,7 +1530,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join3(groupDir, entry);
|
||||
const full = join4(groupDir, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat(full)).isDirectory();
|
||||
|
|
@ -1444,7 +1539,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skillPath = join3(full, "SKILL.md");
|
||||
const skillPath = join4(full, "SKILL.md");
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(skillPath, "utf8");
|
||||
|
|
@ -1491,6 +1586,15 @@ The following tools are disabled: ${list}. Do not call them.
|
|||
以下工具已禁用:${list},不要调用。
|
||||
`;
|
||||
}
|
||||
function resolveSkillsRoot(options) {
|
||||
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
|
||||
return options.skillsRoot.trim();
|
||||
}
|
||||
const connection = getNetxConnection();
|
||||
if (!connection?.apiUrl?.trim())
|
||||
return "";
|
||||
return skillsCacheDir(connection.apiUrl);
|
||||
}
|
||||
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
|
||||
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
|
||||
const signal = options.signal;
|
||||
|
|
@ -1498,10 +1602,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const root = opsSkillsRoot();
|
||||
const root = resolveSkillsRoot(options);
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
if (!root) {
|
||||
if (enabled.size > 0) {
|
||||
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
try {
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
if (!enabled.has(groupId))
|
||||
|
|
@ -1513,7 +1623,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
|
|||
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join3(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join4(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
|
||||
for (const skill of skills) {
|
||||
|
|
@ -1549,12 +1659,12 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
|
|||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
|
||||
import { join as join6 } from "node:path";
|
||||
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
|
||||
import { join as join7 } from "node:path";
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
||||
import { basename, dirname as dirname3, join as join4, resolve } from "node:path";
|
||||
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
||||
import { basename, dirname as dirname3, join as join5, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
|
|
@ -1607,7 +1717,7 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
if (!rootStat.isDirectory()) {
|
||||
return { paths: [], error: `kbRoot is not a directory: ${root}` };
|
||||
}
|
||||
const direct = join4(root, "MANIFEST.json");
|
||||
const direct = join5(root, "MANIFEST.json");
|
||||
if (existsSync4(direct)) {
|
||||
try {
|
||||
if (statSync2(direct).isFile())
|
||||
|
|
@ -1618,7 +1728,7 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
const walk = (dir, depth) => {
|
||||
if (depth > maxDepth)
|
||||
return;
|
||||
const candidate = join4(dir, "MANIFEST.json");
|
||||
const candidate = join5(dir, "MANIFEST.json");
|
||||
if (existsSync4(candidate)) {
|
||||
try {
|
||||
if (statSync2(candidate).isFile())
|
||||
|
|
@ -1629,14 +1739,14 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
return;
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
entries = readdirSync2(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join4(dir, name);
|
||||
const full = join5(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync2(full);
|
||||
|
|
@ -1649,14 +1759,14 @@ function findManifest(kbRoot, maxDepth = 3) {
|
|||
};
|
||||
let topEntries;
|
||||
try {
|
||||
topEntries = readdirSync(root);
|
||||
topEntries = readdirSync2(root);
|
||||
} catch {
|
||||
return { paths: [], error: `cannot read kbRoot: ${root}` };
|
||||
}
|
||||
for (const name of topEntries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join4(root, name);
|
||||
const full = join5(root, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync2(full);
|
||||
|
|
@ -1817,7 +1927,7 @@ function renderKbPathTable(snapshot) {
|
|||
|
||||
// src/netx/kb-local-path.ts
|
||||
import { existsSync as existsSync5, realpathSync, statSync as statSync3 } from "node:fs";
|
||||
import { join as join5, relative, resolve as resolve2, sep } from "node:path";
|
||||
import { join as join6, relative, resolve as resolve2, sep } from "node:path";
|
||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||
"memories",
|
||||
"drafts",
|
||||
|
|
@ -1953,16 +2063,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
|||
return checked;
|
||||
}
|
||||
function memoryDir(bucket) {
|
||||
return join5("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
return join6("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||
}
|
||||
function suggestionDir(kind) {
|
||||
return join5("suggestions", kind);
|
||||
return join6("suggestions", kind);
|
||||
}
|
||||
function draftDir(domain) {
|
||||
const d = domain?.trim();
|
||||
if (!d)
|
||||
return "drafts";
|
||||
return join5("drafts", sanitizeSlug(d, "misc"));
|
||||
return join6("drafts", sanitizeSlug(d, "misc"));
|
||||
}
|
||||
function refDir(area, device) {
|
||||
if (!REF_AREAS.includes(area)) {
|
||||
|
|
@ -1973,9 +2083,9 @@ function refDir(area, device) {
|
|||
if (!host) {
|
||||
throw new Error("refs/devices requires device (host_name)");
|
||||
}
|
||||
return join5("refs", "devices", host);
|
||||
return join6("refs", "devices", host);
|
||||
}
|
||||
return join5("refs", area);
|
||||
return join6("refs", area);
|
||||
}
|
||||
|
||||
// src/netx/kb-context-skill.ts
|
||||
|
|
@ -1986,10 +2096,10 @@ function readKbIdentity(snapshot) {
|
|||
const localRoot = resolveKbLocalRoot(snapshot);
|
||||
if (!localRoot)
|
||||
return [];
|
||||
const dir = join6(localRoot, "identity");
|
||||
const dir = join7(localRoot, "identity");
|
||||
let names;
|
||||
try {
|
||||
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -1998,7 +2108,7 @@ function readKbIdentity(snapshot) {
|
|||
for (const name of names) {
|
||||
if (budget <= 0)
|
||||
break;
|
||||
const full = join6(dir, name);
|
||||
const full = join7(dir, name);
|
||||
let text;
|
||||
try {
|
||||
if (!statSync4(full).isFile())
|
||||
|
|
@ -2127,14 +2237,14 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|||
// src/netx/kb-local-ops.ts
|
||||
import {
|
||||
existsSync as existsSync6,
|
||||
mkdirSync as mkdirSync2,
|
||||
readdirSync as readdirSync3,
|
||||
mkdirSync as mkdirSync3,
|
||||
readdirSync as readdirSync4,
|
||||
readFileSync as readFileSync5,
|
||||
rmSync,
|
||||
rmSync as rmSync2,
|
||||
statSync as statSync5,
|
||||
writeFileSync as writeFileSync3
|
||||
writeFileSync as writeFileSync4
|
||||
} from "node:fs";
|
||||
import { dirname as dirname4, join as join7 } from "node:path";
|
||||
import { dirname as dirname4, join as join8 } from "node:path";
|
||||
function requireLocalRoot(snapshot) {
|
||||
const root = resolveKbLocalRoot(snapshot);
|
||||
if (!root) {
|
||||
|
|
@ -2183,9 +2293,9 @@ function createMemory(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2202,9 +2312,9 @@ function createDraft(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
writeFileSync4(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
action: existed ? "updated" : "created",
|
||||
|
|
@ -2219,9 +2329,9 @@ function createSuggestion(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2238,9 +2348,9 @@ function createRef(snapshot, args) {
|
|||
if (existsSync6(target.absolutePath) && !args.overwrite) {
|
||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||
}
|
||||
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
|
||||
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
|
||||
const existed = existsSync6(target.absolutePath);
|
||||
writeFileSync3(target.absolutePath, args.body.endsWith(`
|
||||
writeFileSync4(target.absolutePath, args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`, "utf8");
|
||||
return {
|
||||
|
|
@ -2259,7 +2369,7 @@ function updateLocalFile(snapshot, args) {
|
|||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||
`) ? args.body : `${args.body}
|
||||
`;
|
||||
writeFileSync3(target.absolutePath, body, "utf8");
|
||||
writeFileSync4(target.absolutePath, body, "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
action: "updated",
|
||||
|
|
@ -2273,7 +2383,7 @@ function deleteLocalFile(snapshot, args) {
|
|||
if (!existsSync6(target.absolutePath)) {
|
||||
throw new Error(`file not found: ${target.relativePath}`);
|
||||
}
|
||||
rmSync(target.absolutePath, { force: false });
|
||||
rmSync2(target.absolutePath, { force: false });
|
||||
return {
|
||||
ok: true,
|
||||
action: "deleted",
|
||||
|
|
@ -2284,14 +2394,14 @@ function deleteLocalFile(snapshot, args) {
|
|||
function walkFiles(dir, baseLocal, out) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync3(dir);
|
||||
entries = readdirSync4(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === "." || name === "..")
|
||||
continue;
|
||||
const full = join7(dir, name);
|
||||
const full = join8(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync5(full);
|
||||
|
|
@ -2316,7 +2426,7 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||
const entries = [];
|
||||
for (const r of roots) {
|
||||
walkFiles(join7(localRoot, r), localRoot, entries);
|
||||
walkFiles(join8(localRoot, r), localRoot, entries);
|
||||
}
|
||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||
|
|
@ -2328,8 +2438,8 @@ function listLocalFiles(snapshot, args = {}) {
|
|||
}
|
||||
|
||||
// src/netx/kb-read-ops.ts
|
||||
import { readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
||||
import { extname, join as join8, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
import { readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
||||
import { extname, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||
var READ_DEFAULT_LINES = 400;
|
||||
|
|
@ -2375,12 +2485,12 @@ function readKbFile(snapshot, args) {
|
|||
const target = resolveKbReadPath(snapshot, args.path);
|
||||
const st = statSync6(target.absolutePath);
|
||||
if (st.isDirectory()) {
|
||||
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
|
||||
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
|
||||
if (entry.isDirectory())
|
||||
return { name: entry.name, type: "dir" };
|
||||
let size;
|
||||
try {
|
||||
size = statSync6(join8(target.absolutePath, entry.name)).size;
|
||||
size = statSync6(join9(target.absolutePath, entry.name)).size;
|
||||
} catch {}
|
||||
return { name: entry.name, type: "file", size };
|
||||
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||
|
|
@ -2449,14 +2559,14 @@ function cachedText(path, mtimeMs, size) {
|
|||
function walkTextFiles(dir, out) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync4(dir, { withFileTypes: true });
|
||||
entries = readdirSync5(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name))
|
||||
continue;
|
||||
const full = join8(dir, entry.name);
|
||||
const full = join9(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTextFiles(full, out);
|
||||
continue;
|
||||
|
|
@ -2563,7 +2673,7 @@ function searchKb(snapshot, args) {
|
|||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
|
||||
var ENV_KEYS = [
|
||||
"KB_ROOT",
|
||||
"KB_LOCAL",
|
||||
|
|
@ -2574,17 +2684,17 @@ var ENV_KEYS = [
|
|||
"KB_PATHS",
|
||||
"KB_STATUS"
|
||||
];
|
||||
function store2() {
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
const snap = store2().snapshot;
|
||||
const snap = store3().snapshot;
|
||||
return {
|
||||
...snap,
|
||||
content: { ...snap.content },
|
||||
|
|
@ -2592,7 +2702,7 @@ function getKbContext() {
|
|||
};
|
||||
}
|
||||
function publishKbContext(next, options = {}) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
if (!options.force && JSON.stringify(state.snapshot) === JSON.stringify(next))
|
||||
return;
|
||||
state.snapshot = {
|
||||
|
|
@ -2609,7 +2719,7 @@ function publishKbContext(next, options = {}) {
|
|||
}
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -2656,16 +2766,16 @@ function toLosslessJson(value) {
|
|||
}
|
||||
|
||||
// src/netx/tool-mount-status.ts
|
||||
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
||||
import { homedir as homedir3 } from "node:os";
|
||||
import { dirname as dirname5, join as join9 } from "node:path";
|
||||
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
||||
import { homedir as homedir4 } from "node:os";
|
||||
import { dirname as dirname5, join as join10 } from "node:path";
|
||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||
function store3() {
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
|
||||
}
|
||||
function getToolMountId(ctx, source) {
|
||||
const state = store3();
|
||||
const state = store4();
|
||||
let id = state.contexts.get(ctx);
|
||||
if (id === undefined) {
|
||||
id = ++state.sequence;
|
||||
|
|
@ -2674,7 +2784,7 @@ function getToolMountId(ctx, source) {
|
|||
return `${source}:${id}`;
|
||||
}
|
||||
function getToolMountStatuses() {
|
||||
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
|
||||
}
|
||||
function getToolMountStatus() {
|
||||
const rows = getToolMountStatuses();
|
||||
|
|
@ -2682,22 +2792,22 @@ function getToolMountStatus() {
|
|||
}
|
||||
function persist() {
|
||||
try {
|
||||
const path = join9(process.env.DSH_HOME?.trim() || join9(homedir3(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync3(dirname5(path), { recursive: true });
|
||||
writeFileSync4(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
const path = join10(process.env.DSH_HOME?.trim() || join10(homedir4(), ".dsh"), "netxops-mount-status.json");
|
||||
mkdirSync4(dirname5(path), { recursive: true });
|
||||
writeFileSync5(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||
`, "utf8");
|
||||
} catch {}
|
||||
}
|
||||
function publishToolMountStatus(next) {
|
||||
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
|
||||
persist();
|
||||
}
|
||||
function removeToolMountStatus(id) {
|
||||
store3().mounts.delete(id);
|
||||
store4().mounts.delete(id);
|
||||
persist();
|
||||
}
|
||||
function resetToolMountStatus() {
|
||||
store3().mounts.clear();
|
||||
store4().mounts.clear();
|
||||
persist();
|
||||
}
|
||||
|
||||
|
|
@ -2894,13 +3004,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
|||
|
||||
// src/netx/kb-pack-skills.ts
|
||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||
import { join as join10 } from "node:path";
|
||||
import { join as join11 } from "node:path";
|
||||
function kbPackSkillsEnabled(snapshot) {
|
||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||
}
|
||||
function resolveKbSkillsRoot(snapshot) {
|
||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||
return join10(snapshot.realRoot, rel);
|
||||
return join11(snapshot.realRoot, rel);
|
||||
}
|
||||
function kbPackSkillFooter(snapshot) {
|
||||
return [
|
||||
|
|
@ -2922,7 +3032,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
|
|||
const rel = snapshot.paths.localSkills?.trim();
|
||||
if (!rel)
|
||||
return null;
|
||||
return join10(snapshot.realRoot, rel);
|
||||
return join11(snapshot.realRoot, rel);
|
||||
}
|
||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||
let entries;
|
||||
|
|
@ -2941,7 +3051,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
|||
for (const entry of entries) {
|
||||
if (options.signal?.aborted)
|
||||
break;
|
||||
const full = join10(root, entry);
|
||||
const full = join11(root, entry);
|
||||
let isDir = false;
|
||||
try {
|
||||
isDir = (await stat2(full)).isDirectory();
|
||||
|
|
@ -3160,64 +3270,10 @@ function replyReminder(setting) {
|
|||
].join(" ");
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store4() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store4();
|
||||
if (JSON.stringify(state.connection) === JSON.stringify(next))
|
||||
return;
|
||||
state.connection = next;
|
||||
for (const listener of [...state.listeners]) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error("netxops: connection subscriber failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store4().connection;
|
||||
}
|
||||
function provisionalNetxConnection() {
|
||||
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
|
||||
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
|
||||
return {
|
||||
toolsEnabled: false,
|
||||
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
|
||||
token: process.env.NETX_API_TOKEN?.trim() ?? "",
|
||||
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
|
||||
toolCallTimeoutMs: 120000,
|
||||
groups: DEFAULT_CAPABILITY_GROUPS,
|
||||
disabledTools: [],
|
||||
readOnlyOnly: false,
|
||||
groupKbInPreset: true,
|
||||
groupKbPublic: false
|
||||
};
|
||||
}
|
||||
function requireNetxConnection() {
|
||||
return getNetxConnection() ?? provisionalNetxConnection();
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store4();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/session-export.ts
|
||||
import { homedir as homedir4, hostname as osHostname } from "node:os";
|
||||
import { homedir as homedir5, hostname as osHostname } from "node:os";
|
||||
|
||||
// node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
||||
// ../../netxops/node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
||||
import { createRequire as createRequire3 } from "module";
|
||||
var require2 = createRequire3("/");
|
||||
var _a;
|
||||
|
|
@ -4485,8 +4541,8 @@ function mountNetxopsWebRoute(ctx, channel, handler) {
|
|||
|
||||
// src/netx/tools.ts
|
||||
import { readFileSync as readFileSync7 } from "node:fs";
|
||||
import { dirname as dirname6, join as join11 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
import { dirname as dirname6, join as join12 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||
|
||||
// src/netx/mcp-types.ts
|
||||
|
|
@ -4563,6 +4619,7 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
|
|||
// src/netx/mcp-remote.ts
|
||||
var PROTOCOL_VERSION = "2025-03-26";
|
||||
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
|
||||
var SKILLS_URI_PREFIX = "netx://skills/";
|
||||
|
||||
class McpRemoteError extends Error {
|
||||
status;
|
||||
|
|
@ -4594,9 +4651,46 @@ function parseTool(raw) {
|
|||
const readOnlyHint = annotations.readOnlyHint === true;
|
||||
const meta = row._meta && typeof row._meta === "object" && !Array.isArray(row._meta) ? row._meta : {};
|
||||
const netx = meta.netx && typeof meta.netx === "object" && !Array.isArray(meta.netx) ? meta.netx : {};
|
||||
if (netx.group === "meta")
|
||||
return null;
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined;
|
||||
return { name, description, inputSchema, readOnlyHint, group };
|
||||
}
|
||||
function readMetaNetx(row) {
|
||||
const meta = row._meta ?? row.meta;
|
||||
if (!meta || typeof meta !== "object" || Array.isArray(meta))
|
||||
return {};
|
||||
const netx = meta.netx;
|
||||
if (!netx || typeof netx !== "object" || Array.isArray(netx))
|
||||
return {};
|
||||
return netx;
|
||||
}
|
||||
function parseSkillResource(resource, text) {
|
||||
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
||||
return null;
|
||||
const row = resource;
|
||||
const uri = typeof row.uri === "string" ? row.uri.trim() : "";
|
||||
const netx = readMetaNetx(row);
|
||||
const group = isGroupId(netx.group) ? netx.group : undefined;
|
||||
const skill = typeof netx.skill === "string" ? netx.skill.trim() : "";
|
||||
if (!group || !skill)
|
||||
return null;
|
||||
let relPath = typeof row.name === "string" ? row.name.trim().replace(/\\/g, "/") : "";
|
||||
if (!relPath && uri.startsWith(SKILLS_URI_PREFIX)) {
|
||||
const rest = uri.slice(SKILLS_URI_PREFIX.length);
|
||||
const parts = rest.split("/");
|
||||
if (parts.length >= 3)
|
||||
relPath = parts.slice(2).join("/");
|
||||
}
|
||||
if (!relPath || relPath.includes(".."))
|
||||
return null;
|
||||
return { group, skill, relPath, text };
|
||||
}
|
||||
function resourcesCapability(capabilities) {
|
||||
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities))
|
||||
return false;
|
||||
return "resources" in capabilities;
|
||||
}
|
||||
function parseMcpResponseBody(raw) {
|
||||
const text = raw.replace(/^\uFEFF/, "").trim();
|
||||
if (!text)
|
||||
|
|
@ -4692,24 +4786,57 @@ async function postRpc(options, method, params, notification = false) {
|
|||
}
|
||||
return asRpcResult(parseMcpResponseBody(responseText));
|
||||
}
|
||||
async function listMcpTools(options) {
|
||||
async function fetchMcpCatalog(options) {
|
||||
if (!options.mcpUrl)
|
||||
throw new McpRemoteError("mcp url is empty");
|
||||
await postRpc(options, "initialize", {
|
||||
const init = await postRpc(options, "initialize", {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: CLIENT_INFO
|
||||
});
|
||||
await postRpc(options, "notifications/initialized", {}, true);
|
||||
const result = await postRpc(options, "tools/list", {});
|
||||
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
||||
const out = [];
|
||||
for (const item of tools) {
|
||||
const toolsResult = await postRpc(options, "tools/list", {});
|
||||
const toolsRaw = Array.isArray(toolsResult?.tools) ? toolsResult.tools : [];
|
||||
const tools = [];
|
||||
for (const item of toolsRaw) {
|
||||
const parsed = parseTool(item);
|
||||
if (parsed)
|
||||
out.push(parsed);
|
||||
tools.push(parsed);
|
||||
}
|
||||
if (!resourcesCapability(init?.capabilities)) {
|
||||
return { tools, skills: [] };
|
||||
}
|
||||
try {
|
||||
const listed = await postRpc(options, "resources/list", {});
|
||||
const resources = Array.isArray(listed?.resources) ? listed.resources : [];
|
||||
const skills = [];
|
||||
for (const resource of resources) {
|
||||
if (!resource || typeof resource !== "object" || Array.isArray(resource))
|
||||
continue;
|
||||
const uri = typeof resource.uri === "string" ? resource.uri.trim() : "";
|
||||
if (!uri.startsWith(SKILLS_URI_PREFIX))
|
||||
continue;
|
||||
const read = await postRpc(options, "resources/read", { uri });
|
||||
const contents = Array.isArray(read?.contents) ? read.contents : [];
|
||||
let text = "";
|
||||
for (const block of contents) {
|
||||
if (!block || typeof block !== "object" || Array.isArray(block))
|
||||
continue;
|
||||
const row = block;
|
||||
if (typeof row.text === "string") {
|
||||
text = row.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const parsed = parseSkillResource(resource, text);
|
||||
if (parsed)
|
||||
skills.push(parsed);
|
||||
}
|
||||
return { tools, skills };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { tools, skillsError: message };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
async function callMcpTool(options, name, args) {
|
||||
if (!options.mcpUrl)
|
||||
|
|
@ -4912,7 +5039,7 @@ async function refreshMcpCatalog(input) {
|
|||
fromCache: previous.tools.length > 0
|
||||
});
|
||||
try {
|
||||
const tools = await listMcpTools({
|
||||
const fetched = await fetchMcpCatalog({
|
||||
mcpUrl,
|
||||
token: input.token,
|
||||
lang: input.lang,
|
||||
|
|
@ -4922,10 +5049,15 @@ async function refreshMcpCatalog(input) {
|
|||
});
|
||||
if (gen !== store5().generation)
|
||||
return store5().state;
|
||||
if (fetched.skills !== undefined) {
|
||||
replaceSkillsCache(input.apiUrl, fetched.skills);
|
||||
} else {
|
||||
console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError);
|
||||
}
|
||||
const next = {
|
||||
phase: "connected",
|
||||
mcpUrl,
|
||||
tools: Object.freeze([...tools]),
|
||||
tools: Object.freeze([...fetched.tools]),
|
||||
error: "",
|
||||
listedAt: new Date().toISOString(),
|
||||
fromCache: false
|
||||
|
|
@ -4952,8 +5084,8 @@ async function refreshMcpCatalog(input) {
|
|||
// src/netx/tools.ts
|
||||
function packageVersion() {
|
||||
try {
|
||||
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname6(fileURLToPath2(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync7(join12(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -5086,12 +5218,12 @@ import { createScope, scopeOf } from "@deepseek-ai/dsh-scope";
|
|||
|
||||
// src/netx/group-tools-plugin.ts
|
||||
import { readFileSync as readFileSync8 } from "node:fs";
|
||||
import { dirname as dirname7, join as join12 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
||||
import { dirname as dirname7, join as join13 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||
function packageVersion2() {
|
||||
try {
|
||||
const here = dirname7(fileURLToPath4(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync8(join12(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname7(fileURLToPath3(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync8(join13(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
@ -5417,8 +5549,8 @@ function resolveApiUrlForConnection(value) {
|
|||
}
|
||||
function packageVersion3() {
|
||||
try {
|
||||
const here = dirname8(fileURLToPath5(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync9(join13(here, "..", "package.json"), "utf8"));
|
||||
const here = dirname8(fileURLToPath4(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync9(join14(here, "..", "package.json"), "utf8"));
|
||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||
} catch {
|
||||
return "0";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue