The VitePress config declared no position for the subsystem or other-interface sections, so `indexOf` returned -1 and sorted them ahead of every declared group: the reference landing page's own sidebar entry sat 1549px below the fold. Four subsystem pages also shared `order` values with pages in the same section, resolved only by sort stability and array concatenation order. Section placement and collapse move into the manifest as a per-locale declaration, and `sectionSpec` throws for an undeclared section instead of sorting it silently to the top. Subsystem pages are grouped by concern, the six topical groups collapse until one holds the page being read, and page order derives from array position. The projector drops the language-switcher line and repository badge the canonical pages carry for their GitHub readers. The navigation bar gains the DeepSeek wordmark, a release-stage tag, and a favicon; the sidebar scrollbar rests invisible and appears while scrolling. Subsystem pages carry a two-level outline, and the two plugin-development tracks now cross-link.
143 lines
3.5 KiB
Markdown
143 lines
3.5 KiB
Markdown
# Your first plugin
|
|
|
|
English | [中文](index.zh.md)
|
|
|
|
This tutorial creates a minimal Harness plugin and loads it into the Web UI. Start from a repository checkout that has completed the [run-from-source path](../../../../README.md#run-from-source).
|
|
|
|
## Create a local project
|
|
|
|
From the repository root, create a scratch project for the tutorial:
|
|
|
|
```sh
|
|
mkdir -p scratch-plugin/src
|
|
```
|
|
|
|
## What is a plugin?
|
|
|
|
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
|
|
|
|
```ts
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
|
export const name = 'my-plugin'
|
|
|
|
export function apply(ctx: Context) {
|
|
// Register capabilities here.
|
|
}
|
|
```
|
|
|
|
That is the complete configuration.
|
|
|
|
## Create the plugin file
|
|
|
|
Create `scratch-plugin/src/my-plugin.ts`:
|
|
|
|
```ts
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
|
export const name = 'hello-plugin'
|
|
|
|
export function apply(ctx: Context) {
|
|
// Required dependencies are ready before apply runs.
|
|
console.log('[hello-plugin] plugin loaded!')
|
|
}
|
|
```
|
|
|
|
## Register it in cordis.yml
|
|
|
|
Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin:
|
|
|
|
```yaml
|
|
- insert:
|
|
- id: hello
|
|
name: './src/my-plugin.ts'
|
|
```
|
|
|
|
Start the Web UI with that overlay:
|
|
|
|
```sh
|
|
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
|
```
|
|
|
|
Open `http://127.0.0.1:3080`. The terminal prints `[hello-plugin] plugin loaded!` during startup.
|
|
|
|
## Automatic cleanup
|
|
|
|
Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
|
|
|
|
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
|
|
|
|
```ts
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
|
export function apply(ctx: Context) {
|
|
ctx.effect(() => {
|
|
const timer = setInterval(() => {
|
|
console.log('heartbeat')
|
|
}, 5000)
|
|
|
|
// The returned function runs when the plugin unloads.
|
|
return () => clearInterval(timer)
|
|
})
|
|
}
|
|
```
|
|
|
|
## Declare dependencies
|
|
|
|
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
|
|
|
|
```ts ignore-check
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
|
export const name = 'my-tool-plugin'
|
|
export const inject = ['tools']
|
|
|
|
export function apply(ctx: Context) {
|
|
// ctx.tools is ready here.
|
|
ctx.tools.register(/* ... */)
|
|
}
|
|
```
|
|
|
|
The framework waits for every required service before loading the plugin.
|
|
|
|
## Three plugin forms
|
|
|
|
In addition to a function module, a plugin can use object or class form.
|
|
|
|
### Object form
|
|
|
|
```ts
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
|
export default {
|
|
name: 'my-plugin',
|
|
inject: ['tools'],
|
|
apply(ctx: Context) {
|
|
// ...
|
|
},
|
|
}
|
|
```
|
|
|
|
### Class form
|
|
|
|
```ts
|
|
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
|
|
export default class MyService extends Service {
|
|
static inject = ['tools']
|
|
|
|
constructor(ctx: Context) {
|
|
super(ctx, 'myService')
|
|
// Perform synchronous initialization in the constructor.
|
|
}
|
|
}
|
|
```
|
|
|
|
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
|
|
|
|
## Next steps
|
|
|
|
- [Build a tool](./tool.md) — learn the tool definition DSL
|
|
- [Plugin configuration](./config.md) — accept user configuration
|
|
- [Cordis tutorial](../../../cordis-tutorial/index.md) — the plugin framework underneath, built from a scratch directory with no API key
|