feat(docs): build maintainable documentation site
This commit is contained in:
@@ -15,6 +15,7 @@ Every fact has exactly one home — the tier whose job it is — and every other
|
||||
| [rfc/](rfc/README.md) | Decision records: the why and the what-was-given-up; `implemented/` RFCs describe shipped reality in present tense | Migration plans, test checklists, and spec-speak ("should…") once the decision has shipped |
|
||||
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
|
||||
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the RFC each guide links) |
|
||||
| [user/](user/zh-CN/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ RFCs), gate-by-gate enumerations that drift from `package.json` scripts |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
|
||||
@@ -160,6 +160,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
|
||||
| [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 |
|
||||
| [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 |
|
||||
| [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 |
|
||||
| [Project canonical documentation into the website](implemented/process/2026-07-13-documentation-site-projection.md) | 2026-07-13 |
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# RFC: Project canonical documentation into the website
|
||||
|
||||
Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
|
||||
|
||||
## Decision
|
||||
|
||||
Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
|
||||
|
||||
`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
|
||||
|
||||
`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
|
||||
|
||||
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
|
||||
|
||||
Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
|
||||
|
||||
Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
|
||||
|
||||
**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
|
||||
|
||||
**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
|
||||
|
||||
**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
|
||||
|
||||
**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
|
||||
|
||||
## Consequences
|
||||
|
||||
Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
|
||||
|
||||
The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.
|
||||
108
docs/user/zh-CN/develop/basic/config.md
Normal file
108
docs/user/zh-CN/develop/basic/config.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 插件配置
|
||||
|
||||
让你的插件接受用户在 `cordis.yml` 中传入的配置。
|
||||
|
||||
## 定义 Config 类型
|
||||
|
||||
在插件中导出一个 `Config` 类型和可选的默认值:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config = {
|
||||
greeting: 'Hello',
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting) // 用户配置或默认值
|
||||
}
|
||||
```
|
||||
|
||||
用户在 `cordis.yml` 中这样使用:
|
||||
|
||||
```yaml
|
||||
- name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
未提供的字段使用导出的 `Config` 对象中的默认值。
|
||||
|
||||
## Schema 校验
|
||||
|
||||
对于需要严格校验的场景,使用 Schemastery 定义 schema:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config 已经过校验,类型安全
|
||||
}
|
||||
```
|
||||
|
||||
Schema 在插件加载时执行校验。如果配置不合法,插件会加载失败并给出明确错误信息。
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 无硬编码可调参数
|
||||
|
||||
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
|
||||
|
||||
```typescript
|
||||
// 错误 — 硬编码超时时间
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// 正确 — 可配置
|
||||
export interface Config {
|
||||
timeoutMs: number // 默认 30000
|
||||
}
|
||||
```
|
||||
|
||||
检验标准:能否在 `cordis.yml` 中改变这个值,而不需要修改代码?
|
||||
|
||||
### 配置错误要响亮
|
||||
|
||||
如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过:
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
if (!ctx.llm.hasAdapter(config.model)) {
|
||||
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配合 HMR
|
||||
|
||||
配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期
|
||||
- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务
|
||||
148
docs/user/zh-CN/develop/basic/index.md
Normal file
148
docs/user/zh-CN/develop/basic/index.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 第一个插件
|
||||
|
||||
本文带你编写一个最小的 Harness 插件并加载到 Agent 中。
|
||||
|
||||
## 插件是什么
|
||||
|
||||
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 在这里注册能力
|
||||
}
|
||||
```
|
||||
|
||||
就这么简单。
|
||||
|
||||
## 创建插件文件
|
||||
|
||||
在你的项目目录下创建 `src/my-plugin.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 监听 agent-loop 的 ready 事件
|
||||
ctx.on('ready', () => {
|
||||
console.log('[hello-plugin] 插件已加载!')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 注册到 cordis.yml
|
||||
|
||||
在你的 `cordis.yml` 中添加一条:
|
||||
|
||||
```yaml
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
启动后你会在控制台看到 `[hello-plugin] 插件已加载!`。
|
||||
|
||||
## 自动清理
|
||||
|
||||
通过 `ctx` 注册的任何东西——事件监听、tool、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。
|
||||
|
||||
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => {
|
||||
console.log('heartbeat')
|
||||
}, 5000)
|
||||
|
||||
// 返回的函数会在插件卸载时被调用
|
||||
return () => clearInterval(timer)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 声明依赖
|
||||
|
||||
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`:
|
||||
|
||||
```typescript
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 现在可用
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
框架会确保依赖的服务就绪后才加载你的插件。
|
||||
|
||||
## 插件的三种形态
|
||||
|
||||
除了函数形式,插件还支持对象形式和类形式:
|
||||
|
||||
### 对象形式
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
inject: ['tools'],
|
||||
apply(ctx: Context) {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 类形式
|
||||
|
||||
```typescript
|
||||
import { Service } from 'cordis'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myService')
|
||||
}
|
||||
|
||||
start() {
|
||||
// 服务启动逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。
|
||||
|
||||
## 完整示例
|
||||
|
||||
参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'echo-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'echo',
|
||||
description: 'Echo the given text back, uppercased.',
|
||||
parameters: {
|
||||
text: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](./config.md) — 让插件接受用户配置
|
||||
199
docs/user/zh-CN/develop/basic/tool.md
Normal file
199
docs/user/zh-CN/develop/basic/tool.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 开发一个 Tool
|
||||
|
||||
Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。
|
||||
|
||||
## 最小示例
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'greet',
|
||||
description: 'Greet someone by name.',
|
||||
parameters: {
|
||||
name: { type: 'string', required: true, description: 'The name to greet' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args 自动推导为 { name: string }
|
||||
return [{ type: 'text', text: `Hello, ${args.name}!` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 参数定义
|
||||
|
||||
`parameters` 用一种简洁的格式描述参数,框架会自动转换为模型需要的 JSON Schema。
|
||||
|
||||
### 基本类型
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
}
|
||||
// 推导类型: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### 枚举
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
// 推导类型: { mode: string } (运行时校验 enum 值)
|
||||
```
|
||||
|
||||
### 嵌套对象
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout: { type: 'number' },
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
// 推导类型: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### 数组
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
// 推导类型: { tags?: string[] }
|
||||
```
|
||||
|
||||
### 每个属性的字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | 值类型 |
|
||||
| `required` | `true` | 标记为必填(影响类型推导) |
|
||||
| `description` | `string` | 发送给模型的描述 |
|
||||
| `enum` | `string[]` | 允许的枚举值 |
|
||||
| `properties` | `SchemaSpec` | 嵌套属性(type 为 object 时) |
|
||||
| `items` | `SchemaProp` | 数组元素 schema(type 为 array 时) |
|
||||
|
||||
## execute 函数
|
||||
|
||||
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
|
||||
|
||||
```typescript
|
||||
async execute(args, exec) {
|
||||
// args: 根据 parameters 自动推导的类型
|
||||
// exec: ToolExecution 对象,提供执行上下文
|
||||
|
||||
// 返回 ContentBlock 数组
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
}
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
|
||||
|
||||
```typescript
|
||||
// 文本结果
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
|
||||
// 多个 block
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
|
||||
`defineTool` 在调用 `execute` 之前会自动校验模型生成的参数。如果参数不合法,会抛出 `ToolArgsError`,框架将其转换为 `isError` 结果返回给模型,让模型自行修正。
|
||||
|
||||
你不需要在 `execute` 里手动校验参数类型。
|
||||
|
||||
## 展示层 (Presentation)
|
||||
|
||||
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result:
|
||||
|
||||
```typescript
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
presentCall(args) {
|
||||
return {
|
||||
intent: 'terminal',
|
||||
title: `bash(${JSON.stringify(args.command).slice(0, 60)})`,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
intent: 'terminal',
|
||||
body: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`presentCall` 和 `presentResult` 是**纯函数**,不能有副作用——UI 可能在流式传输中和会话回放中多次调用它们。
|
||||
|
||||
## 注册与卸载
|
||||
|
||||
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
|
||||
|
||||
```typescript
|
||||
// 这样就够了:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
// 不需要:
|
||||
// const dispose = ctx.tools.register(...)
|
||||
// ctx.on('dispose', dispose)
|
||||
```
|
||||
|
||||
## 完整实战示例
|
||||
|
||||
一个文件计数 tool:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export const name = 'file-counter'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'count_files',
|
||||
description: 'Count files in a directory.',
|
||||
parameters: {
|
||||
path: { type: 'string', required: true, description: 'Directory path' },
|
||||
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
|
||||
},
|
||||
async execute(args) {
|
||||
const entries = await readdir(args.path, { withFileTypes: true })
|
||||
let files = entries.filter(e => e.isFile())
|
||||
if (args.extension) {
|
||||
files = files.filter(f => f.name.endsWith(args.extension!))
|
||||
}
|
||||
return [{ type: 'text', text: `Found ${files.length} files.` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件配置](./config.md) — 让你的 tool 可配置
|
||||
- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式
|
||||
152
docs/user/zh-CN/develop/framework/events.md
Normal file
152
docs/user/zh-CN/develop/framework/events.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 事件系统
|
||||
|
||||
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 监听事件
|
||||
|
||||
```typescript
|
||||
ctx.on('event-name', (payload) => {
|
||||
// 处理事件
|
||||
})
|
||||
```
|
||||
|
||||
### 触发事件
|
||||
|
||||
```typescript
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## 事件模式
|
||||
|
||||
Cordis 提供多种事件触发模式,适用于不同场景:
|
||||
|
||||
### emit — 广播
|
||||
|
||||
所有监听器并行执行,不关心返回值:
|
||||
|
||||
```typescript
|
||||
// 触发
|
||||
ctx.emit('agent/turn-end', { agentId, turnIndex })
|
||||
|
||||
// 监听
|
||||
ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
|
||||
console.log(`Turn ${turnIndex} ended`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — 短路
|
||||
|
||||
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
|
||||
|
||||
```typescript
|
||||
// 触发
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// 监听(返回值阻止后续监听器)
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// 返回 undefined 继续传递给下一个监听器
|
||||
})
|
||||
```
|
||||
|
||||
### serial — 顺序执行
|
||||
|
||||
所有监听器按注册顺序依次执行(异步安全):
|
||||
|
||||
```typescript
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — 管道
|
||||
|
||||
每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
|
||||
```typescript
|
||||
// 触发
|
||||
const finalMessages = await ctx.waterfall('llm/pre-request', messages)
|
||||
|
||||
// 监听(必须调用 next)
|
||||
ctx.on('llm/pre-request', async (messages, next) => {
|
||||
// 可以修改 messages
|
||||
messages.push(extraMessage)
|
||||
// 必须调用 next() 传递给下一个监听器
|
||||
return next(messages)
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
:::
|
||||
|
||||
## Typed Events
|
||||
|
||||
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
|
||||
|
||||
```typescript
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 现在 ctx.on('my-plugin/ready', ...) 和 ctx.emit('my-plugin/ready', ...)
|
||||
// 都有正确的类型推导
|
||||
```
|
||||
|
||||
## 命名约定
|
||||
|
||||
Harness 事件遵循 `namespace/action` 命名:
|
||||
|
||||
```
|
||||
agent/pre-step — agent 执行一步之前
|
||||
agent/post-step — agent 执行一步之后
|
||||
tool/call — tool 被调用
|
||||
tool/result — tool 返回结果
|
||||
llm/pre-request — LLM 请求发送前
|
||||
session/event — 会话事件被记录
|
||||
compact/start — 压缩开始
|
||||
compact/end — 压缩结束
|
||||
```
|
||||
|
||||
## 事件也是效果
|
||||
|
||||
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context) {
|
||||
// 这个监听器在插件 dispose 时自动清理
|
||||
ctx.on('agent/turn-end', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## 实战示例:日志插件
|
||||
|
||||
一个记录所有 tool 调用的简单插件:
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tool/call', ({ name, args }) => {
|
||||
console.log(`[tool] ${name}(${JSON.stringify(args)})`)
|
||||
})
|
||||
|
||||
ctx.on('tool/result', ({ name, result }) => {
|
||||
const text = result.content
|
||||
.filter(b => b.type === 'text')
|
||||
.map(b => b.text)
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
|
||||
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
|
||||
139
docs/user/zh-CN/develop/framework/index.md
Normal file
139
docs/user/zh-CN/develop/framework/index.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 插件与生命周期
|
||||
|
||||
深入了解 Cordis 插件模型和生命周期状态机。
|
||||
|
||||
## Fiber 状态机
|
||||
|
||||
每个被加载的插件对应一个 **Fiber**(作用域)。Fiber 有以下状态:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE
|
||||
↘ FAILED
|
||||
ACTIVE → UNLOADING → DISPOSED
|
||||
```
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| PENDING | 已声明但依赖未就绪 |
|
||||
| LOADING | 依赖就绪,正在执行 `apply` |
|
||||
| ACTIVE | 插件运行中 |
|
||||
| FAILED | `apply` 抛出异常 |
|
||||
| UNLOADING | 正在卸载,清理中 |
|
||||
| DISPOSED | 已完全卸载 |
|
||||
|
||||
## 依赖驱动的加载
|
||||
|
||||
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
|
||||
|
||||
```typescript
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 到这里时,ctx.tools 和 ctx.llm 一定存在
|
||||
}
|
||||
```
|
||||
|
||||
如果依赖的服务消失(比如提供者被热替换),插件会被自动卸载(ACTIVE → DISPOSED),待服务恢复后重新加载。
|
||||
|
||||
## 自动清理机制
|
||||
|
||||
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context) {
|
||||
// 事件监听——卸载时自动移除
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// 自定义资源——卸载时调用返回的函数
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
以下操作都会被自动追踪和清理:
|
||||
- `ctx.on(event, handler)` — 事件监听
|
||||
- `ctx.tools.register(tool)` — tool 注册
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
|
||||
- `ctx.effect(() => cleanup)` — 自定义资源
|
||||
|
||||
插件卸载时,这些注册按倒序逐个撤销。
|
||||
|
||||
## 嵌套上下文
|
||||
|
||||
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context) {
|
||||
// 注册一个子插件
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// 子插件有自己的 Fiber,父卸载时子也卸载
|
||||
}
|
||||
```
|
||||
|
||||
## dispose 语义
|
||||
|
||||
当你需要提前终止一个插件实例:
|
||||
|
||||
```typescript
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// 之后可以手动 dispose
|
||||
fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` 保证:
|
||||
1. 该插件注册的所有东西被撤销
|
||||
2. 它的子插件也被递归卸载
|
||||
3. 所有异步清理完成后 Promise resolve
|
||||
|
||||
## 热替换 (HMR)
|
||||
|
||||
在开发环境中(`cordis.yml` 加载了 `@cordisjs/plugin-hmr`),修改插件源文件会自动触发:
|
||||
|
||||
1. 卸载旧插件(清理所有注册)
|
||||
2. 重新加载新代码
|
||||
3. 执行新的 `apply`
|
||||
|
||||
因为所有注册都会被自动清理,所以热替换天然安全——不会留下旧状态。
|
||||
|
||||
## 实战:理解生命周期
|
||||
|
||||
```typescript
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.on('ready', () => {
|
||||
console.log('context ready')
|
||||
})
|
||||
|
||||
ctx.on('dispose', () => {
|
||||
console.log('plugin disposing')
|
||||
})
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
加载时输出:
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
context ready
|
||||
```
|
||||
|
||||
卸载时输出(逆序):
|
||||
```
|
||||
plugin disposing
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [服务与依赖](./service.md) — 让你的插件对外提供能力
|
||||
- [事件系统](./events.md) — 插件间通信的核心机制
|
||||
137
docs/user/zh-CN/develop/framework/service.md
Normal file
137
docs/user/zh-CN/develop/framework/service.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 服务与依赖
|
||||
|
||||
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
|
||||
|
||||
## 什么是服务
|
||||
|
||||
在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
|
||||
|
||||
```typescript
|
||||
ctx.tools // ToolRegistry 服务
|
||||
ctx.llm // LLM 服务
|
||||
ctx.agents // Agent 服务
|
||||
```
|
||||
|
||||
任何插件都可以提供一个新服务,供其他插件使用。
|
||||
|
||||
## 使用服务
|
||||
|
||||
声明 `inject` 来使用已有服务:
|
||||
|
||||
```typescript
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 在这里一定存在且就绪
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
框架保证:在 `apply` 执行时,`inject` 声明的服务已经全部就绪。如果服务还没准备好,你的插件会等着,不会执行。
|
||||
|
||||
## 提供服务
|
||||
|
||||
### 使用 Service 基类
|
||||
|
||||
```typescript
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // 本服务也可以依赖其他服务
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' 是服务名
|
||||
}
|
||||
|
||||
// 服务的公开方法
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
|
||||
|
||||
```typescript
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.metrics.record('tool_call', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### 类型声明
|
||||
|
||||
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
|
||||
|
||||
```typescript
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
metrics: MetricsService
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics')
|
||||
}
|
||||
|
||||
record(event: string, value: number) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖的行为
|
||||
|
||||
### 必选依赖 vs 可选依赖
|
||||
|
||||
```typescript
|
||||
// 必选:服务不存在时,插件不会加载
|
||||
export const inject = ['tools']
|
||||
|
||||
// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined
|
||||
export const inject = { optional: ['metrics'] }
|
||||
```
|
||||
|
||||
### 服务消失时的行为
|
||||
|
||||
如果一个必选依赖的服务在运行时消失(比如提供者被卸载):
|
||||
|
||||
1. 依赖它的插件自动 dispose
|
||||
2. 当服务重新出现时,插件自动重新加载
|
||||
|
||||
这保证了不会出现"调用一个已不存在的服务"的情况。
|
||||
|
||||
## 服务隔离
|
||||
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
name: 'group:'
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 5000
|
||||
- name: './src/plugin-a.ts'
|
||||
|
||||
- id: group-b
|
||||
name: 'group:'
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
|
||||
|
||||
## Harness 内置服务
|
||||
|
||||
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [事件系统](./events.md) — 插件间松耦合通信
|
||||
- [能力三件套](../practice/) — 服务在 seam 模式中的应用
|
||||
156
docs/user/zh-CN/develop/practice/index.md
Normal file
156
docs/user/zh-CN/develop/practice/index.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 能力的三层拆分
|
||||
|
||||
当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。
|
||||
|
||||
## 以 Bash 为例
|
||||
|
||||
考虑 "Bash 执行" 这个能力:
|
||||
|
||||
- **接口** (`dsh-bash`) — 定义"bash 执行"长什么样:输入是什么、输出是什么
|
||||
- **实现** (`dsh-bash-local`) — 真正在本地跑命令的代码
|
||||
- **消费者** (`dsh-tool-bash`) — 把这个能力包装成模型能调用的 tool
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
||||
│ (接口) │ │ (实现) │ │ (消费者/tool)│
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
▲ │
|
||||
└────────────────────────────────────────────┘
|
||||
inject: ['bash']
|
||||
```
|
||||
|
||||
## 拆分的好处
|
||||
|
||||
### 具体实现可替换
|
||||
|
||||
同一个接口可以有多种实现。用户通过 `cordis.yml` 选择:
|
||||
|
||||
```yaml
|
||||
# 本地执行
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# 或:远程沙箱执行(未来)
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
```
|
||||
|
||||
接口不变、tool 不变,只换实现。
|
||||
|
||||
### 独立演进
|
||||
|
||||
- 接口定义稳定后很少改动
|
||||
- 实现可以独立优化(性能、安全)
|
||||
- 消费者(tool)可以调整对模型的呈现方式
|
||||
|
||||
### 依赖解耦
|
||||
|
||||
- 实现 depend on 接口
|
||||
- 消费者 depend on 接口
|
||||
- 实现和消费者**互不依赖**
|
||||
|
||||
## Harness 中内置的三件套
|
||||
|
||||
| 能力 | 接口 (seam) | 实现 | 消费者 (tool) |
|
||||
|------|-------------|------|---------------|
|
||||
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
|
||||
| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
|
||||
|
||||
## 开发你自己的三件套
|
||||
|
||||
### 第一步:定义接口
|
||||
|
||||
```typescript
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
myCap: MyCapService
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MyCapService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myCap')
|
||||
}
|
||||
|
||||
/** 执行能力的核心方法 */
|
||||
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
||||
}
|
||||
|
||||
export interface MyCapRequest {
|
||||
input: string
|
||||
}
|
||||
|
||||
export interface MyCapResult {
|
||||
output: string
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:编写实现
|
||||
|
||||
```typescript
|
||||
// packages/my-cap/my-cap-local/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
|
||||
|
||||
class MyCapLocal extends MyCapService {
|
||||
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
||||
// 具体实现
|
||||
return { output: request.input.toUpperCase() }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'my-cap-local'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(MyCapLocal)
|
||||
}
|
||||
```
|
||||
|
||||
### 第三步:编写消费者 (tool)
|
||||
|
||||
```typescript
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-my-cap'
|
||||
export const inject = ['tools', 'myCap']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'my_cap',
|
||||
description: 'Execute my capability.',
|
||||
parameters: {
|
||||
input: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
const result = await ctx.myCap.execute({ input: args.input })
|
||||
return [{ type: 'text', text: result.output }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### 在 cordis.yml 中组合
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-my-cap-local'
|
||||
- name: '@deepseek-ai/dsh-tool-my-cap'
|
||||
```
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **不要预防性拆分** — 只有当你确实需要可替换实现时才拆三件套。一个简单的 tool 插件不需要拆分。
|
||||
- **接口定义 Request/Result 类型** — 实现和消费者只依赖接口包。
|
||||
- **Explicit > Implicit** — 实现中的默认值处理应该是显式的 `resolve(request): Spec` 步骤,不是隐藏在 `run()` 中的 `?? default`。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
169
docs/user/zh-CN/develop/practice/llm-adapter.md
Normal file
169
docs/user/zh-CN/develop/practice/llm-adapter.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# LLM 适配器
|
||||
|
||||
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
|
||||
|
||||
## 概述
|
||||
|
||||
LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,将 Harness 的统一请求格式转换为具体 API 的调用。
|
||||
|
||||
## 最小实现
|
||||
|
||||
```typescript
|
||||
import type { Context } from 'cordis'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class MyAdapter extends LlmAdapter {
|
||||
private apiKey: string
|
||||
|
||||
constructor(apiKey: string) {
|
||||
super()
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// 1. 将 options.messages 转换为你的 API 格式
|
||||
// 2. 调用 API(流式)
|
||||
// 3. 将 API 响应转换为 StreamChunk 序列
|
||||
}
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export const name = 'my-llm-adapter'
|
||||
export const inject = ['llm']
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
const adapter = new MyAdapter(config.apiKey)
|
||||
ctx.llm.registerAdapter(config.models, adapter)
|
||||
}
|
||||
```
|
||||
|
||||
## StreamChunk 协议
|
||||
|
||||
`stream()` 必须按以下协议 yield chunk:
|
||||
|
||||
```typescript
|
||||
// 1. 每个内容块以 block-start 开始
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 2. 文本块使用 text-delta
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 3. 每个内容块以 block-end 结束(携带完整 block)
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool call 块
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 1,
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
argumentsDelta: '{"command":"ls"}',
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
arguments: '{"command":"ls"}',
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token 用量
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. 结束原因
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
|
||||
- 每个 `block-start` 必须有对应的 `block-end`
|
||||
- `index` 从 0 递增,标识内容块顺序
|
||||
- `tool-call-delta` 的 `argumentsDelta` 是 JSON 字符串的增量(可以一次 yield 全部,也可以分多次)
|
||||
- `finish` 必须是最后一个 chunk
|
||||
- `usage` 在 `finish` 之前 yield
|
||||
|
||||
## GenerateOptions
|
||||
|
||||
`stream()` 接收的请求包含:
|
||||
|
||||
```typescript
|
||||
interface GenerateOptions {
|
||||
/** 模型名 */
|
||||
model: string
|
||||
/** 对话历史 */
|
||||
messages: Message[]
|
||||
/** 可用的 tool 列表 */
|
||||
tools?: ToolSpec[]
|
||||
/** 系统提示词 */
|
||||
system?: string
|
||||
/** 最大输出 token */
|
||||
maxTokens?: number
|
||||
/** 温度 */
|
||||
temperature?: number
|
||||
}
|
||||
```
|
||||
|
||||
你的适配器需要将这些映射到具体 API 的参数。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
```typescript
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
第一个参数是该适配器支持的模型名列表。当用户在 `cordis.yml` 中配置 `model: model-name-1` 时,框架会路由到这个适配器。
|
||||
|
||||
## 在 cordis.yml 中使用
|
||||
|
||||
```yaml
|
||||
- id: my-llm
|
||||
name: './src/my-llm-adapter.ts'
|
||||
config:
|
||||
apiKey: !!js process.env.MY_API_KEY
|
||||
models:
|
||||
- my-model-v1
|
||||
- my-model-v2
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-agent'
|
||||
config:
|
||||
model: my-model-v1 # 引用上面注册的模型名
|
||||
```
|
||||
|
||||
## 实战参考
|
||||
|
||||
仓库中有两个完整实现可供参考:
|
||||
|
||||
- `packages/llm/llm-deepseek/` — DeepSeek API 适配器(OpenAI 兼容格式)
|
||||
- `packages/llm/llm-pi-ai/` — Pi AI 适配器(不同的 API 格式)
|
||||
- `examples/echo-agent/src/mock-llm.ts` — 最简 mock 适配器(教学用)
|
||||
|
||||
mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地逻辑演示了完整的 chunk 序列。
|
||||
|
||||
## 错误处理
|
||||
|
||||
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
|
||||
|
||||
```typescript
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, { /* ... */ })
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
}
|
||||
// ... 正常流式处理
|
||||
}
|
||||
```
|
||||
57
docs/user/zh-CN/guide/config.md
Normal file
57
docs/user/zh-CN/guide/config.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 配置文件
|
||||
|
||||
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
|
||||
## 从真实配置开始
|
||||
|
||||
仓库中的示例就是可以运行的配置,也是新项目最可靠的起点:
|
||||
|
||||
- [echo-agent](../../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。
|
||||
- [coding-agent](../../../../examples/coding-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。
|
||||
- [acp-agent](../../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。
|
||||
|
||||
最小配置由一组插件条目组成:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-agent'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## 插件条目
|
||||
|
||||
`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`。
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
|
||||
## JavaScript 值和环境变量
|
||||
|
||||
Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
标签是 `!!js`,不是 `!js`。
|
||||
|
||||
## 精确配置参考
|
||||
|
||||
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../../architecture.md)和[能力接口](../../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../../examples/README.md)中最接近的例子。
|
||||
47
docs/user/zh-CN/guide/index.md
Normal file
47
docs/user/zh-CN/guide/index.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 介绍
|
||||
|
||||
DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。
|
||||
|
||||
## 它是什么
|
||||
|
||||
Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。
|
||||
|
||||
```yaml
|
||||
# 选择 LLM 后端
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
# 选择应用模板
|
||||
- name: '@deepseek-ai/dsh-stdio-agent'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## 适合谁
|
||||
|
||||
### 应用使用者
|
||||
|
||||
如果你只是想用一个现成的 Agent 应用(如编程助手、对话代理),你需要的全部操作就是:
|
||||
|
||||
1. 复制一个 example 模板
|
||||
2. 填写 API key
|
||||
3. 运行
|
||||
|
||||
不需要写任何代码。详见 [快速开始](./quickstart.md)。
|
||||
|
||||
### 插件开发者
|
||||
|
||||
如果你想为 Agent 添加新能力——一个自定义 tool、一个新的 LLM 适配器、一个新的执行后端——你需要编写一个插件。Harness 提供了清晰的扩展接口和类型安全的开发体验。详见 [开发](../develop/basic/)。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **只需要配置** — `cordis.yml` 决定能力集合,换模型、加工具只需改一行
|
||||
- **随时替换 (HMR)** — 开发时修改插件代码,无需重启进程
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **运行时**: Node.js ^22.19 或 >= 24
|
||||
- **语言**: TypeScript (ESM)
|
||||
- **框架**: Cordis
|
||||
- **包管理**: pnpm workspaces(仓库固定使用 pnpm 11)
|
||||
97
docs/user/zh-CN/guide/quickstart.md
Normal file
97
docs/user/zh-CN/guide/quickstart.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 快速开始
|
||||
|
||||
本指南带你在 5 分钟内跑起一个 Agent。
|
||||
|
||||
## 环境准备
|
||||
|
||||
- [Node.js](https://nodejs.org/) ^22.19 或 >= 24
|
||||
- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本)
|
||||
|
||||
```sh
|
||||
# 确认版本
|
||||
node -v # v22.19.x,或 v24.x 及更高版本
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## 第一步:运行 echo-agent
|
||||
|
||||
echo-agent 不需要 API key,装好依赖就能跑。
|
||||
|
||||
```sh
|
||||
# 克隆仓库
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动 echo-agent
|
||||
pnpm run demo:echo
|
||||
```
|
||||
|
||||
启动后你会看到:
|
||||
|
||||
```
|
||||
echo-agent ready. Type a message ("echo <text>" triggers the tool).
|
||||
>
|
||||
```
|
||||
|
||||
试着输入:
|
||||
|
||||
```
|
||||
> echo hello world
|
||||
```
|
||||
|
||||
你会看到模型发起了一次 tool call(工具调用),echo 工具将文本转为大写并返回:
|
||||
|
||||
```
|
||||
[tool call] echo({"text":"hello world"})
|
||||
[tool result] ECHO: HELLO WORLD
|
||||
```
|
||||
|
||||
恭喜!环境没问题。
|
||||
|
||||
## 第二步:使用真实模型调用
|
||||
|
||||
接下来接入真实的 DeepSeek 模型,跑一个完整的命令行 Agent。
|
||||
|
||||
### 获取 API Key
|
||||
|
||||
前往 [DeepSeek Platform](https://platform.deepseek.com/) 获取你的 API key。
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
在仓库根目录创建 `.env` 文件(已被 gitignore):
|
||||
|
||||
```sh
|
||||
DEEPSEEK_API_KEY=sk-your-key-here
|
||||
```
|
||||
|
||||
### 启动 coding-agent
|
||||
|
||||
```sh
|
||||
pnpm run demo:repl
|
||||
```
|
||||
|
||||
```
|
||||
agent REPL ready. Give it a coding task.
|
||||
>
|
||||
```
|
||||
|
||||
这就是一个完整的编程助手,它能读写文件、跑命令、拆分子任务。
|
||||
|
||||
试着给它一个任务:
|
||||
|
||||
```
|
||||
> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它
|
||||
```
|
||||
|
||||
## 回头看
|
||||
|
||||
echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-agent`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法
|
||||
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端
|
||||
21
docs/user/zh-CN/index.md
Normal file
21
docs/user/zh-CN/index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: home
|
||||
hero:
|
||||
name: DeepSeek Harness
|
||||
text: 插件化 Agent 开发框架
|
||||
tagline: 基于 Cordis 微内核,一切皆插件
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guide/quickstart
|
||||
- theme: alt
|
||||
text: 开发插件
|
||||
link: /develop/basic/
|
||||
features:
|
||||
- title: 插件化架构
|
||||
details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
- title: 配置即组合
|
||||
details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。
|
||||
- title: 开箱即用
|
||||
details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。
|
||||
---
|
||||
Reference in New Issue
Block a user