mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-15 08:30:44 +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
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -111,16 +111,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -180,9 +184,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -193,11 +198,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -207,10 +238,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -2996,12 +3034,16 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
ctx.logger.info("%s: no groups enabled", options.name);
|
||||
return;
|
||||
}
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
try {
|
||||
unregisterTools = registerNetxTools(ctx, connection, {
|
||||
plane: "preset",
|
||||
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);
|
||||
} catch (error) {
|
||||
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
|
||||
}
|
||||
};
|
||||
const remountKbLocal = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
|
|
@ -2397,6 +2397,16 @@ class NetxopsCardController {
|
|||
}
|
||||
inject() {
|
||||
const actions = this.form.actions();
|
||||
const capabilityFields = new Set([
|
||||
"groupOpsInPreset",
|
||||
"groupOpsPublic",
|
||||
"groupTopologyInPreset",
|
||||
"groupTopologyPublic",
|
||||
"groupBizMonitorInPreset",
|
||||
"groupBizMonitorPublic",
|
||||
"groupKbInPreset",
|
||||
"groupKbPublic"
|
||||
]);
|
||||
return {
|
||||
hooks: { netxopsCard: this.store },
|
||||
...actions,
|
||||
|
|
@ -2404,6 +2414,8 @@ class NetxopsCardController {
|
|||
actions.edit(field, text);
|
||||
if (field === "kbRoot")
|
||||
this.refreshKbPreview();
|
||||
if (capabilityFields.has(field))
|
||||
this.form.save();
|
||||
},
|
||||
discard: () => {
|
||||
actions.discard();
|
||||
|
|
@ -2413,6 +2425,8 @@ class NetxopsCardController {
|
|||
actions.resetField(field);
|
||||
if (field === "kbRoot")
|
||||
this.refreshKbPreview();
|
||||
if (capabilityFields.has(field))
|
||||
this.form.save();
|
||||
},
|
||||
exportAllSessions: () => {
|
||||
this.exportAllSessions();
|
||||
|
|
@ -2492,7 +2506,7 @@ var en = {
|
|||
groupBizMonitor: "bizMonitor",
|
||||
groupKb: "Knowledge skills",
|
||||
groupInPreset: "In Netx Ops preset",
|
||||
groupPublic: "Publish to other presets",
|
||||
groupPublic: "Publish to other presets (auto-saves; new sessions)",
|
||||
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.",
|
||||
kbBrowse: "Browse…",
|
||||
|
|
@ -2570,7 +2584,7 @@ var zh = {
|
|||
groupBizMonitor: "bizMonitor",
|
||||
groupKb: "知识库技能",
|
||||
groupInPreset: "在 Netx Ops 预设中启用",
|
||||
groupPublic: "对其他预设公开",
|
||||
groupPublic: "对其他预设公开(勾选后自动保存;新会话生效)",
|
||||
kbRoot: "知识包根目录",
|
||||
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。",
|
||||
kbBrowse: "浏览…",
|
||||
|
|
@ -2865,16 +2879,19 @@ function apply(ctx) {
|
|||
formsCtx.logger?.warn?.("netxops: configForms present but no get/whileServed match for %s", candidates.join(","));
|
||||
}
|
||||
});
|
||||
ctx.inject(["settingsScope"], (scopeCtx) => {
|
||||
const binder = scopeCtx.settingsScope;
|
||||
if (!binder || typeof binder.bind !== "function") {
|
||||
scopeCtx.logger?.warn?.("netxops: settingsScope present but .bind missing");
|
||||
return;
|
||||
try {
|
||||
const getter = ctx.get;
|
||||
if (typeof getter === "function" && getter.call(ctx, "configForms") === undefined) {
|
||||
const binder = getter.call(ctx, "settingsScope");
|
||||
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");
|
||||
deferred.attach(binder.bind({ namespace: NETXOPS_NS }));
|
||||
ensureExtras();
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.logger?.warn?.("netxops: legacy settingsScope probe failed: %s", error);
|
||||
}
|
||||
}
|
||||
function wireOptionalServices(ctx, card) {
|
||||
const soft = (deps, run, label) => {
|
||||
|
|
|
|||
74
lib/index.js
74
lib/index.js
|
|
@ -458,11 +458,7 @@ ${declarativePatch.trimEnd()}
|
|||
${DESKTOP_PATCH_END}
|
||||
`;
|
||||
writeFileSync(patchPath, `${patch}${managed}`, "utf8");
|
||||
logger.info(
|
||||
"netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)",
|
||||
profileName,
|
||||
profileName === "desktop" ? "Desktop" : "dsh web"
|
||||
);
|
||||
logger.info("netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
|
||||
}
|
||||
function stripManagedPresetBlocks(patch) {
|
||||
let out = patch;
|
||||
|
|
@ -1211,16 +1207,20 @@ function opsSkillsRoot() {
|
|||
return packaged[0];
|
||||
}
|
||||
function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith("---"))
|
||||
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
|
||||
`).replace(/\r/g, `
|
||||
`);
|
||||
if (!text.startsWith("---"))
|
||||
return;
|
||||
const end = raw.indexOf(`
|
||||
const end = text.indexOf(`
|
||||
---`, 3);
|
||||
if (end < 0)
|
||||
return;
|
||||
const yaml = raw.slice(3, end).replace(/^\r?\n/, "");
|
||||
const body = raw.slice(end + 4).replace(/^\r?\n/, "");
|
||||
const yaml = text.slice(3, end).replace(/^\n/, "");
|
||||
const body = text.slice(end + 4).replace(/^\n/, "");
|
||||
const data = {};
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
const lines = yaml.split(`
|
||||
`);
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
|
||||
|
|
@ -1280,9 +1280,10 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
try {
|
||||
entries = await readdir(groupDir);
|
||||
} catch {
|
||||
return [];
|
||||
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
|
||||
}
|
||||
const skills = [];
|
||||
const skipped = [];
|
||||
for (const entry of entries) {
|
||||
const full = join3(groupDir, entry);
|
||||
let isDir = false;
|
||||
|
|
@ -1293,11 +1294,37 @@ async function loadGroupSkills(skillsRoot, groupId) {
|
|||
}
|
||||
if (!isDir)
|
||||
continue;
|
||||
const skill = await loadSkillBundle(full);
|
||||
if (skill)
|
||||
skills.push(skill);
|
||||
const skillPath = join3(full, "SKILL.md");
|
||||
let raw;
|
||||
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) {
|
||||
const skillsApi = ctx.skills;
|
||||
|
|
@ -1307,10 +1334,17 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
const root = opsSkillsRoot();
|
||||
const disposers = [];
|
||||
const enabled = new Set(groupIds);
|
||||
const log = ctx.logger;
|
||||
for (const groupId of CAPABILITY_GROUP_IDS) {
|
||||
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, join3(root, SKILL_DIR_BY_GROUP[groupId]));
|
||||
}
|
||||
for (const skill of skills) {
|
||||
disposers.push(skillsApi.register({
|
||||
name: skill.name,
|
||||
|
|
@ -5988,8 +6022,12 @@ function apply(ctx, config = Config({})) {
|
|||
if (!connection)
|
||||
return;
|
||||
const enabled = groupsForPlane(connection.groups, "public");
|
||||
unregisterTools = registerNetxTools(toolsCtx, connection, { plane: "public" });
|
||||
toolsCtx.logger.info("netxops: host public tools groups=[%s]", enabled.join(",") || "(none)");
|
||||
try {
|
||||
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 = () => {
|
||||
unregisterKbLocal?.();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue