插件配置
本章目标
- 用「
Config类型 + 同名 Schemastery schema」声明插件配置,理解默认值归属 - 读懂
cordis.yml行结构(id/name/config)与覆盖规则 - 理解
Requires(inject)语义:为什么部署必须同时加载服务提供者 - 了解 loader 机制(schema 校验、默认值填充、
!!js插值) - 区分
settings.yaml与凭据:机密不进cordis.yml,redactSecrets保护密钥 - 完成带必填/可选字段的配置示例
声明 Config:类型 + schema
Cordis 插件通过两个同名导出声明配置:Config 接口(类型)与同名 Schemastery schema(运行时校验器)。默认值直接写在 schema 中;插件加载时 Cordis 用该 schema 校验并填充默认值,apply 收到的 config 已校验且类型安全:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
/** 配置类型:apply 收到的 config 就是它 */
export interface Config {
/** 问候语模板,{name} 会被替换为被问候者的名字 */
greeting: string
/** 是否把问候语转成大写 */
uppercase: boolean
}
/** 与类型同名的 Schemastery schema:默认值写在这里 */
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
uppercase: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
let message = `${config.greeting}, ${args.name}!`
if (config.uppercase) message = message.toUpperCase()
return message
},
}))
}注意:不要导出普通对象作为 Config——它不满足 Cordis 要求的 Standard Schema 接口。
cordis.yml 的行结构
每个插件行由三部分组成:
- insert:
- id: greet
name: './src/my-plugin.ts'
config:
greeting: 'Hi there'
uppercase: trueid:部署内唯一的插件实例 id,也是 patch 覆盖的定位键;name:插件入口。本地开发用相对源码路径,发布后写包名(如'@deepseek-ai/dsh-tool-fs');config:传给apply的配置对象,逐字段经 schema 校验。
patch 覆盖规则:后应用的行按 id 覆盖先前的行,且是整行替换(非深度合并)——覆盖时必须重述该行需要的每个键。
配置如何被读取
Schema 在插件加载时执行校验:配置不合法则加载失败并给出明确错误;通过后未提供的字段取默认值。修改 cordis.yml 中某插件的 config 会触发热替换(HMR):卸载旧实例、加载新实例;注册都是 effect,卸载时自动清理,不留旧注册。
Harness 的硬约定——无硬编码可调参数:凡不同部署取值可能不同的参数,必须定义为配置字段。检验标准:能否在 cordis.yml 中改值而不改代码?
Requires 语义:inject 服务键
inject: ['tools'] 声明插件依赖 tools 服务键。它既是时序约束(注册表就绪后再 apply),也是部署约束:配置树必须同时加载这些服务的提供者(官方配置目录的 Requires: 行正是这些键)。缺少提供者,插件就无法激活。
loader 的 !!js 插值
除静态值外,loader 允许配置项写 !!js 表达式:它挂载一次组合,等待每行普通注入,再基于该行已注入的上下文求值配置。两种常见用途:
- id: my-app
name: '@example/my-app'
inject: [myAppStartup]
config:
port: !!js ctx.myAppStartup.port ?? 8080 # 引用已注入的服务
cwd: !!js process.env.PROJECT_DIR ?? process.cwd() # 引用环境变量这让同一份 patch 在不同环境落地不同取值,而无需维护多份配置文件。
settings.yaml 与凭据:机密不进 cordis.yml
harness 还有两个与配置相关的文件:
settings.yaml(位于 $DSH_HOME):用户可编辑的设置文档,按 namespace 分节。插件经 ctx.settings 注册 namespace,解析顺序为 schema 默认值 → 组合 base → 用户分节。组合配置仍留在 cordis.yml——namespace 只承载用户可编辑的子集。
.credentials.yaml($DSH_HOME/.credentials.yaml):凭据的真正存放处。cordis.yml 与 settings 里只出现凭据引用——一个环境变量名(CredentialRef),值归凭据提供方所有,消费方每次操作解析一次。轮换密钥只需更新存储,无需重启即可作用于下一次请求;空存储值在任何地方都视为不存在。例如:
- id: llm
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKeyEnv: MY_GATEWAY_KEY # 只写环境变量名(CredentialRef),不写密钥本身配置界面的兜底是 redactSecrets:describe({ redactSecrets: true }) 会把 role('secret') 字段从返回的 value / base / user 中剥离,只枚举 {path, set} 槽位——页面因此能渲染只写输入框而永远收不到明文密钥;任何对外传输接口都必须传入该选项。
完整示例:必填 + 可选字段
需要严格校验时,让无效配置在加载时失败:
export interface Config {
apiKey: string
timeout: number
mode: 'fast' | 'accurate'
}
export const Config = Schema.object({
apiKey: Schema.string().required(), // 必填:缺失直接加载失败
timeout: Schema.number().default(30000), // 可选:默认值写在 schema
mode: Schema.union(['fast', 'accurate']).default('fast'),
})
export function apply(ctx: Context, config: Config) {
// config 已校验且类型安全
}对应的 cordis.yml 行:
- insert:
- id: validated
name: './src/validated-plugin.ts'
config:
apiKey: !!js process.env.MY_API_KEY
mode: accurate # timeout 省略,取默认 30000小结
- 配置 = 同名
Config类型 + Schemastery schema,默认值写在 schema,非法配置加载即失败; cordis.yml行由id/name/config组成,覆盖是整行替换;inject即Requires:部署必须加载对应服务提供者;!!js让配置项引用环境变量与已注入服务;- 密钥只以
CredentialRef(环境变量名)出现,值在.credentials.yaml;redactSecrets保证界面只见槽位、不见明文。
下一步
- 打包与安装插件:把本地插件发布为可安装的组合包(bundle)与 profile;
- 官方插件配置目录(config-catalog):查看每个包的完整配置字段与默认值;
- 能力分层:把可替换能力拆分为 Service Definition / Service Provider / Consumer。