DeepSeek Harness: 声明依赖:inject
最后更新:2026-08-31
插件不是孤岛——大多数插件需要依赖其他插件提供的服务。inject 数组是 Cordis 的依赖声明机制,它确保依赖先于使用者加载,杜绝"服务不存在"的运行时错误。
📋 前置知识:已完成 11-first-plugin.md,理解 apply 和 Context
1. 你将学到
inject数组声明依赖- 内置服务列表:tools、llm、sessions、fs、shell 等
- 依赖加载顺序保证
- 可选依赖
inject: ['tools', 'llm?'] - 循环依赖检测
- 依赖注入的底层机制
2. inject 数组声明依赖
(1) ▶ 示例 1
import { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool'
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools 和 ctx.llm 在 apply 调用时保证已就绪
ctx.logger.info('tools ready:', !!ctx.tools)
ctx.logger.info('llm ready:', !!ctx.llm)
}
inject 数组列出插件需要的所有服务名。框架确保这些服务在 apply 被调用前已经注册完成。
(2) ▶ 示例 2
export default {
name: 'my-tool',
inject: ['tools', 'llm'],
apply(ctx: Context) {
// ...
}
}
(3) ▶ 示例 3
export default class MyPlugin {
static name = 'my-tool'
static inject = ['tools', 'llm']
constructor(private ctx: Context) {
// ...
}
}
(4) 不声明 inject 的后果
// ❌ 不声明 inject,直接使用服务
export function apply(ctx: Context) {
ctx.tools.register(...) // 运行时错误:ctx.tools 可能不存在
}
不声明 inject 时,插件可能在依赖服务注册之前就被加载,导致 ctx.tools 为 undefined。
3. 内置服务列表
(1) 核心服务
DSH 通过内置插件提供以下服务:
| 服务名 | 提供者 | 功能 |
|---|---|---|
tools |
dsh-core | 工具注册与执行 |
llm |
dsh-plugin-llm | LLM 适配器 |
sessions |
dsh-core | 会话管理 |
fs |
dsh-plugin-fs | 文件系统操作 |
shell |
dsh-plugin-shell | Shell 命令执行 |
sandbox |
dsh-plugin-sandbox | 沙箱环境 |
search |
dsh-plugin-search | 代码搜索 |
trajectory |
dsh-core | 日志记录 |
(2) 服务访问方式
声明 inject 后,通过 ctx.服务名 访问:
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools — 工具服务
ctx.tools.register({
name: 'my_tool',
// ...
})
// ctx.llm — LLM 服务
const response = await ctx.llm.complete({
messages: [{ role: 'user', content: 'hello' }]
})
}
(3) 服务类型推断
TypeScript 会根据 inject 自动推断 ctx 上的服务类型:
// inject = ['tools'] → ctx.tools: ToolsService
// inject = ['llm'] → ctx.llm: LLMService
// inject = ['tools', 'llm'] → ctx.tools + ctx.llm 都有类型
4. 依赖加载顺序保证
(1) 拓扑排序
Cordis 根据所有插件的 inject 声明构建依赖图,然后按拓扑排序加载:
graph LR
A[plugin-a<br/>inject: []] --> B[plugin-b<br/>inject: ['a']]
B --> C[plugin-c<br/>inject: ['a', 'b']]
加载顺序:A → B → C
(2) 自动排序
你不需要手动控制加载顺序。即使在 cordis.yml 中 C 写在 A 前面:
plugins:
plugin-c: ...
plugin-a: ...
plugin-b: ...
框架仍然按 A → B → C 的顺序加载。
(3) 并行加载
没有依赖关系的插件可以并行加载:
graph TB
A[plugin-a] --> C[plugin-c<br/>inject: a, b]
B[plugin-b] --> C
A 和 B 可以同时加载,都完成后才加载 C。
(4) 加载阶段示意
Phase 1: 加载无依赖插件 → [core, logger]
Phase 2: 加载依赖 Phase 1 的 → [tools, sessions]
Phase 3: 加载依赖 Phase 2 的 → [my-plugin, other-plugin]
...
5. 可选依赖
(1) 语法
在依赖名后加 ? 表示可选:
export const inject = ['tools', 'llm?']
含义:tools 是必需依赖(缺失则加载失败),llm 是可选依赖(缺失时正常加载)。
(2) 可选依赖的访问
export const inject = ['tools', 'llm?']
export function apply(ctx: Context) {
// tools 一定存在
ctx.tools.register(...)
// llm 可能不存在
if (ctx.llm) {
ctx.llm.complete(...)
} else {
ctx.logger.warn('llm not available, skipping LLM features')
}
}
(3) 可选依赖的使用场景
| 场景 | 必需/可选 | 原因 |
|---|---|---|
| 工具注册必须用 tools | 必需 | 核心功能 |
| LLM 能力增强 | 可选 | 没有也能工作 |
| 沙箱功能 | 可选 | 不是所有环境都有沙箱 |
| 日志服务 | 必需 | 基础设施 |
(4) 运行时检测
export const inject = ['tools', 'search?']
export function apply(ctx: Context) {
ctx.tools.register({
name: 'smart_search',
async execute(params) {
if (ctx.search) {
return ctx.search.query(params.query)
}
return 'search service not available'
}
})
}
6. 循环依赖检测
(1) 什么是循环依赖
A 依赖 B,B 又依赖 A:
A inject: ['B']
B inject: ['A']
这形成死锁:A 等 B 加载,B 等 A 加载,谁也加载不了。
(2) Cordis 的检测机制
框架在启动时检查依赖图,发现循环后立即报错:
Error: Circular dependency detected:
plugin-a → plugin-b → plugin-a
Please review your inject declarations.
(3) 解决循环依赖
方案 1:提取公共依赖
Before: A → B → A
After: A → C, B → C
将 A 和 B 共同需要的逻辑提取到 C 中。
方案 2:使用事件解耦
// A 不直接依赖 B,而是监听事件
export const inject = []
export function apply(ctx: Context) {
ctx.on('b/ready', (bService) => {
// A 使用 B 的能力,但不声明依赖
})
}
方案 3:使用可选依赖
// A 可选依赖 B
export const inject = ['B?']
export function apply(ctx: Context) {
if (ctx.B) {
// 使用 B
}
}
(4) 三节点循环
A → B → C → A
Cordis 同样能检测多节点循环。报错信息会显示完整链路。
7. 依赖注入的底层机制
(1) 服务注册与发现
// Provider 插件注册服务
ctx.provide('tools', toolsInstance)
// Consumer 插件发现服务
const tools = ctx.get('tools')
(2) inject 与 apply 的时序
sequenceDiagram
participant F as Framework
participant P as Provider Plugin
participant C as Consumer Plugin
F->>P: 加载 Provider
P->>F: apply() → 注册 'tools' 服务
F->>C: 检查 inject ['tools'] ✅ 已就绪
F->>C: 调用 apply()
C->>F: ctx.tools 可用
(3) 依赖未就绪时
sequenceDiagram
participant F as Framework
participant C as Consumer Plugin
F->>F: 检查 inject ['tools'] ❌ 未就绪
F->>C: 插件进入 pending 状态
Note over F: 等待 tools 服务注册
F->>F: tools 服务注册完成
F->>C: 重新检查 ✅ → 调用 apply()
插件不会因依赖缺失而被丢弃,而是进入 pending 状态,等依赖就绪后自动激活。
(4) 类型安全的依赖注入
// 框架内部类型映射
interface Context {
tools: ToolsService // 当 inject 包含 'tools'
llm: LLMService // 当 inject 包含 'llm'
sessions: SessionService // 当 inject 包含 'sessions'
// ...
}
TypeScript 的条件类型机制根据 inject 数组自动扩展 ctx 的类型定义,确保编译期类型安全。
❓ 常见问题
bash pnpm dsh web --patch --dump-config # 或在运行时 ctx.logger.info(Object.keys(ctx.services)) import 是 TypeScript 的静态模块引用,编译期确定。inject 是运行时服务依赖,由 Cordis 框架在插件加载时解析。两者互补:import 引入类型和工具函数,inject 声明运行时服务依赖。 ---📖 小节
inject数组声明插件的运行时依赖,框架保证依赖先于 apply 加载- 内置服务:tools、llm、sessions、fs、shell、sandbox、search、trajectory
- 依赖名后加
?表示可选依赖,缺失时插件仍正常加载 - Cordis 自动检测循环依赖并报错,通过提取公共依赖/事件解耦/可选依赖解决
- 依赖缺失时插件进入 pending 状态,依赖就绪后自动激活
- inject 是声明式依赖,永远不要手动控制加载顺序
📝 作业
1. ⭐ 基础题:编写一个插件,声明 inject: ['tools'],在 apply 中用 ctx.tools.register 注册一个简单工具。启动后验证工具可用。
2. ⭐⭐ 进阶题:编写一个插件,声明 inject: ['tools', 'llm?']。当 llm 可用时,工具调用 LLM 增强;不可用时,工具返回降级结果。分别测试两种场景。
3. ⭐⭐⭐ 挑战题:故意创建两个互相依赖的插件 A(inject: ['B'])和 B(inject: ['A']),观察 Cordis 的循环依赖报错信息。然后用事件解耦的方式重写,消除循环依赖,验证两个插件都能正常加载。