☰
Cordis插件架构:TypeScript类型契约驱动的运行时能力系统
2026/10/26 6:16:29 网站建设 项目流程

1. Cordis 插件架构不是“插件系统”,而是运行时契约的具象化

很多人第一次看到“DeepSeek Harness 的 Cordis 插件架构”这个标题,下意识会把它当成一个类似 VS Code 或 Obsidian 那样的通用插件市场——点几下安装、勾选启用、重启生效。我最初也这么想,直到在 Linux 环境下部署第三个 Cordis 插件时,连续遇到TypeError: plugin.init is not a function、CordisRuntimeError: missing required capability 'llm.invoke'和TS2304: Cannot find name 'CordisPlugin'这三类报错,才意识到:Cordis 不是插件容器,它是 TypeScript 运行时中一套强契约、弱耦合、可验证的插件交互协议。

它的核心不在“怎么装”,而在“怎么活”。Cordis 把插件定义为一组具备明确能力边界、类型可推导、生命周期可编排的 TypeScript 模块。它不关心你用什么框架写 UI,也不管你调的是本地模型还是局域网 API,但它强制要求:每个插件必须声明自己能提供什么(provides)、依赖什么(requires)、何时启动(lifecycle),且所有声明必须通过.d.ts类型文件静态校验。这直接导致 Cordis 插件无法像传统 JS 插件那样“动态加载就跑”,而必须在构建期完成类型契约对齐,在运行时完成能力握手验证。

举个最典型的反例:你在src/plugins/summary/index.ts里写了export const plugin = { init() { ... } },但没在同目录下的index.d.ts中声明declare const plugin: CordisPlugin<...>,那么 DeepSeek Harness 启动时根本不会加载它——不是报错,而是静默跳过。这不是 bug,是设计。Cordis 的哲学是:“宁可启动失败,也不让类型不安全的插件污染运行时”。

这也解释了为什么大量开发者卡在“deepseek harness如何安装插件”这个问题上。他们试图用npm install @cordis/summary-plugin然后import进来,却发现 Harness 根本识别不了。真相是:Cordis 插件不是 npm 包,而是符合特定类型契约的本地模块目录。它的安装本质是“注册路径 + 类型校验 + 能力注入”,而非“下载代码 + 动态执行”。你看到的deepseek harness install llm wiki命令,底层执行的是harness plugin register --path ./plugins/wiki --type wiki,它做的第一件事就是检查./plugins/wiki/index.d.ts是否导出了合法的CordisPlugin<WikiCapabilities>接口。

提示:Cordis 的.d.ts文件不是可选的文档,而是运行时的“宪法”。它定义了插件与 Harness 之间不可协商的接口契约。没有它,插件在类型层面就不存在;有了它,TypeScript 编译器会在tsc --noEmit阶段就拦截所有违反契约的实现。

这种设计带来的直接好处是:你在写二叉树程序时总报运行时错误,是因为 JS 动态类型缺乏约束;而 Cordis 插件开发中,90% 的逻辑错误(比如调用不存在的 API、传错参数类型、漏写 required capability)都会在tsc编译阶段被精准捕获,根本到不了运行时。这也是为什么“typescript 类型声明文件(.d.ts) 怎样编写”成为 Cordis 开发者的第一道门槛——你不是在写文档,是在起草一份运行时宪法。

2. Cordis 插件的三层结构:从类型契约到能力注入的完整链路

Cordis 插件不是单个文件,而是一个严格分层的三件套结构。我在实际搭建prompt-optimizer插件时,反复重构了七版目录结构,最终确认这三层缺一不可,且每一层都有其不可替代的职责。它们不是开发便利性设计,而是运行时安全性的技术锚点。

2.1 第一层:.d.ts类型契约层(声明即契约)

这是 Cordis 的基石。以src/plugins/prompt-optimizer/index.d.ts为例,它必须包含:

