diff --git a/cordis.bundle.patch.yml b/cordis.bundle.patch.yml index 8b9094c..75621a3 100644 --- a/cordis.bundle.patch.yml +++ b/cordis.bundle.patch.yml @@ -37,6 +37,8 @@ groupOpsPublic: false groupTopologyInPreset: false groupTopologyPublic: false + groupBizMonitorInPreset: true + groupBizMonitorPublic: false kbRoot: '' groupKbInPreset: true groupKbPublic: false diff --git a/cordis.patch.yml b/cordis.patch.yml index bf23083..a2aa5cd 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -33,6 +33,8 @@ groupOpsPublic: false groupTopologyInPreset: false groupTopologyPublic: false + groupBizMonitorInPreset: true + groupBizMonitorPublic: false kbRoot: '' groupKbInPreset: true groupKbPublic: false diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 9b03282..4867d02 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -43,6 +43,7 @@ bun run 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` 再重启 | +| `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`。 diff --git a/lib/agent-tools-biz-monitor.js b/lib/agent-tools-biz-monitor.js index 761642c..8c45b98 100644 --- a/lib/agent-tools-biz-monitor.js +++ b/lib/agent-tools-biz-monitor.js @@ -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?.(); diff --git a/lib/agent-tools-common.js b/lib/agent-tools-common.js index 7d8eff6..c4b9bdf 100644 --- a/lib/agent-tools-common.js +++ b/lib/agent-tools-common.js @@ -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?.(); diff --git a/lib/agent-tools-nms.js b/lib/agent-tools-nms.js index 497e79f..61c606c 100644 --- a/lib/agent-tools-nms.js +++ b/lib/agent-tools-nms.js @@ -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?.(); diff --git a/lib/agent-tools-ops.js b/lib/agent-tools-ops.js index 93f1558..1b3e4f3 100644 --- a/lib/agent-tools-ops.js +++ b/lib/agent-tools-ops.js @@ -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?.(); diff --git a/lib/agent-tools-topology.js b/lib/agent-tools-topology.js index 9f91257..dcabd2c 100644 --- a/lib/agent-tools-topology.js +++ b/lib/agent-tools-topology.js @@ -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?.(); diff --git a/lib/agent-tools.js b/lib/agent-tools.js index b5ad2cf..2f25f69 100644 --- a/lib/agent-tools.js +++ b/lib/agent-tools.js @@ -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?.(); diff --git a/lib/client.js b/lib/client.js index bb113d1..bedcf41 100644 --- a/lib/client.js +++ b/lib/client.js @@ -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) => { diff --git a/lib/index.js b/lib/index.js index 3556dd7..b612922 100644 --- a/lib/index.js +++ b/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?.(); diff --git a/package.json b/package.json index 3c42576..f01ac27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "license": "MIT", "type": "module", diff --git a/src/client/controller.ts b/src/client/controller.ts index 0540222..8ff3819 100644 --- a/src/client/controller.ts +++ b/src/client/controller.ts @@ -679,12 +679,25 @@ export class NetxopsCardController { inject(): NetxopsCardFace { const actions = this.form.actions() + const capabilityFields = new Set([ + 'groupOpsInPreset', + 'groupOpsPublic', + 'groupTopologyInPreset', + 'groupTopologyPublic', + 'groupBizMonitorInPreset', + 'groupBizMonitorPublic', + 'groupKbInPreset', + 'groupKbPublic', + ]) return { hooks: { netxopsCard: this.store }, ...actions, edit: (field, text) => { actions.edit(field, text) 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: () => { actions.discard() @@ -693,6 +706,7 @@ export class NetxopsCardController { resetField: (field) => { actions.resetField(field) if (field === 'kbRoot') void this.refreshKbPreview() + if (capabilityFields.has(field)) void this.form.save() }, exportAllSessions: () => { this.exportAllSessions() }, browseKbRoot: () => { this.browseKbRoot() }, diff --git a/src/client/index.ts b/src/client/index.ts index 55397b1..faa2230 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,15 +1,15 @@ /** * Browser half — Netx Ops settings UI. * - * Hard-inject only services present on both DSH ≤0.1.5 and ≥0.1.7. - * Do NOT hard-inject `settingsScope`: DSH ≥0.1.7 / 0.2.0 removed it - * (`configForms` replaced it) and a hard wait kills web boot. + * Hard-inject only `slots` + `locale`. Never wait on `settingsScope` + * (removed in DSH ≥0.1.7 / 0.2 — `configForms` replaced it). A pending + * 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: * - Always register `settings.section` in `apply` (Settings sidebar). - * Gate only the form transport on `configForms` / `settingsScope` — - * never gate the nav entry itself (Desktop 0.2 often never serves - * our ns via whileServed, which previously hid the whole page). + * - Form transport: soft `configForms` (0.2); one-shot `ctx.get('settingsScope')` + * for ≤0.1.5 only — never `ctx.inject(['settingsScope'])`. * - Soft-attach real form scopes into a deferred memory scope so Save * still writes the host entry when the transport is ready. * - 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. - ctx.inject(['settingsScope'], (scopeCtx) => { - const binder = (scopeCtx as { settingsScope?: SettingsScopeLike }).settingsScope - if (!binder || typeof binder.bind !== 'function') { - scopeCtx.logger?.warn?.('netxops: settingsScope present but .bind missing') - return + // DSH ≤0.1.5 — one-shot settingsScope bind. Do NOT ctx.inject(['settingsScope']): + // on Desktop 0.2 that service is gone and a waiting inject shows up as + // `dsh-netxops: pending (waiting for service: settingsScope)`, which + // prevents the host bridge from publishing a connection → no netx__* tools. + try { + 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') - deferred.attach(binder.bind({ namespace: NETXOPS_NS })) - ensureExtras() - }) + } catch (error) { + ctx.logger?.warn?.('netxops: legacy settingsScope probe failed: %s', error) + } } /** Soft-wire credentials / connection / directoryPicker when present. */ diff --git a/src/client/locales.ts b/src/client/locales.ts index 154daf1..78fe328 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -102,7 +102,7 @@ export const en: Record = { 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…', @@ -181,7 +181,7 @@ export const zh: Record = { groupBizMonitor: 'bizMonitor', groupKb: '知识库技能', groupInPreset: '在 Netx Ops 预设中启用', - groupPublic: '对其他预设公开', + groupPublic: '对其他预设公开(勾选后自动保存;新会话生效)', kbRoot: '知识包根目录', kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。', kbBrowse: '浏览…', diff --git a/src/index.ts b/src/index.ts index d3e206d..689754f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -522,11 +522,15 @@ export function apply(ctx: Context, config: Config = Config({})): void { const connection = getNetxConnection() 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 = (): void => { unregisterKbLocal?.() diff --git a/src/netx/group-skills.ts b/src/netx/group-skills.ts index 7cd587a..bc211a2 100644 --- a/src/netx/group-skills.ts +++ b/src/netx/group-skills.ts @@ -56,13 +56,17 @@ export function opsSkillsRoot(): string { } function parseFrontmatter(raw: string): { data: Record; body: string } | undefined { - if (!raw.startsWith('---')) return undefined - const end = raw.indexOf('\n---', 3) + // Windows CRLF (and mixed) checkouts break `$`-anchored YAML lines: a trailing + // `\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 - 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: Record = {} - const lines = yaml.split(/\r?\n/) + const lines = yaml.split('\n') for (let i = 0; i < lines.length; i += 1) { const line = lines[i] const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line) @@ -124,18 +128,22 @@ export async function loadSkillBundle(dir: string): Promise } } +/** Why a skill directory was skipped (for Host logs). */ +export type SkillLoadSkip = { directory: string; reason: string } + async function loadGroupSkills( skillsRoot: string, groupId: NetxCapabilityGroupId, -): Promise { +): Promise<{ skills: ParsedSkill[]; skipped: SkillLoadSkip[] }> { const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]) let entries: string[] try { entries = await readdir(groupDir) } catch { - return [] + return { skills: [], skipped: [{ directory: groupDir, reason: 'missing group dir' }] } } const skills: ParsedSkill[] = [] + const skipped: SkillLoadSkip[] = [] for (const entry of entries) { const full = join(groupDir, entry) let isDir = false @@ -145,10 +153,39 @@ async function loadGroupSkills( continue } if (!isDir) continue - const skill = await loadSkillBundle(full) - if (skill) skills.push(skill) + const skillPath = join(full, 'SKILL.md') + 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 disposers: Array<() => void> = [] const enabled = new Set(groupIds) + const log = (ctx as { logger?: { warn: (msg: string, ...args: unknown[]) => void } }).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, diff --git a/src/netx/group-tools-plugin.ts b/src/netx/group-tools-plugin.ts index 19a2239..7ea3519 100644 --- a/src/netx/group-tools-plugin.ts +++ b/src/netx/group-tools-plugin.ts @@ -59,18 +59,22 @@ export function applyGroupToolsPlugin(ctx: Context, options: GroupToolsPluginOpt 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 = (): void => {