cordis-plugin-sofagent
一次挂载 sofagent 全套能力——6 项能力一次到位(注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)(seam: non-seam:plugin-suite)——只编排不重实现——sofagent 约束层在 DSH(DeepSeek Harness)生态的插件形态。
Install
npx skills add https://github.com/KongFangXun/sofagent/tree/main/engine/dsh-plugins/cordis-plugin-sofagent
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kongfangxun-sofagent@llmmart
git clone https://github.com/KongFangXun/sofagent.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole kongfangxun/sofagent collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
cordis-plugin-sofagent
一次挂载 sofagent 全套能力——6 项能力一次到位(注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)(seam: non-seam:plugin-suite)——只编排不重实现,能力仍由各原子插件提供
用途
一次 apply 把 6 个原子插件(inject / audit / evolve / rollback / daemon / fde)逐个挂到同一个 ctx 上,挂完把结果(loaded / failed / total)注册成 sofagent.suite 服务。适合「整套装上、不想逐个装」的场景。什么时候用:一次拿到全套约束层能力、不想逐个挑插件时。
三条硬约束:
- 只编排,不重实现 —— 每个能力仍由原子插件
provide,本层只依次调用它们的apply,不复制任何子插件逻辑; - 逐个降级,不整挂失败 —— 缺任一个原子插件只记入
failed数组(含包名与原因),其余 5 个照常加载; - 不替代细粒度插件 —— 6 个原子插件全部保留,本插件是新增的整装选项,按需二选一。
本插件随 sofagent 主线版本发布,走宿主挂载通道:skillhub install cordis-plugin-sofagent。⚠️ 本插件不在 npm 通道——11 款插件(7 DSH + 4 OpenClaw,v1.4.9 P2 合并批 14→11)均为 private,npm 装不到;npm 侧另有库通道 npm i -g sofagent(总包 engine/umbrella,聚合 @sofagent/audit / mcp / orchestrator / daemon 四个能力包,不含本插件)。两条通道按所在环境二选一,互不依赖、互不为前置。版本号与 sofagent 主线对齐。
本插件是宿主挂载入口;npm 安装入口见裸名总包(engine/umbrella,包名 sofagent)—— 两个入口互不引用为前置,按使用场景各取其一。
相关链接
- sofagent 主仓:https://github.com/KongFangXun/sofagent
- 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族)
Files (sofagent)
-
src
-
index.test.ts 12.7 KB
// ============================================================ // cordis-plugin-sofagent · 插件单测(v1.4.8 第8批 · 聚合编排层;v1.4.9 P2 合并批:9→6) // ============================================================ // 断言面 = 三条硬约束(P2 合并后口径): // ① 只编排不重实现 —— 注册的服务只有 6 个原子插件 + 本层的 sofagent.suite // ② 逐个降级不整挂失败 —— 缺一个原子插件,其余 5 个照常;failed 精确报出缺的那一个 // ③ 不替代细粒度插件 —— 编排清单与 plugins.json 的 suite 段逐条一致(6 个原子插件全在) // ============================================================ import { describe, it, expect, vi } from 'vitest'; import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import plugin, { pluginMeta, capability, suite } from './index'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PLUGIN_DIR = path.resolve(HERE, '..'); const SIBLINGS_DIR = path.resolve(PLUGIN_DIR, '..'); /** 聚合层 apply 的结果快照(与 src/index.ts 的 SuiteReport 同形) */ interface SuiteReport { loaded: string[]; failed: Array<{ name: string; reason: string }>; total: number; capability: string; viaHost: string[]; viaDirect: string[]; unloadHook: 'ctx.on' | 'apply-return'; } /** 假 ctx 的可选宿主能力开关(用于覆盖三条挂载/卸载路径) */ interface CtxOptions { /** 是否提供宿主惯用法 ctx.plugin(false = 裸 ctx,只能回落直呼 apply) */ withHostPlugin?: boolean; /** 是否提供宿主生命周期钩子 ctx.on('dispose', …) */ withDisposeHook?: boolean; /** ctx.plugin 是否抛错(模拟宿主拒收 → 必须回落直呼) */ rejectHostMount?: boolean; } /** * 构造鸭子类型 ctx:**对齐 cordis 4.x 的真实契约**(实测 ~/.dsh/.../@deepseek-ai/cordis 4.0.2)。 * ① `provide(name, value)` **返回 disposer**(宿主实现走 fiber.effect,返回反注册函数); * ② `plugin(p)` 挂载插件并返回 ForkScope(含 `dispose`),句柄 dispose 时反向撤销该子插件的注册; * ③ `on('dispose', fn)` 登记卸载监听。 * 🔴 刻意不 import 任何宿主类型——与适配层红线同口径(ctx: unknown + 结构访问)。 */ function makeCtx(opts: CtxOptions = {}) { const { withHostPlugin = true, withDisposeHook = true, rejectHostMount = false } = opts; const services = new Map<string, unknown>(); /** ctx.on('dispose') 登记的回调(模拟宿主卸载事件触发面) */ const onDispose: Array<() => unknown> = []; /** 经宿主路径挂载过的插件(断言「走的是宿主惯用法」) */ const mounted: unknown[] = []; const ctx: Record<string, unknown> = { services, onDispose, mounted, provide: (name: string, service: unknown): (() => void) => { services.set(name, service); return () => { services.delete(name); }; }, sofagent: {}, }; if (withHostPlugin) { ctx.plugin = (p: { apply?: (c: unknown) => unknown }): { dispose: () => Promise<void> } => { if (rejectHostMount) throw new Error('宿主拒收(合成:契约不符)'); mounted.push(p); // 宿主职责:调用插件 apply 并**按 fiber 托管其返回的 disposer** const childDisposers: Array<() => unknown> = []; const ret = typeof p?.apply === 'function' ? p.apply(ctx) : undefined; if (typeof ret === 'function') childDisposers.push(ret as () => unknown); return { dispose: async (): Promise<void> => { for (const undo of childDisposers.splice(0).reverse()) await undo(); }, }; }; } if (withDisposeHook) { ctx.on = (event: string, listener: () => unknown): (() => void) => { if (event === 'dispose') onDispose.push(listener); return () => { const i = onDispose.indexOf(listener); if (i >= 0) onDispose.splice(i, 1); }; }; } return ctx as { services: Map<string, unknown>; onDispose: Array<() => unknown>; mounted: unknown[]; sofagent: Record<string, unknown>; plugin?: unknown; on?: unknown; provide: (name: string, service: unknown) => () => void; }; } /** 取聚合层自己 provide 的报告 */ function reportOf(ctx: ReturnType<typeof makeCtx>): SuiteReport { return ctx.services.get('sofagent.suite') as SuiteReport; } describe('cordis-plugin-sofagent', () => { it('插件元数据完整(id/version/seam/description 与 package.json SSOT 对齐)', () => { const pkg = require('../package.json') as { version: string }; expect(pluginMeta.id).toBe('cordis-plugin-sofagent'); expect(pluginMeta.version).toBe(pkg.version); // SSOT 对齐:版本跟随主版本(读 package.json) expect(pluginMeta.seam).toBe('non-seam:plugin-suite'); expect(pluginMeta.description).toContain(`seam: ${pluginMeta.seam}`); // 描述不滞后于契约 }); it('编排清单恰好 6 条(v1.4.9 P2 合并批),且与 plugins.json 的 suite 段逐条一致', () => { const manifest = require('../../plugins.json') as { plugins: Array<{ id: string; kind?: string; capability: string; suite?: string[] }>; }; const entry = manifest.plugins.find((p) => p.id === pluginMeta.id); expect(entry, 'plugins.json 未登记聚合条目').toBeDefined(); expect(entry!.kind).toBe('suite'); // 聚合型条目(bridgePkg/bridgeApi 语义不适用) expect(capability).toBe(entry!.capability); expect(suite).toHaveLength(6); expect(suite.map(([, pkg]) => pkg).sort()).toEqual([...entry!.suite!].sort()); }); it('P2 合并落位:SUITE 不含已并入的 -gate/-ontology/-commons 三包', () => { const pkgs = suite.map(([, pkg]) => pkg); expect(pkgs).not.toContain('cordis-plugin-sofagent-gate'); expect(pkgs).not.toContain('cordis-plugin-sofagent-ontology'); expect(pkgs).not.toContain('cordis-plugin-sofagent-commons'); }); it('一次 apply 挂满 6 个能力,且 6 个 sofagent.* 服务逐个可访问', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const ctx = makeCtx(); await plugin.apply(ctx); const report = reportOf(ctx); expect(report.total).toBe(6); expect(report.failed).toEqual([]); expect([...report.loaded].sort()).toEqual(suite.map(([key]) => key).sort()); // 逐个断言(不只看数组长度)——每个原子插件的能力确实挂上了 for (const [key] of suite) { const svc = ctx.services.get(`sofagent.${key}`) as { invoke?: unknown } | undefined; expect(svc, `sofagent.${key} 未注册`).toBeDefined(); expect(typeof svc!.invoke).toBe('function'); } errSpy.mockRestore(); }); it('只编排不重实现:注册面 = 6 个原子服务 + sofagent.suite,本层不复制任何子插件逻辑', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const ctx = makeCtx(); await plugin.apply(ctx); const names = [...ctx.services.keys()].sort(); expect(names).toHaveLength(7); // 6 原子 + 1 聚合报告 expect(names.filter((n) => n !== 'sofagent.suite')).toHaveLength(6); expect(reportOf(ctx).capability).toBe(capability); errSpy.mockRestore(); }); it('逐个降级:某原子插件缺 dist/ → 其余 5 个照常加载,failed 精确报出缺的那一个', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const fdeDist = path.join(SIBLINGS_DIR, 'cordis-plugin-sofagent-fde', 'dist', 'index.js'); const stash = `${fdeDist}.__hidden__`; expect(fs.existsSync(fdeDist), '前置条件:fde 已 build(npm run build)').toBe(true); fs.renameSync(fdeDist, stash); try { // 清模块缓存后重新 import——否则拿到的是前面测试已缓存的 fde 模块,注入不生效 vi.resetModules(); const fresh = (await import('./index')).default; const ctx = makeCtx(); await fresh.apply(ctx); const report = reportOf(ctx); expect(report.loaded).toHaveLength(5); expect(report.loaded).not.toContain('fde'); expect(report.failed).toHaveLength(1); // 精确 1 个 expect(report.failed[0].name).toBe('fde'); expect(report.failed[0].reason.length).toBeGreaterThan(0); expect(ctx.services.get('sofagent.fde')).toBeUndefined(); // 其余 5 个能力仍逐个可用(缺一个不让其余 5 个挂掉) for (const [key] of suite.filter(([k]) => k !== 'fde')) { expect(ctx.services.get(`sofagent.${key}`), `sofagent.${key} 应仍可用`).toBeDefined(); } } finally { if (fs.existsSync(stash)) fs.renameSync(stash, fdeDist); errSpy.mockRestore(); } }); it('A2·挂载路径:ctx.plugin 在场时走宿主惯用法(逐插件带 inject 声明),不静默直呼', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const ctx = makeCtx(); await plugin.apply(ctx); const report = reportOf(ctx); expect(report.viaHost, '宿主路径挂载数').toHaveLength(6); expect(report.viaDirect, '宿主可用时回落路径应为空').toEqual([]); expect(ctx.mounted, '宿主 ctx.plugin 收到的插件数').toHaveLength(6); expect(report.loaded.length + report.failed.length, '守恒:loaded + failed = total').toBe(report.total); // 走宿主路径的插件必须自带 inject 声明——否则宿主无从做就绪门控(A2 根因 1) for (const p of ctx.mounted as Array<{ inject?: readonly string[] }>) { expect(Array.isArray(p.inject), '插件对象须声明 inject').toBe(true); expect(p.inject).toContain('settings'); expect(p.inject).toContain('dynamicCordisRunner'); } errSpy.mockRestore(); }); it('A2·卸载路径:聚合层卸载后本层与 6 个原子服务全部反注册(不留残留)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const ctx = makeCtx(); const dispose = await plugin.apply(ctx); expect(ctx.services.get('sofagent.suite'), '装载后本层报告服务在').toBeDefined(); expect(ctx.services.get('sofagent.audit'), '装载后原子服务在').toBeDefined(); expect(ctx.services.size, '装载面 = 6 原子 + 1 聚合报告').toBe(7); // 宿主生命周期钩子已登记(双保险之一) expect(ctx.onDispose).toHaveLength(1); expect(reportOf(ctx).unloadHook).toBe('ctx.on'); // 触发卸载(模拟宿主 dispose 事件) await ctx.onDispose[0](); expect(ctx.services.get('sofagent.suite'), '卸载后本层服务须反注册').toBeUndefined(); expect(ctx.services.get('sofagent.audit'), '卸载后原子服务须反注册').toBeUndefined(); expect(ctx.services.size, '卸载后 ctx 上不留任何 sofagent.* 服务').toBe(0); // 幂等:apply 返回值再调一次不抛、不复活 await dispose(); expect(ctx.services.size).toBe(0); errSpy.mockRestore(); }); it('A2·回落路径:宿主拒收 ctx.plugin 时回落直呼 apply,逐个降级不整挂失败', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const ctx = makeCtx({ rejectHostMount: true }); await plugin.apply(ctx); const report = reportOf(ctx); expect(report.viaHost, '宿主拒收 → 不应有宿主路径挂载').toEqual([]); expect(report.viaDirect, '全部走回落直呼').toHaveLength(6); expect(report.loaded).toHaveLength(6); expect(report.failed).toEqual([]); expect(report.loaded.length + report.failed.length).toBe(report.total); errSpy.mockRestore(); }); it('A2·静默空转已修:子插件无 apply 时计入 failed,不得谎报 loaded', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.doMock('cordis-plugin-sofagent-evolve', () => ({ default: { pluginMeta: {}, capability: '' } })); // 无 apply vi.resetModules(); try { const fresh = (await import('./index')).default; const ctx = makeCtx({ withHostPlugin: false, withDisposeHook: false }); // 裸 ctx:必走回落分支 await fresh.apply(ctx); const report = reportOf(ctx); expect(report.loaded, '无 apply 者不得计入 loaded').not.toContain('evolve'); expect(report.failed.map((f) => f.name)).toContain('evolve'); expect(report.loaded).toHaveLength(5); expect(report.loaded.length + report.failed.length, '守恒:6 = loaded + failed').toBe(6); expect(ctx.services.get('sofagent.evolve'), '缺 apply 者不得注册服务').toBeUndefined(); expect(report.unloadHook, '裸 ctx 无 ctx.on → 仅靠 apply 返回值').toBe('apply-return'); } finally { vi.doUnmock('cordis-plugin-sofagent-evolve'); vi.resetModules(); errSpy.mockRestore(); } }); }); -
index.ts 10.8 KB
// cordis-plugin-sofagent · DSH 反向插件(v1.5.2 第8批 · 聚合编排层;v1.5.2 P2 合并批:SUITE 9→6) // seam 挂载:non-seam:plugin-suite # 语义:非宿主事件接入(插件聚合)——一次 apply 逐个挂载 6 个原子插件;能力仍由各原子插件 provide // 清单生成源 = engine/dsh-plugins/plugins.json(生成 package.json 的 description/sofagent/dsh/optionalDependencies 段与 cordis.patch.yml);本文件的 seam 字面量由生成器 --check 与之对账。 // // 🔴 三条硬约束(缺一不可): // ① 只编排,不重实现——每个能力仍由原子插件 provide,本层只依次调用它们的 apply; // ② 逐个降级,不整挂失败——缺任一原子插件只记入 failed 数组,其余 5 个照常加载; // ③ 不替代细粒度插件——6 个原子插件全部保留,本插件是**新增的整装选项**,不是替代品。 // // v1.5.2 P2 合并说明:原 -ontology/-commons 并入 -fde(厚插件三域), // 原 -gate 并入 -audit(验收 seam 四值)——原子插件 9→6,本 SUITE 同批收口。 const SUITE: ReadonlyArray<readonly [string, string]> = [ ['inject', 'cordis-plugin-sofagent-inject'], ['audit', 'cordis-plugin-sofagent-audit'], ['evolve', 'cordis-plugin-sofagent-evolve'], ['rollback', 'cordis-plugin-sofagent-rollback'], ['daemon', 'cordis-plugin-sofagent-daemon'], ['fde', 'cordis-plugin-sofagent-fde'], ]; /** 插件自己的 package.json(版本 SSOT 在插件自身,与其余 6 个插件同形) */ const HOST_PKG = require('../package.json') as { version?: string }; /** * 插件元数据(DSH profile / 注册表消费)。 * 纯声明——本层**不注册任何自己的能力**,故此处只有身份与 seam,没有 bridge 字段。 */ export const pluginMeta = { id: 'cordis-plugin-sofagent', version: HOST_PKG.version ?? '0.0.0-unknown', description: '一次挂载 sofagent 全套能力(聚合编排层,只编排不重实现)(seam: non-seam:plugin-suite)', seam: 'non-seam:plugin-suite', } as const; /** 依赖的 sofagent 能力说明(DSH skill 引导链展示) */ export const capability = '一次挂载 sofagent 全套能力(6 项)'; /** 被编排的原子插件清单(短名 → 包名)——供外部只读查阅(与 plugins.json 的 suite 段同源) */ export const suite: ReadonlyArray<readonly [string, string]> = SUITE; /** * 一次 apply 的结果快照:哪几个挂上了、哪几个没挂上(失败**逐个可见**,不静默)。 * 另记「走的哪条挂载路径」与「卸载钩子形态」——三条信息合起来才看得出是否有静默降级。 */ export interface SuiteReport { /** 成功挂载的原子插件短名,如 ['inject', 'audit', …](宿主路径=宿主已收;回落路径=apply 已执行) */ readonly loaded: string[]; /** 未挂上的原子插件(短名 + 失败原因)——非空即说明该能力缺席,但其余能力不受影响 */ readonly failed: Array<{ name: string; reason: string }>; /** 原子插件总数(恒为 6) */ readonly total: number; /** 能力说明(与 capability 同值) */ readonly capability: string; /** 经宿主惯用法 `ctx.plugin()` 挂载的短名——这些拿到 inject 就绪门控 + 宿主托管卸载 */ readonly viaHost: string[]; /** 经回落路径「直呼 apply」挂载的短名——裸 ctx / 宿主无 ctx.plugin 时即全部 6 个 */ readonly viaDirect: string[]; /** 卸载钩子形态:`ctx.on` = 宿主生命周期事件;`apply-return` = 仅靠 apply 返回值(宿主据 _execute 登记) */ readonly unloadHook: 'ctx.on' | 'apply-return'; } /** 子插件的可调用面(鸭子类型:apply 可调用 + 可选的 inject/name;不 import cordis / 宿主类型) */ type SubPlugin = { apply?: (c: unknown, config?: unknown) => unknown; inject?: readonly string[]; name?: string; }; /** * 宿主 Cordis 上下文的最小结构面(鸭子类型)。 * 🔴 刻意不 import cordis / 宿主 SDK 的类型——只用结构描述(适配层红线)。 */ interface HostCtx { /** 宿主惯用法:挂载一个插件,返回 ForkScope(含 dispose)或 disposer */ plugin?: (p: SubPlugin, config?: unknown) => unknown; /** 宿主生命周期事件(unload 钩子) */ on?: (event: string, listener: (...a: unknown[]) => unknown) => unknown; provide?: (name: string, service: unknown) => unknown; sofagent?: Record<string, unknown>; [key: string]: unknown; } /** * 跨模块形态取「插件对象」。 * * 🔴 为什么不能直接写 `mod.default?.apply`(实测结论,见 tools/gen 与本批汇报): * 6 个原子插件是 TS 编译出的 **CJS** 模块(`exports.default = kit.plugin` + `__esModule`), * 而 `await import(pkg)` 在 **CJS 里被 TS 保留为原生动态 import**(`module: node16`)。 * Node 对 CJS 的 ESM 互操作把 `default` 指向 `module.exports` 整体,于是 * · `mod.default` = `{ __esModule, default: kit.plugin, pluginMeta, … }` * · `mod.default.apply` = **undefined** ← 直呼只会静默空转(?. 兜住,不报错也不生效) * · `mod.default.default.apply` = kit.plugin.apply ← 真正的插件面 * 故这里逐层解一层 default;对「真 ESM(default 即插件)」形态同样成立。 */ function pluginOf(mod: unknown): SubPlugin { const layer1 = ((mod ?? {}) as { default?: unknown }).default ?? mod; const layer2 = ((layer1 ?? {}) as { default?: unknown }).default ?? layer1; return (layer2 ?? {}) as SubPlugin; } /** * DSH Cordis 插件契约:一次 apply 把 6 个原子插件逐个挂到同一个 ctx 上。 * * 🔴 挂载路径(A2 修复):**优先宿主惯用法 `ctx.plugin(插件)`**——由宿主按各原子插件自己声明的 * `inject` 做就绪门控,并把它挂成独立 fiber(卸载时宿主按 fiber 托管反注册其服务); * 宿主无 `ctx.plugin`(裸 ctx)或宿主拒收时才**回落直呼 `apply`**,且仅在 apply 真正可调用时执行。 * * 🔴 卸载路径:apply 返回一条幂等复合 disposer(宿主的 `Fiber._execute` 把「apply 返回函数」 * 登记为 effect disposer);宿主另有 `ctx.on` 时再登记一条 `dispose` 监听做双保险(幂等,重复触发只生效一次)。 * * 🔴 适配层红线(与 plugin-kit 同口径):`ctx: unknown` + 运行时鸭子类型, * 不 import cordis / 宿主 SDK 的**类型**;宿主 API 缺席时降级不抛。 * * @param ctx 宿主上下文(鸭子类型:plugin / on / provide 任一缺席都不崩) * @returns 幂等复合 disposer(卸载本层时反注册它经手的服务) */ export default { async apply(ctx: unknown): Promise<() => Promise<void>> { const c = (ctx ?? {}) as HostCtx; const loaded: string[] = []; const failed: Array<{ name: string; reason: string }> = []; const viaHost: string[] = []; const viaDirect: string[] = []; /** 卸载时逆序执行的 disposer(子插件句柄 / 宿主注册 API 返回值 / 本层命名空间条目) */ const disposers: Array<() => unknown> = []; /** 收集宿主返回的可卸载句柄:函数直接用;ForkScope 形态取 .dispose */ const collect = (ret: unknown): void => { if (typeof ret === 'function') { disposers.push(ret as () => unknown); return; } const handle = ret as { dispose?: unknown } | null | undefined; if (handle && typeof handle.dispose === 'function') { disposers.push(() => (handle.dispose as () => unknown).call(handle)); } }; const mountViaHost = typeof c.plugin === 'function'; for (const [key, pkg] of SUITE) { let sub: SubPlugin; try { const mod = await import(pkg); // 懒加载:缺哪个报哪个,不整挂失败 sub = pluginOf(mod); } catch (err) { // 逐个降级:单个原子插件缺席(未装 / 未 build)不阻断其余 5 个 failed.push({ name: key, reason: err instanceof Error ? err.message : String(err) }); continue; } // ① 宿主惯用法优先:拿到 inject 就绪门控 + 独立 fiber + 宿主托管卸载 let hostReject: string | null = null; if (mountViaHost) { try { collect(c.plugin?.(sub)); loaded.push(key); viaHost.push(key); continue; } catch (err) { hostReject = `宿主 ctx.plugin 拒收:${err instanceof Error ? err.message : String(err)}`; } } // ② 回落直呼 apply —— 仅当 apply **真正可调用**(apply 缺席时不得计入 loaded:那就是静默空转) if (typeof sub.apply !== 'function') { failed.push({ name: key, reason: hostReject ? `${hostReject};且插件无 apply(不可调用)` : '插件无 apply(不可调用)——未挂载', }); continue; } try { collect(await sub.apply(ctx)); loaded.push(key); viaDirect.push(key); } catch (err) { const reason = err instanceof Error ? err.message : String(err); failed.push({ name: key, reason: hostReject ? `${hostReject};回落直呼亦失败:${reason}` : reason }); } } // ③ 卸载路径:先建幂等 disposer(闭包按引用读 disposers,故此处可先于「本层服务注册」定义) let disposed = false; const dispose = async (): Promise<void> => { if (disposed) return; disposed = true; for (const undo of disposers.splice(0).reverse()) { try { await undo(); } catch (err) { console.error('[sofagent-suite] 卸载失败:', err instanceof Error ? err.message : String(err)); } } }; // ④ 宿主生命周期钩子(鸭子类型)——与 apply 返回值双保险 let unloadHook: SuiteReport['unloadHook'] = 'apply-return'; if (typeof c.on === 'function') { try { c.on('dispose', dispose); unloadHook = 'ctx.on'; } catch { // 为何可静默:apply 返回值仍在——宿主按 _execute 登记 disposer, // c.on 只是双保险;登记失败不等于卸载能力缺失,无降级告警可报。 } } // ⑤ 本层自己的报告服务(同样收 disposer,卸载时反注册) const report: SuiteReport = { loaded, failed, total: SUITE.length, capability, viaHost, viaDirect, unloadHook }; if (typeof c.provide === 'function') { collect(c.provide('sofagent.suite', report)); } else { c.sofagent = { ...(c.sofagent ?? {}), suite: report }; disposers.push(() => { const now = (c.sofagent ?? {}) as Record<string, unknown>; const next: Record<string, unknown> = { ...now }; delete next.suite; c.sofagent = next; }); } return dispose; }, };
-
-
cordis.patch.yml 632 B
# sofagent cordis-plugin-sofagent bundle patch v1.5.2——注册为 DSH profile layer - insert: - id: sofagent-suite name: 'cordis-plugin-sofagent' inject: [settings, dynamicCordisRunner] config: seam: "non-seam:plugin-suite" # 语义:非宿主事件接入(插件聚合)——一次 apply 逐个挂载 6 个原子插件;能力仍由各原子插件 provide,聚合层自身不挂任何生命周期 description: "一次挂载 sofagent 全套能力(6 项一次到位:注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)——一次性挂载 6 个原子插件" -
package.json 1.8 KB
{ "name": "cordis-plugin-sofagent", "version": "1.5.2", "engines": { "node": ">=18" }, "description": "一次挂载 sofagent 全套能力(6 项一次到位:注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)(seam: non-seam:plugin-suite)——一次性挂载 6 个原子插件", "license": "MIT", "author": "KongFangXun", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "test": "vitest run src/index.test.ts" }, "keywords": [ "sofagent", "dsh", "cordis-plugin" ], "sofagent": { "type": "dsh-plugin", "family": "cordis", "seam": "non-seam:plugin-suite", "seamSemantics": "非宿主事件接入(插件聚合)——一次 apply 逐个挂载 6 个原子插件;能力仍由各原子插件 provide,聚合层自身不挂任何生命周期" }, "//optionalDependencies": "v1.4.8 第8批:src/index.ts 逐个 await import('<兄弟插件 id>')(懒加载 + 逐个降级不抛——缺任一原子插件只记入 failed 数组,不整挂失败),聚合层自身零 @sofagent/* 依赖;对齐 root package.json F-18 optionalDependencies 先例。", "optionalDependencies": { "cordis-plugin-sofagent-inject": "1.5.2", "cordis-plugin-sofagent-audit": "1.5.2", "cordis-plugin-sofagent-evolve": "1.5.2", "cordis-plugin-sofagent-rollback": "1.5.2", "cordis-plugin-sofagent-daemon": "1.5.2", "cordis-plugin-sofagent-fde": "1.5.2" }, "devDependencies": { "typescript": "^7.0.2", "vitest": "^5.0.0" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, "files": [ "dist/", "cordis.patch.yml", "SKILL.md", "!dist/**/*.js.map", "!dist/**/*.d.ts.map", "!dist/**/*.test.*" ] } -
SKILL.md 2.3 KB
--- name: cordis-plugin-sofagent slug: cordis-plugin-sofagent version: 1.5.2 displayName: cordis-plugin-sofagent description: > 一次挂载 sofagent 全套能力——6 项能力一次到位(注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)(seam: non-seam:plugin-suite)——只编排不重实现——sofagent 约束层在 DSH(DeepSeek Harness)生态的插件形态。 --- # cordis-plugin-sofagent 一次挂载 sofagent 全套能力——6 项能力一次到位(注入 · 审计与验收 · 经验 · 回溯 · 巡检 · FDE 三域)(seam: non-seam:plugin-suite)——只编排不重实现,能力仍由各原子插件提供 ## 用途 一次 `apply` 把 6 个原子插件(inject / audit / evolve / rollback / daemon / fde)逐个挂到同一个 `ctx` 上,挂完把结果(`loaded` / `failed` / `total`)注册成 `sofagent.suite` 服务。适合「整套装上、不想逐个装」的场景。**什么时候用**:一次拿到全套约束层能力、不想逐个挑插件时。 三条硬约束: 1. **只编排,不重实现** —— 每个能力仍由原子插件 `provide`,本层只依次调用它们的 `apply`,不复制任何子插件逻辑; 2. **逐个降级,不整挂失败** —— 缺任一个原子插件只记入 `failed` 数组(含包名与原因),其余 5 个照常加载; 3. **不替代细粒度插件** —— 6 个原子插件全部保留,本插件是**新增的整装选项**,按需二选一。 本插件随 sofagent 主线版本发布,走**宿主挂载通道**:`skillhub install cordis-plugin-sofagent`。⚠️ **本插件不在 npm 通道**——11 款插件(7 DSH + 4 OpenClaw,v1.4.9 P2 合并批 14→11)均为 `private`,npm 装不到;npm 侧另有**库通道** `npm i -g sofagent`(总包 `engine/umbrella`,聚合 `@sofagent/audit` / `mcp` / `orchestrator` / `daemon` 四个能力包,**不含本插件**)。两条通道按所在环境二选一,互不依赖、互不为前置。版本号与 sofagent 主线对齐。 本插件是**宿主挂载入口**;**npm 安装入口见裸名总包**(`engine/umbrella`,包名 `sofagent`)—— 两个入口互不引用为前置,按使用场景各取其一。 ## 相关链接 - sofagent 主仓:https://github.com/KongFangXun/sofagent - 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族) -
tsconfig.json 185 B
{ "extends": "../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": [ "src" ], "exclude": [ "src/**/*.test.ts" ] }
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.