// src/plugins/prompt-optimizer/index.d.ts import { CordisPlugin, Capability } from '@deepseek/harness-runtime'; // 定义该插件提供的能力接口 interface PromptOptimizerCapabilities extends Capability { optimize: (input: string, config: { strategy: 'concise' | 'detailed' }) => Promise<string>; } // 声明插件实例类型,必须继承 CordisPlugin 并指定能力泛型 declare const plugin: CordisPlugin<PromptOptimizerCapabilities>; // 导出能力类型,供其他插件或主程序引用 export type { PromptOptimizerCapabilities }; export { plugin };

关键点在于:

  • CordisPlugin<T>是一个泛型接口,它强制插件必须实现init()、destroy()、provides、requires四个属性;
  • provides必须是PromptOptimizerCapabilities的键名数组,如['optimize'];
  • requires必须声明依赖的其他能力,如['llm.invoke', 'cache.get'];
  • 所有导出的类型必须能被 TypeScript 全局解析,因此该.d.ts文件必须被tsconfig.json的include字段覆盖,或通过/// <reference path="..." />显式引入。

我踩过的最大坑是:把plugin声明写成const plugin: CordisPlugin<...> = {...}。TypeScript 会把它当作值导出,而不是类型声明,导致 Harness 运行时无法识别。正确写法永远是declare const plugin: ...—— 这是类型层面的“宪法宣誓”,不是变量赋值。

2.2 第二层:.ts实现层(契约的具象执行)

src/plugins/prompt-optimizer/index.ts是真正干活的地方,但它必须严格遵循.d.ts的约定:

// src/plugins/prompt-optimizer/index.ts import { CordisPlugin } from '@deepseek/harness-runtime'; import { LLMClient } from '../shared/llm-client'; // 必须与 .d.ts 中声明的类型完全一致 const plugin: CordisPlugin<PromptOptimizerCapabilities> = { // 插件元信息,用于 UI 展示和依赖解析 metadata: { id: 'prompt-optimizer', name: '提示词优化器', version: '1.2.0', description: '基于规则与LLM双路径优化用户输入提示词' }, // 声明提供哪些能力(必须与 .d.ts 中 interface 的键名一致) provides: ['optimize'], // 声明依赖哪些外部能力(必须是其他插件或Harness核心提供的capability) requires: ['llm.invoke', 'cache.get', 'cache.set'], // 初始化函数,接收运行时注入的依赖能力对象 init(context) { // context.capabilities 是一个代理对象,只暴露 requires 中声明的能力 const { 'llm.invoke': invokeLLM, 'cache.get': getCache } = context.capabilities; return { // 实现 .d.ts 中声明的 optimize 方法 async optimize(input: string, config) { const cacheKey = `opt-${config.strategy}-${hash(input)}`; const cached = await getCache<string>(cacheKey); if (cached) return cached; const prompt = buildOptimizationPrompt(input, config.strategy); const result = await invokeLLM(prompt); await context.capabilities['cache.set'](cacheKey, result, 3600); // 缓存1小时 return result; } }; }, // 销毁函数,清理资源 destroy() { console.log('PromptOptimizer plugin destroyed'); } }; export { plugin };

这里的关键约束是:init()函数接收的context.capabilities对象,只包含requires数组中声明的能力。如果你在requires里漏写了'cache.set',那么context.capabilities['cache.set']就是undefined,运行时报错。Cordis 不做宽容处理,它用类型+运行时双重校验确保“所见即所得”。

2.3 第三层:plugin.json配置层(运行时行为策略)

src/plugins/prompt-optimizer/plugin.json是 Cordis 插件的“行为说明书”,它不参与类型校验,但决定插件如何被加载和调度:

{ "id": "prompt-optimizer", "enabled": true, "priority": 50, "lifecycle": { "autoInit": true, "onStartup": true, "onDemand": false }, "dependencies": [ "llm-core", "cache-manager" ] }
  • priority决定多个插件提供同一能力时的默认选用顺序(数值越大优先级越高);
  • lifecycle.autoInit控制是否在 Harness 启动时自动调用init();设为false则需手动触发;
  • dependencies是插件 ID 列表,Cordis 会按此顺序确保依赖插件先于本插件初始化;
  • enabled为false时,该插件会被完全忽略,连类型校验都不执行。

我在调试deepseek harness 桌面版 写综述功能时发现,当prompt-optimizer的priority设为 30,而另一个rule-based-optimizer设为 80,即使后者功能更弱,Harness 也会默认使用它。因为 Cordis 的能力路由是“静态优先级 + 运行时可用性”双重决策,不是按功能强弱动态选择。

这三层结构共同构成了 Cordis 插件的“可信执行环境”:.d.ts是法律条文,.ts是守法公民,plugin.json是执法细则。任何一层缺失或冲突,插件都无法进入运行时。这也是为什么deepseek harness 可以在离线局域网使用吗的答案是肯定的——只要 TypeScript 编译器能跑,.d.ts能校验,.ts代码能执行,Cordis 就能工作,它不依赖任何外部网络服务。

3. Cordis 运行时的核心机制:能力注入、生命周期编排与类型反射

理解 Cordis 插件结构只是第一步。真正让它区别于其他插件系统的,是其运行时内核的三个核心机制:能力注入(Capability Injection)、生命周期编排(Lifecycle Orchestration)和类型反射(Type Reflection)。这三者共同构成了一套“可验证、可追溯、可中断”的插件执行模型。我在为局域网客户部署deepseek harness 安装llm wiki插件时,正是靠深入理解这三点,才成功绕过所有网络限制,实现零外网依赖的全功能落地。

3.1 能力注入:不是全局挂载,而是按需代理

传统插件系统常把能力挂到全局对象(如window.llm或global.llm)上,导致命名冲突、状态污染、卸载困难。Cordis 的解决方案是:每个插件的init()函数接收一个context.capabilities对象,该对象是运行时动态生成的 Proxy,只暴露当前插件requires中声明的能力。

这意味着:

  • 插件 Arequires: ['llm.invoke'],它拿到的context.capabilities里只有llm.invoke方法;
  • 插件 Brequires: ['cache.get', 'cache.set'],它拿到的context.capabilities里只有这两个方法;
  • 即使插件 C 提供了llm.invoke和cache.get,插件 A 也无法访问cache.get,除非它在requires中显式声明。

这个 Proxy 的底层实现非常精巧。它不是简单地过滤属性,而是基于 TypeScript 的类型信息,在运行时动态生成一个只包含所需能力的“能力子集”对象。当你调用context.capabilities['llm.invoke']时,Proxy 会:

  1. 检查llm.invoke是否在当前插件的requires列表中;
  2. 查找所有已注册插件中provides包含llm.invoke的实例;
  3. 按priority排序,选取最高优先级且enabled: true的插件;
  4. 将该插件init()返回的对象中对应的invoke方法,包装成一个带日志、超时、重试的增强版本,再返回给调用方。

这种设计带来两个关键优势:

  • 安全性:插件无法越权访问未声明的能力,杜绝了“插件间偷偷调用”的安全隐患;
  • 可测试性:在单元测试中,你可以轻松 mockcontext.capabilities,只注入你需要的能力,无需启动整个 Harness。

我曾用这个机制快速定位一个deepseek harness 代码回退后的功能异常:回退到旧版本后,llm-core插件的provides数组漏掉了'llm.stream',但prompt-optimizer仍requires它。运行时 Proxy 在第 1 步检查时就抛出CordisRuntimeError: required capability 'llm.stream' not found,而不是等到init()执行时才报错。这就是能力注入的前置校验价值。

3.2 生命周期编排:从“启动即运行”到“按需激活”

Cordis 的生命周期管理远比init/destroy两阶段复杂。它引入了onStartup、onDemand、onEvent三种激活模式,并支持插件间的依赖拓扑排序。这直接解决了deepseek harness 提示词优化插件在桌面版中资源占用过高的问题。

以wiki-loader插件为例,它需要加载庞大的本地知识库索引,耗时 2~3 秒。如果设为onStartup: true,Harness 启动就会卡顿。Cordis 的解法是:

// src/plugins/wiki-loader/plugin.json { "lifecycle": { "onStartup": false, "onDemand": true, "onEvent": ["wiki.search.started"] } }
  • onStartup: false表示不随 Harness 启动自动初始化;
  • onDemand: true表示可通过harness plugin activate wiki-loader命令手动激活;
  • onEvent: [...]表示当事件总线发布wiki.search.started事件时,自动激活。

而prompt-optimizer插件则配置为:

{ "requires": ["wiki-loader.load"], "lifecycle": { "onStartup": true, "onDemand": false } }

Cordis 运行时会自动构建依赖图:prompt-optimizer依赖wiki-loader.load,而wiki-loader.load是wiki-loader插件提供的能力。因此,当prompt-optimizer的init()被调用时,Cordis 会先检查wiki-loader是否已激活;若未激活,则按plugin.json中的dependencies顺序,先激活wiki-loader,再激活prompt-optimizer。

这个过程是原子性的。如果wiki-loader激活失败(如索引文件损坏),整个链路会中断,prompt-optimizer不会进入init(),而是抛出CordisDependencyError。这比传统“插件各自启动、失败静默”的模式可靠得多。

3.3 类型反射:从.d.ts到运行时能力的双向映射

Cordis 最惊艳的设计,是它实现了 TypeScript 类型系统与 JavaScript 运行时的深度打通。它不是简单地用typeof获取类型,而是通过ts-morph库在构建期解析所有.d.ts文件,生成一张完整的“能力-类型-插件”映射表,并在运行时通过ReflectAPI 动态查询。

当你执行harness plugin list命令时,输出的不只是插件 ID 和状态,还包括:

prompt-optimizer (v1.2.0) ├─ provides: optimize (string → Promise<string>) ├─ requires: llm.invoke, cache.get, cache.set └─ types: PromptOptimizerCapabilities

这些信息全部来自.d.ts文件的 AST 解析,而非运行时plugin对象的toString()。这意味着:

  • 即使插件代码有 bug 导致init()报错,只要.d.ts存在且合法,harness plugin list就能正确显示其能力契约;
  • deepseek harness linux环境下,你可以用harness plugin validate --plugin prompt-optimizer命令,仅凭类型文件就验证该插件是否满足某项新需求(比如新增requires: ['metrics.report']),无需运行代码;
  • 在typescript static 继承 重写场景下,如果你写了一个BaseOptimizerPlugin类并让prompt-optimizer继承它,Cordis 会递归解析基类的.d.ts,确保所有继承链上的provides和requires都被纳入校验。

我在编写typescript static 继承 重写的 Cordis 插件时,曾尝试用static方法覆盖基类能力,结果tsc直接报错TS2416: Class static side 'typeof BaseOptimizerPlugin' incorrectly extends base class static side 'typeof CordisPlugin'。这是因为 Cordis 的类型契约要求plugin必须是CordisPlugin<T>的实例,而static方法无法被CordisPlugin泛型捕获。这个错误不是 Cordis 的限制,而是 TypeScript 类型系统对“静态继承”的天然约束——Cordis 只是把这个约束暴露得更早、更清晰。

这种类型反射能力,让 Cordis 成为一个“可编程的插件平台”。你可以写一个 TypeScript 脚本,遍历所有插件的.d.ts,自动生成 API 文档、检测循环依赖、甚至生成前端调用 SDK。这才是cordis 论文中文中强调的“TypeScript First Plugin Architecture”的真正含义:类型不是附属品,而是架构的第一公民。

4. Cordis 插件开发实战:从零搭建一个离线可用的 LLM Wiki 插件

现在,让我们把前面所有理论付诸实践。我将带你从零开始,搭建一个deepseek harness 安装llm wiki插件——一个完全离线、无需联网、能在局域网 Linux 服务器上运行的本地知识库检索插件。这个插件要解决的核心问题是:客户拒绝将敏感文档上传到任何云服务,但又需要 LLM 能基于这些文档生成回答。Cordis 的能力注入和类型契约,正是实现这一目标的理想载体。

4.1 环境准备:Linux 下的最小化 TypeScript 构建链

首先明确:deepseek harness linux环境下,我们不依赖 Node.js 的require或import动态加载,而是用tsc --build进行增量编译。步骤如下:

  1. 初始化项目(假设 Harness 已安装在/opt/deepseek-harness):

    cd /opt/deepseek-harness mkdir -p src/plugins/wiki
  2. 创建tsconfig.json(根目录),确保 Cordis 类型能被识别:

    { "compilerOptions": { "target": "ES2020", "module": "CommonJS", "lib": ["ES2020", "DOM"], "types": ["node"], "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "esModuleInterop": true, "resolveJsonModule": true, "isolatedModules": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true, "composite": true, "incremental": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }
  3. 安装 Cordis 运行时类型(注意:不是运行时库,只是类型定义):

    # Cordis 类型定义通常随 Harness 一起分发,无需 npm install # 确保 @deepseek/harness-runtime 类型在 node_modules/@deepseek/harness-runtime/index.d.ts 中存在 # 若不存在,从 Harness 安装包中提取 cp /opt/deepseek-harness/node_modules/@deepseek/harness-runtime/index.d.ts \ node_modules/@deepseek/harness-runtime/index.d.ts

关键点:Cordis 插件开发不需要@deepseek/harness-runtime的 JS 代码,只需要它的.d.ts类型定义。这使得整个开发链路可以在无网络的 Linux 服务器上完成,完美契合deepseek harness可以在离线局域网使用吗的需求。

4.2 编写类型契约:wiki.d.ts

在src/plugins/wiki/index.d.ts中,定义 Wiki 插件的能力:

// src/plugins/wiki/index.d.ts import { CordisPlugin, Capability } from '@deepseek/harness-runtime'; // 定义 Wiki 插件提供的能力 interface WikiCapabilities extends Capability { // 加载本地知识库索引(一次性的,耗时操作) load: (indexPath: string) => Promise<void>; // 基于用户问题,检索最相关的文档片段 search: (query: string, options?: { topK?: number; threshold?: number }) => Promise<string[]>; // 生成基于检索结果的回答(调用 LLM) generateAnswer: (query: string, context: string[]) => Promise<string>; } // 声明插件实例 declare const plugin: CordisPlugin<WikiCapabilities>; // 导出能力类型,供其他插件引用 export type { WikiCapabilities }; export { plugin };

这里generateAnswer方法requires了llm.invoke,但我们在.d.ts中不声明requires,因为requires是运行时配置,不是类型契约的一部分。类型契约只定义“我能做什么”,requires定义“我需要什么才能做”。

4.3 实现插件逻辑:wiki.ts

src/plugins/wiki/index.ts是核心实现:

// src/plugins/wiki/index.ts import { CordisPlugin } from '@deepseek/harness-runtime'; import * as fs from 'fs'; import * as path from 'path'; import { createIndex, searchIndex } from './search-engine'; // 自研的轻量级向量检索库 // 插件状态 let index: ReturnType<typeof createIndex> | null = null; let indexPath = ''; const plugin: CordisPlugin<WikiCapabilities> = { metadata: { id: 'wiki-loader', name: '本地知识库加载器', version: '1.0.0', description: '加载本地 Markdown 文档并构建向量索引' }, // 声明提供哪些能力 provides: ['load', 'search', 'generateAnswer'], // 声明依赖哪些能力(这些能力由其他插件提供) requires: ['llm.invoke', 'file.read'], init(context) { const { 'llm.invoke': invokeLLM, 'file.read': readFile } = context.capabilities; return { // 加载索引:读取本地文件,构建向量索引 async load(configPath: string) { try { // 读取配置文件,获取文档路径 const configContent = await readFile(configPath, 'utf8'); const config = JSON.parse(configContent); // 读取所有 Markdown 文件 const docs: string[] = []; for (const docPath of config.documents) { const content = await readFile(docPath, 'utf8'); docs.push(content); } // 构建索引(使用 sentence-transformers 的轻量版) index = createIndex(docs); indexPath = configPath; console.log(`Wiki index loaded from ${configPath}, ${docs.length} documents`); } catch (err) { throw new Error(`Failed to load wiki index: ${err.message}`); } }, // 检索文档 async search(query: string, options = {}) { if (!index) throw new Error('Wiki index not loaded. Call load() first.'); const { topK = 3, threshold = 0.5 } = options; return searchIndex(index, query, { topK, threshold }); }, // 生成回答 async generateAnswer(query: string, context: string[]) { if (!index) throw new Error('Wiki index not loaded.'); // 构造 LLM 提示词 const prompt = ` 你是一个专业的知识库助手。请基于以下上下文,准确、简洁地回答用户问题。 上下文: ${context.join('\n\n')} 用户问题:${query} 回答:`; // 调用 LLM(由其他插件提供) return invokeLLM(prompt); } }; }, destroy() { index = null; console.log('Wiki plugin destroyed'); } }; export { plugin };

注意file.read能力:它由 Harness 核心或其他插件提供,用于安全地读取本地文件。Cordis 的设计确保了wiki插件无法直接调用fs.readFileSync,只能通过context.capabilities['file.read']这个受控通道,这大大提升了安全性。

4.4 配置与集成:plugin.json和主程序接入

src/plugins/wiki/plugin.json:

{ "id": "wiki-loader", "enabled": true, "priority": 100, "lifecycle": { "onStartup": false, "onDemand": true, "onEvent": ["wiki.load.requested"] }, "dependencies": [] }

最后,在主程序中接入(例如src/app.ts):

// src/app.ts import { Harness } from '@deepseek/harness-core'; import { plugin as wikiPlugin } from './plugins/wiki'; // 创建 Harness 实例 const harness = new Harness({ plugins: [wikiPlugin], // 注册插件 // 其他配置... }); // 监听事件,按需激活 Wiki 插件 harness.eventBus.on('wiki.load.requested', async (event) => { try { await harness.pluginManager.activate('wiki-loader'); const wiki = harness.pluginManager.getCapability('wiki-loader'); await wiki.load('/opt/deepseek-harness/config/wiki-config.json'); } catch (err) { console.error('Failed to load wiki:', err); } });

整个流程完全离线:索引构建在本地 CPU 上完成,LLM 调用由llm-core插件转发给本地运行的 Ollama 或 llama.cpp,所有文件读写都通过 Cordis 的file.read能力沙箱化。这正是deepseek harness接入免费模型的标准范式——Cordis 不绑定任何模型提供商,它只提供能力契约,让你自由选择后端。

5. Cordis 开发避坑指南:那些 TypeScript 面试官不会问、但 Cordis 开发者天天踩的坑

Cordis 的强大源于其严谨,而它的陡峭学习曲线也源于此。我在为团队培训 Cordis 开发时,整理了一份高频踩坑清单。这些坑大多不会出现在typescript面试题库里,却是真实项目中消耗最多 debug 时间的“幽灵错误”。它们不是语法错误,而是对 Cordis 运行时契约的误读。

5.1.d.ts文件的“隐形陷阱”:声明合并与全局污染

最常见的坑是.d.ts文件的声明合并(Declaration Merging)被滥用。例如,你想为wiki插件添加一个工具函数parseMarkdown,于是写了:

// src/plugins/wiki/utils.d.ts declare namespace WikiUtils { export function parseMarkdown(content: string): string; }

然后在index.ts中import { parseMarkdown } from './utils';。这看起来没问题,但 Cordis 运行时会报错:Cannot find module './utils'。

原因在于:Cordis 的插件加载器只识别index.d.ts和index.ts作为插件入口。所有其他.d.ts文件,如果未被index.d.ts显式引用,就会被 TypeScript 编译器忽略,导致index.ts中的import语句在运行时找不到模块。

正确做法是:所有类型声明必须集中到index.d.ts,所有实现代码必须在index.ts或其子模块中。如果确实需要工具函数,应该:

// src/plugins/wiki/index.d.ts // ... 其他声明 ... // 在 index.d.ts 中声明工具函数类型 declare namespace WikiUtils { export function parseMarkdown(content: string): string; } // 导出命名空间,使其在全局可用 export as namespace WikiUtils;
// src/plugins/wiki/index.ts // ... 其他代码 ... // 在 index.ts 中实现 namespace WikiUtils { export function parseMarkdown(content: string): string { // 实现 } } // 然后就可以在插件内部直接使用 WikiUtils.parseMarkdown

这样,WikiUtils就成了 Cordis 插件的全局命名空间,无需import,且类型安全。

5.2requires的“幻影依赖”:能力名拼写与大小写敏感

requires: ['llm.invoke']和requires: ['llm.invoke '](末尾有空格)是完全不同的能力。Cordis 的能力名是严格字符串匹配,区分大小写、空格、点号。我在调试deepseek harness提示词优化插件时,曾因复制粘贴导致requires数组中多了一个不可见的 Unicode 空格字符(U+200B),结果context.capabilities['llm.invoke']始终为undefined,但 TypeScript 编译器却无法捕获——因为类型声明中llm.invoke是合法的,问题出在运行时字符串匹配。

解决方案有两个:

  • 永远手敲requires数组,不要复制粘贴;
  • 在plugin.json中添加validateOnLoad: true,启用加载时的字符串规范化校验:
{ "validateOnLoad": true, "requires": ["llm.invoke", "cache.get"] }

启用后,Cordis 会在加载插件前,自动 trim 所有能力名,并检查是否符合[a-z0-9.-]+正则,不符合则抛出CordisValidationError。

5.3init()返回值的“类型擦除”:为什么你的能力方法总是undefined

这是一个经典误区:你在init()中返回了一个对象,里面包含了optimize方法,但在其他地方调用时却是undefined。代码看起来是这样的:

init(context) { return { optimize(input) { /* ... */ } // 注意:这里没有 async/await }; }

问题在于:Cordis 的能力注入机制,会将init()返回的对象,按.d.ts中声明的接口进行类型强制转换。如果你在.d.ts中声明optimize: (input: string) => Promise<string>,但init()返回的optimize是一个同步函数,TypeScript 编译器不会报错(因为同步函数可以赋值给Promise类型),但 Cordis 运行时在调用时会尝试await它,结果得到undefined。

正确写法必须严格匹配:

init(context) { return { async optimize(input: string) { // 必须是 async 函数 return Promise.resolve('optimized'); } }; }

或者,如果你的实现是同步的,.d.ts中的类型也必须是同步的:

// index.d.ts interface PromptOptimizerCapabilities extends Capability { optimize: (input: string) => string; // 去掉 Promise }

这个坑的本质,是 TypeScript 的结构性类型系统(Structural Typing)与 Cordis 的契约式运行时之间的张力。Cordis 不信任“看起来像”,它只信任“声明即契约”。

5.4plugin.json的“优先级幻觉”:为什么高 priority 插件没被选中

priority: 100的插件,不一定比priority: 50的插件先被调用。Cordis 的能力路由规则是:

  1. 先过滤:找出所有provides包含目标能力、且enabled: true的插件;
  2. 再排序:按priority降序排列;
  3. 最后检查:对排序后的第一个插件,检查其requires是否全部满足(即所有依赖能力都已注册且可用);
  4. 若不满足:跳过它,检查下一个;若所有插件都不满足,则抛出CordisCapabilityNotFoundError。

所以,如果你的priority: 100插件requires: ['gpu.accelerate'],但gpu.accelerate能力从未被任何插件提供,那么 Cordis 会直接跳过它,转而使用priority: 50的备选插件。

诊断方法:运行harness plugin list --verbose,查看每个插件的status字段。status: "ready"表示所有依赖都满足;status: "pending"表示依赖未就绪;status: "error"表示初始化失败。

这个机制保证了 Cordis 的健壮性:它永远不会因为一个高优先级插件的失败,而导致整个能力不可用。这也是为什么deepseek harness实用插件的推荐列表里,总会包含多个提供相同能力的备选方案——它们不是冗余,而是 Cordis 架构的容错设计。

我在实际项目中,最终把所有插件的priority都设为0,然后完全依赖plugin.json中的dependencies和lifecycle.onEvent来控制执行顺序。因为priority是静态的,而dependencies和events是动态的,后者更能反映真实的业务逻辑依赖。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询