mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-15 02:40:46 +08:00
Fix Netx Ops tools missing on DSH 0.2 and public-preset publish.
Stop waiting on removed settingsScope so the host can publish a connection, harden skill frontmatter for CRLF, and auto-save capability public toggles so other presets actually get netx__* tools (0.1.50). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a313dd0eae
commit
d4f2459eb2
18 changed files with 567 additions and 178 deletions
|
|
@ -37,6 +37,8 @@
|
||||||
groupOpsPublic: false
|
groupOpsPublic: false
|
||||||
groupTopologyInPreset: false
|
groupTopologyInPreset: false
|
||||||
groupTopologyPublic: false
|
groupTopologyPublic: false
|
||||||
|
groupBizMonitorInPreset: true
|
||||||
|
groupBizMonitorPublic: false
|
||||||
kbRoot: ''
|
kbRoot: ''
|
||||||
groupKbInPreset: true
|
groupKbInPreset: true
|
||||||
groupKbPublic: false
|
groupKbPublic: false
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
groupOpsPublic: false
|
groupOpsPublic: false
|
||||||
groupTopologyInPreset: false
|
groupTopologyInPreset: false
|
||||||
groupTopologyPublic: false
|
groupTopologyPublic: false
|
||||||
|
groupBizMonitorInPreset: true
|
||||||
|
groupBizMonitorPublic: false
|
||||||
kbRoot: ''
|
kbRoot: ''
|
||||||
groupKbInPreset: true
|
groupKbInPreset: true
|
||||||
groupKbPublic: false
|
groupKbPublic: false
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ bun run link:peers
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Cannot find package '@deepseek-ai/schemastery'` / **netxops 未运行** | 未跑 `link:peers` |
|
| `Cannot find package '@deepseek-ai/schemastery'` / **netxops 未运行** | 未跑 `link:peers` |
|
||||||
| **web boot: dsh-netxops failed** | peers 版本错乱(例如链了 Desktop 的 schemastery 3.18.4)→ 重跑 `bun run link:peers` 再重启 |
|
| **web boot: dsh-netxops failed** | peers 版本错乱(例如链了 Desktop 的 schemastery 3.18.4)→ 重跑 `bun run link:peers` 再重启 |
|
||||||
|
| `pending (waiting for service: settingsScope)` → 会话无 `netx__*` / 无 `netx-ops` | 旧客户端仍硬/软等待已删除的 `settingsScope`(DSH 0.2);用 **≥0.1.50** 的 tgz 重装并完全重启 Desktop,勿用脏的 `link:` / 混锁 `.modules.yaml` |
|
||||||
|
|
||||||
改 Host/Client 后照常 `bun run bundle`,一般不用重装;换机器或清过本仓库 `node_modules` 才需再 `link:peers`。
|
改 Host/Client 后照常 `bun run bundle`,一般不用重装;换机器或清过本仓库 `node_modules` 才需再 `link:peers`。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry);
|
const full = join(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
||||||
ctx.logger.info("%s: no groups enabled", options.name);
|
ctx.logger.info("%s: no groups enabled", options.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: "preset",
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === "forced" ? enabled : options.only,
|
plane: "preset",
|
||||||
forceGroups: options.mode === "forced" ? enabled : undefined
|
only: options.mode === "forced" ? enabled : options.only,
|
||||||
});
|
forceGroups: options.mode === "forced" ? enabled : undefined
|
||||||
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
});
|
||||||
|
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -2397,6 +2397,16 @@ class NetxopsCardController {
|
||||||
}
|
}
|
||||||
inject() {
|
inject() {
|
||||||
const actions = this.form.actions();
|
const actions = this.form.actions();
|
||||||
|
const capabilityFields = new Set([
|
||||||
|
"groupOpsInPreset",
|
||||||
|
"groupOpsPublic",
|
||||||
|
"groupTopologyInPreset",
|
||||||
|
"groupTopologyPublic",
|
||||||
|
"groupBizMonitorInPreset",
|
||||||
|
"groupBizMonitorPublic",
|
||||||
|
"groupKbInPreset",
|
||||||
|
"groupKbPublic"
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
hooks: { netxopsCard: this.store },
|
hooks: { netxopsCard: this.store },
|
||||||
...actions,
|
...actions,
|
||||||
|
|
@ -2404,6 +2414,8 @@ class NetxopsCardController {
|
||||||
actions.edit(field, text);
|
actions.edit(field, text);
|
||||||
if (field === "kbRoot")
|
if (field === "kbRoot")
|
||||||
this.refreshKbPreview();
|
this.refreshKbPreview();
|
||||||
|
if (capabilityFields.has(field))
|
||||||
|
this.form.save();
|
||||||
},
|
},
|
||||||
discard: () => {
|
discard: () => {
|
||||||
actions.discard();
|
actions.discard();
|
||||||
|
|
@ -2413,6 +2425,8 @@ class NetxopsCardController {
|
||||||
actions.resetField(field);
|
actions.resetField(field);
|
||||||
if (field === "kbRoot")
|
if (field === "kbRoot")
|
||||||
this.refreshKbPreview();
|
this.refreshKbPreview();
|
||||||
|
if (capabilityFields.has(field))
|
||||||
|
this.form.save();
|
||||||
},
|
},
|
||||||
exportAllSessions: () => {
|
exportAllSessions: () => {
|
||||||
this.exportAllSessions();
|
this.exportAllSessions();
|
||||||
|
|
@ -2492,7 +2506,7 @@ var en = {
|
||||||
groupBizMonitor: "bizMonitor",
|
groupBizMonitor: "bizMonitor",
|
||||||
groupKb: "Knowledge skills",
|
groupKb: "Knowledge skills",
|
||||||
groupInPreset: "In Netx Ops preset",
|
groupInPreset: "In Netx Ops preset",
|
||||||
groupPublic: "Publish to other presets",
|
groupPublic: "Publish to other presets (auto-saves; new sessions)",
|
||||||
kbRoot: "Package root",
|
kbRoot: "Package root",
|
||||||
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.",
|
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.",
|
||||||
kbBrowse: "Browse…",
|
kbBrowse: "Browse…",
|
||||||
|
|
@ -2570,7 +2584,7 @@ var zh = {
|
||||||
groupBizMonitor: "bizMonitor",
|
groupBizMonitor: "bizMonitor",
|
||||||
groupKb: "知识库技能",
|
groupKb: "知识库技能",
|
||||||
groupInPreset: "在 Netx Ops 预设中启用",
|
groupInPreset: "在 Netx Ops 预设中启用",
|
||||||
groupPublic: "对其他预设公开",
|
groupPublic: "对其他预设公开(勾选后自动保存;新会话生效)",
|
||||||
kbRoot: "知识包根目录",
|
kbRoot: "知识包根目录",
|
||||||
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。",
|
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。",
|
||||||
kbBrowse: "浏览…",
|
kbBrowse: "浏览…",
|
||||||
|
|
@ -2865,16 +2879,19 @@ function apply(ctx) {
|
||||||
formsCtx.logger?.warn?.("netxops: configForms present but no get/whileServed match for %s", candidates.join(","));
|
formsCtx.logger?.warn?.("netxops: configForms present but no get/whileServed match for %s", candidates.join(","));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ctx.inject(["settingsScope"], (scopeCtx) => {
|
try {
|
||||||
const binder = scopeCtx.settingsScope;
|
const getter = ctx.get;
|
||||||
if (!binder || typeof binder.bind !== "function") {
|
if (typeof getter === "function" && getter.call(ctx, "configForms") === undefined) {
|
||||||
scopeCtx.logger?.warn?.("netxops: settingsScope present but .bind missing");
|
const binder = getter.call(ctx, "settingsScope");
|
||||||
return;
|
if (binder && typeof binder.bind === "function") {
|
||||||
|
ctx.logger?.info?.("netxops: attach form via settingsScope (legacy one-shot)");
|
||||||
|
deferred.attach(binder.bind({ namespace: NETXOPS_NS }));
|
||||||
|
ensureExtras();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scopeCtx.logger?.info?.("netxops: attach form via settingsScope");
|
} catch (error) {
|
||||||
deferred.attach(binder.bind({ namespace: NETXOPS_NS }));
|
ctx.logger?.warn?.("netxops: legacy settingsScope probe failed: %s", error);
|
||||||
ensureExtras();
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function wireOptionalServices(ctx, card) {
|
function wireOptionalServices(ctx, card) {
|
||||||
const soft = (deps, run, label) => {
|
const soft = (deps, run, label) => {
|
||||||
|
|
|
||||||
74
lib/index.js
74
lib/index.js
|
|
@ -458,11 +458,7 @@ ${declarativePatch.trimEnd()}
|
||||||
${DESKTOP_PATCH_END}
|
${DESKTOP_PATCH_END}
|
||||||
`;
|
`;
|
||||||
writeFileSync(patchPath, `${patch}${managed}`, "utf8");
|
writeFileSync(patchPath, `${patch}${managed}`, "utf8");
|
||||||
logger.info(
|
logger.info("netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
|
||||||
"netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)",
|
|
||||||
profileName,
|
|
||||||
profileName === "desktop" ? "Desktop" : "dsh web"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
function stripManagedPresetBlocks(patch) {
|
function stripManagedPresetBlocks(patch) {
|
||||||
let out = patch;
|
let out = patch;
|
||||||
|
|
@ -1211,16 +1207,20 @@ function opsSkillsRoot() {
|
||||||
return packaged[0];
|
return packaged[0];
|
||||||
}
|
}
|
||||||
function parseFrontmatter(raw) {
|
function parseFrontmatter(raw) {
|
||||||
if (!raw.startsWith("---"))
|
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||||
|
`).replace(/\r/g, `
|
||||||
|
`);
|
||||||
|
if (!text.startsWith("---"))
|
||||||
return;
|
return;
|
||||||
const end = raw.indexOf(`
|
const end = text.indexOf(`
|
||||||
---`, 3);
|
---`, 3);
|
||||||
if (end < 0)
|
if (end < 0)
|
||||||
return;
|
return;
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||||
const data = {};
|
const data = {};
|
||||||
const lines = yaml.split(/\r?\n/);
|
const lines = yaml.split(`
|
||||||
|
`);
|
||||||
for (let i = 0;i < lines.length; i += 1) {
|
for (let i = 0;i < lines.length; i += 1) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||||
|
|
@ -1280,9 +1280,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir);
|
entries = await readdir(groupDir);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||||
}
|
}
|
||||||
const skills = [];
|
const skills = [];
|
||||||
|
const skipped = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join3(groupDir, entry);
|
const full = join3(groupDir, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -1293,11 +1294,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
||||||
}
|
}
|
||||||
if (!isDir)
|
if (!isDir)
|
||||||
continue;
|
continue;
|
||||||
const skill = await loadSkillBundle(full);
|
const skillPath = join3(full, "SKILL.md");
|
||||||
if (skill)
|
let raw;
|
||||||
skills.push(skill);
|
try {
|
||||||
|
raw = await readFile(skillPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: "no SKILL.md" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: "invalid frontmatter" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
|
||||||
|
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? "missing name" : "missing description"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return skills;
|
return { skills, skipped };
|
||||||
}
|
}
|
||||||
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const skillsApi = ctx.skills;
|
const skillsApi = ctx.skills;
|
||||||
|
|
@ -1307,10 +1334,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
||||||
const root = opsSkillsRoot();
|
const root = opsSkillsRoot();
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const enabled = new Set(groupIds);
|
const enabled = new Set(groupIds);
|
||||||
|
const log = ctx.logger;
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId))
|
if (!enabled.has(groupId))
|
||||||
continue;
|
continue;
|
||||||
const skills = await loadGroupSkills(root, groupId);
|
const { skills, skipped } = await loadGroupSkills(root, groupId);
|
||||||
|
for (const miss of skipped) {
|
||||||
|
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]));
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
@ -5988,8 +6022,12 @@ function apply(ctx, config = Config({})) {
|
||||||
if (!connection)
|
if (!connection)
|
||||||
return;
|
return;
|
||||||
const enabled = groupsForPlane(connection.groups, "public");
|
const enabled = groupsForPlane(connection.groups, "public");
|
||||||
unregisterTools = registerNetxTools(toolsCtx, connection, { plane: "public" });
|
try {
|
||||||
toolsCtx.logger.info("netxops: host public tools groups=[%s]", enabled.join(",") || "(none)");
|
unregisterTools = registerNetxTools(toolsCtx, connection, { plane: "public" });
|
||||||
|
toolsCtx.logger.info("netxops: host public tools groups=[%s]", enabled.join(",") || "(none)");
|
||||||
|
} catch (error) {
|
||||||
|
toolsCtx.logger.error("netxops: host public tools register failed: %s", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const remountKbLocal = () => {
|
const remountKbLocal = () => {
|
||||||
unregisterKbLocal?.();
|
unregisterKbLocal?.();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-netxops",
|
"name": "dsh-netxops",
|
||||||
"version": "0.1.49",
|
"version": "0.1.50",
|
||||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -679,12 +679,25 @@ export class NetxopsCardController {
|
||||||
|
|
||||||
inject(): NetxopsCardFace {
|
inject(): NetxopsCardFace {
|
||||||
const actions = this.form.actions()
|
const actions = this.form.actions()
|
||||||
|
const capabilityFields = new Set([
|
||||||
|
'groupOpsInPreset',
|
||||||
|
'groupOpsPublic',
|
||||||
|
'groupTopologyInPreset',
|
||||||
|
'groupTopologyPublic',
|
||||||
|
'groupBizMonitorInPreset',
|
||||||
|
'groupBizMonitorPublic',
|
||||||
|
'groupKbInPreset',
|
||||||
|
'groupKbPublic',
|
||||||
|
])
|
||||||
return {
|
return {
|
||||||
hooks: { netxopsCard: this.store },
|
hooks: { netxopsCard: this.store },
|
||||||
...actions,
|
...actions,
|
||||||
edit: (field, text) => {
|
edit: (field, text) => {
|
||||||
actions.edit(field, text)
|
actions.edit(field, text)
|
||||||
if (field === 'kbRoot') void this.refreshKbPreview()
|
if (field === 'kbRoot') void this.refreshKbPreview()
|
||||||
|
// Capability toggles must hit the host Config immediately — staged-only
|
||||||
|
// edits never publish public tools/skills to other presets.
|
||||||
|
if (capabilityFields.has(field)) void this.form.save()
|
||||||
},
|
},
|
||||||
discard: () => {
|
discard: () => {
|
||||||
actions.discard()
|
actions.discard()
|
||||||
|
|
@ -693,6 +706,7 @@ export class NetxopsCardController {
|
||||||
resetField: (field) => {
|
resetField: (field) => {
|
||||||
actions.resetField(field)
|
actions.resetField(field)
|
||||||
if (field === 'kbRoot') void this.refreshKbPreview()
|
if (field === 'kbRoot') void this.refreshKbPreview()
|
||||||
|
if (capabilityFields.has(field)) void this.form.save()
|
||||||
},
|
},
|
||||||
exportAllSessions: () => { this.exportAllSessions() },
|
exportAllSessions: () => { this.exportAllSessions() },
|
||||||
browseKbRoot: () => { this.browseKbRoot() },
|
browseKbRoot: () => { this.browseKbRoot() },
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* Browser half — Netx Ops settings UI.
|
* Browser half — Netx Ops settings UI.
|
||||||
*
|
*
|
||||||
* Hard-inject only services present on both DSH ≤0.1.5 and ≥0.1.7.
|
* Hard-inject only `slots` + `locale`. Never wait on `settingsScope`
|
||||||
* Do NOT hard-inject `settingsScope`: DSH ≥0.1.7 / 0.2.0 removed it
|
* (removed in DSH ≥0.1.7 / 0.2 — `configForms` replaced it). A pending
|
||||||
* (`configForms` replaced it) and a hard wait kills web boot.
|
* inject surfaces as `waiting for service: settingsScope`, the host never
|
||||||
|
* publishes a connection, and Netx Ops sessions get zero `netx__*` tools.
|
||||||
*
|
*
|
||||||
* Surfaces (dual-stack), matching working plugins like dsh-im-ops:
|
* Surfaces (dual-stack), matching working plugins like dsh-im-ops:
|
||||||
* - Always register `settings.section` in `apply` (Settings sidebar).
|
* - Always register `settings.section` in `apply` (Settings sidebar).
|
||||||
* Gate only the form transport on `configForms` / `settingsScope` —
|
* - Form transport: soft `configForms` (0.2); one-shot `ctx.get('settingsScope')`
|
||||||
* never gate the nav entry itself (Desktop 0.2 often never serves
|
* for ≤0.1.5 only — never `ctx.inject(['settingsScope'])`.
|
||||||
* our ns via whileServed, which previously hid the whole page).
|
|
||||||
* - Soft-attach real form scopes into a deferred memory scope so Save
|
* - Soft-attach real form scopes into a deferred memory scope so Save
|
||||||
* still writes the host entry when the transport is ready.
|
* still writes the host entry when the transport is ready.
|
||||||
* - On ≥0.1.7 also register `plugins.item` via `configForms.whileServed`.
|
* - On ≥0.1.7 also register `plugins.item` via `configForms.whileServed`.
|
||||||
|
|
@ -329,17 +329,23 @@ export function apply(ctx: ClientContext): void {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// DSH ≤0.1.5 — settingsScope binder.
|
// DSH ≤0.1.5 — one-shot settingsScope bind. Do NOT ctx.inject(['settingsScope']):
|
||||||
ctx.inject(['settingsScope'], (scopeCtx) => {
|
// on Desktop 0.2 that service is gone and a waiting inject shows up as
|
||||||
const binder = (scopeCtx as { settingsScope?: SettingsScopeLike }).settingsScope
|
// `dsh-netxops: pending (waiting for service: settingsScope)`, which
|
||||||
if (!binder || typeof binder.bind !== 'function') {
|
// prevents the host bridge from publishing a connection → no netx__* tools.
|
||||||
scopeCtx.logger?.warn?.('netxops: settingsScope present but .bind missing')
|
try {
|
||||||
return
|
const getter = (ctx as { get?: (name: string) => unknown }).get
|
||||||
|
if (typeof getter === 'function' && getter.call(ctx, 'configForms') === undefined) {
|
||||||
|
const binder = getter.call(ctx, 'settingsScope') as SettingsScopeLike | undefined
|
||||||
|
if (binder && typeof binder.bind === 'function') {
|
||||||
|
ctx.logger?.info?.('netxops: attach form via settingsScope (legacy one-shot)')
|
||||||
|
deferred.attach(binder.bind({ namespace: NETXOPS_NS }))
|
||||||
|
ensureExtras()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scopeCtx.logger?.info?.('netxops: attach form via settingsScope')
|
} catch (error) {
|
||||||
deferred.attach(binder.bind({ namespace: NETXOPS_NS }))
|
ctx.logger?.warn?.('netxops: legacy settingsScope probe failed: %s', error)
|
||||||
ensureExtras()
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Soft-wire credentials / connection / directoryPicker when present. */
|
/** Soft-wire credentials / connection / directoryPicker when present. */
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
||||||
groupBizMonitor: 'bizMonitor',
|
groupBizMonitor: 'bizMonitor',
|
||||||
groupKb: 'Knowledge skills',
|
groupKb: 'Knowledge skills',
|
||||||
groupInPreset: 'In Netx Ops preset',
|
groupInPreset: 'In Netx Ops preset',
|
||||||
groupPublic: 'Publish to other presets',
|
groupPublic: 'Publish to other presets (auto-saves; new sessions)',
|
||||||
kbRoot: 'Package root',
|
kbRoot: 'Package root',
|
||||||
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.',
|
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.',
|
||||||
kbBrowse: 'Browse…',
|
kbBrowse: 'Browse…',
|
||||||
|
|
@ -181,7 +181,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
||||||
groupBizMonitor: 'bizMonitor',
|
groupBizMonitor: 'bizMonitor',
|
||||||
groupKb: '知识库技能',
|
groupKb: '知识库技能',
|
||||||
groupInPreset: '在 Netx Ops 预设中启用',
|
groupInPreset: '在 Netx Ops 预设中启用',
|
||||||
groupPublic: '对其他预设公开',
|
groupPublic: '对其他预设公开(勾选后自动保存;新会话生效)',
|
||||||
kbRoot: '知识包根目录',
|
kbRoot: '知识包根目录',
|
||||||
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。',
|
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。',
|
||||||
kbBrowse: '浏览…',
|
kbBrowse: '浏览…',
|
||||||
|
|
|
||||||
14
src/index.ts
14
src/index.ts
|
|
@ -522,11 +522,15 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
||||||
const connection = getNetxConnection()
|
const connection = getNetxConnection()
|
||||||
if (!connection) return
|
if (!connection) return
|
||||||
const enabled = groupsForPlane(connection.groups, 'public')
|
const enabled = groupsForPlane(connection.groups, 'public')
|
||||||
unregisterTools = registerNetxTools(toolsCtx, connection, { plane: 'public' })
|
try {
|
||||||
toolsCtx.logger.info(
|
unregisterTools = registerNetxTools(toolsCtx, connection, { plane: 'public' })
|
||||||
'netxops: host public tools groups=[%s]',
|
toolsCtx.logger.info(
|
||||||
enabled.join(',') || '(none)',
|
'netxops: host public tools groups=[%s]',
|
||||||
)
|
enabled.join(',') || '(none)',
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
toolsCtx.logger.error('netxops: host public tools register failed: %s', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const remountKbLocal = (): void => {
|
const remountKbLocal = (): void => {
|
||||||
unregisterKbLocal?.()
|
unregisterKbLocal?.()
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,17 @@ export function opsSkillsRoot(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFrontmatter(raw: string): { data: Record<string, unknown>; body: string } | undefined {
|
function parseFrontmatter(raw: string): { data: Record<string, unknown>; body: string } | undefined {
|
||||||
if (!raw.startsWith('---')) return undefined
|
// Windows CRLF (and mixed) checkouts break `$`-anchored YAML lines: a trailing
|
||||||
const end = raw.indexOf('\n---', 3)
|
// `\r` is a JS line terminator, so `.` and `$` both refuse it and description
|
||||||
|
// silently drops — netx-ops then never registers. Normalize before parse.
|
||||||
|
const text = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
if (!text.startsWith('---')) return undefined
|
||||||
|
const end = text.indexOf('\n---', 3)
|
||||||
if (end < 0) return undefined
|
if (end < 0) return undefined
|
||||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, '')
|
const yaml = text.slice(3, end).replace(/^\n/, '')
|
||||||
const body = raw.slice(end + 4).replace(/^\r?\n/, '')
|
const body = text.slice(end + 4).replace(/^\n/, '')
|
||||||
const data: Record<string, unknown> = {}
|
const data: Record<string, unknown> = {}
|
||||||
const lines = yaml.split(/\r?\n/)
|
const lines = yaml.split('\n')
|
||||||
for (let i = 0; i < lines.length; i += 1) {
|
for (let i = 0; i < lines.length; i += 1) {
|
||||||
const line = lines[i]
|
const line = lines[i]
|
||||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line)
|
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line)
|
||||||
|
|
@ -124,18 +128,22 @@ export async function loadSkillBundle(dir: string): Promise<ParsedSkill | null>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Why a skill directory was skipped (for Host logs). */
|
||||||
|
export type SkillLoadSkip = { directory: string; reason: string }
|
||||||
|
|
||||||
async function loadGroupSkills(
|
async function loadGroupSkills(
|
||||||
skillsRoot: string,
|
skillsRoot: string,
|
||||||
groupId: NetxCapabilityGroupId,
|
groupId: NetxCapabilityGroupId,
|
||||||
): Promise<ParsedSkill[]> {
|
): Promise<{ skills: ParsedSkill[]; skipped: SkillLoadSkip[] }> {
|
||||||
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId])
|
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId])
|
||||||
let entries: string[]
|
let entries: string[]
|
||||||
try {
|
try {
|
||||||
entries = await readdir(groupDir)
|
entries = await readdir(groupDir)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return { skills: [], skipped: [{ directory: groupDir, reason: 'missing group dir' }] }
|
||||||
}
|
}
|
||||||
const skills: ParsedSkill[] = []
|
const skills: ParsedSkill[] = []
|
||||||
|
const skipped: SkillLoadSkip[] = []
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(groupDir, entry)
|
const full = join(groupDir, entry)
|
||||||
let isDir = false
|
let isDir = false
|
||||||
|
|
@ -145,10 +153,39 @@ async function loadGroupSkills(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!isDir) continue
|
if (!isDir) continue
|
||||||
const skill = await loadSkillBundle(full)
|
const skillPath = join(full, 'SKILL.md')
|
||||||
if (skill) skills.push(skill)
|
let raw: string
|
||||||
|
try {
|
||||||
|
raw = await readFile(skillPath, 'utf8')
|
||||||
|
} catch {
|
||||||
|
skipped.push({ directory: full, reason: 'no SKILL.md' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const parsed = parseFrontmatter(raw)
|
||||||
|
if (!parsed) {
|
||||||
|
skipped.push({ directory: full, reason: 'invalid frontmatter' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const name = typeof parsed.data.name === 'string' ? parsed.data.name.trim() : ''
|
||||||
|
const description = typeof parsed.data.description === 'string'
|
||||||
|
? parsed.data.description.trim()
|
||||||
|
: ''
|
||||||
|
if (!name || !description) {
|
||||||
|
skipped.push({
|
||||||
|
directory: full,
|
||||||
|
reason: !name ? 'missing name' : 'missing description',
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
skills.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: parsed.body.trimStart(),
|
||||||
|
path: skillPath,
|
||||||
|
directory: full,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return skills
|
return { skills, skipped }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -179,9 +216,21 @@ export async function registerGroupSkills(
|
||||||
const root = opsSkillsRoot()
|
const root = opsSkillsRoot()
|
||||||
const disposers: Array<() => void> = []
|
const disposers: Array<() => void> = []
|
||||||
const enabled = new Set(groupIds)
|
const enabled = new Set(groupIds)
|
||||||
|
const log = (ctx as { logger?: { warn: (msg: string, ...args: unknown[]) => void } }).logger
|
||||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||||
if (!enabled.has(groupId)) continue
|
if (!enabled.has(groupId)) continue
|
||||||
const skills = await loadGroupSkills(root, groupId)
|
const { skills, skipped } = await loadGroupSkills(root, groupId)
|
||||||
|
for (const miss of skipped) {
|
||||||
|
log?.warn?.('%s: skip skill dir %s (%s)', providerLabel, miss.directory, miss.reason)
|
||||||
|
}
|
||||||
|
if (skills.length === 0) {
|
||||||
|
log?.warn?.(
|
||||||
|
'%s: no skills loaded for group=%s from %s',
|
||||||
|
providerLabel,
|
||||||
|
groupId,
|
||||||
|
join(root, SKILL_DIR_BY_GROUP[groupId]),
|
||||||
|
)
|
||||||
|
}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|
|
||||||
|
|
@ -59,18 +59,22 @@ export function applyGroupToolsPlugin(ctx: Context, options: GroupToolsPluginOpt
|
||||||
ctx.logger.info('%s: no groups enabled', options.name)
|
ctx.logger.info('%s: no groups enabled', options.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
unregisterTools = registerNetxTools(ctx, connection, {
|
try {
|
||||||
plane: 'preset',
|
unregisterTools = registerNetxTools(ctx, connection, {
|
||||||
only: options.mode === 'forced' ? enabled : options.only,
|
plane: 'preset',
|
||||||
forceGroups: options.mode === 'forced' ? enabled : undefined,
|
only: options.mode === 'forced' ? enabled : options.only,
|
||||||
})
|
forceGroups: options.mode === 'forced' ? enabled : undefined,
|
||||||
ctx.logger.info(
|
})
|
||||||
'%s: groups=[%s] → %s tokenConfigured=%s',
|
ctx.logger.info(
|
||||||
options.name,
|
'%s: groups=[%s] → %s tokenConfigured=%s',
|
||||||
enabled.join(',') || '(none)',
|
options.name,
|
||||||
connection.apiUrl,
|
enabled.join(',') || '(none)',
|
||||||
connection.token.trim().length > 0,
|
connection.apiUrl,
|
||||||
)
|
connection.token.trim().length > 0,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error('%s: registerNetxTools failed: %s', options.name, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const remountKbLocal = (): void => {
|
const remountKbLocal = (): void => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue