Plugin 开发指南
从目录结构、profile 配置到 Agent 运行时上下文,完整开发一个 Downcity Plugin
Plugin 开发指南
这篇文档介绍 Downcity Plugin 的完整开发模型。Plugin 是一个独立的能力单元:它拥有自己的配置、action、状态和生命周期;运行时能力由 Agent 通过 PluginContext 按最小接口提供。
设计原则
- Plugin 构造函数只接收自己的
profile,不接收Agent、City、Embassy或dependencies容器。 - Plugin 内部创建并拥有自己的 provider、client、store、queue 和资源句柄。
- Workspace 能力通过运行时 Context 暴露;图片、语音等外部服务通过 Plugin 构造函数注入对应的窄接口。
- 配置是可持久化、可校验的 profile;运行态对象是实例内部状态,不能放进配置文件。
- 一个 Plugin 只负责一个清晰的领域能力。需要不同实现时,创建新的 Plugin,而不是在一个 Plugin 内继续堆叠组合开关。
目录结构
一个可发布的第三方 Plugin 最小目录如下:
example-plugin/
├── plugin.json
├── package.json
├── README.md
├── icon.svg
└── plugin.jsplugin.js 是安装产物的自包含 ESM 入口;文件名不固定,只要与 plugin.json 的 entry 一致即可。src/ 和构建配置可以保留在源码仓库中,但安装目录必须包含 plugin.json、package.json、README.md 和入口文件。
plugin.json
清单描述安装、展示和 profile Schema,不描述运行态依赖:
{
"schema_version": 1,
"id": "example",
"version": "1.0.0",
"title": "Example",
"description": "A small example Plugin.",
"icon": "icon.svg",
"entry": "plugin.js",
"source": "github:owner/example-plugin",
"config": {
"schema": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": ["local", "remote"],
"default": "local"
}
},
"additionalProperties": false
},
"defaults": {
"provider": "local"
}
}
}config.schema 用于 CLI/Desktop 表单和启动前校验;config.defaults 是没有指定 profile 时传入构造函数的完整默认对象。Schema 中的字段 default 主要是表单提示,不替代 defaults。
profile 与运行态
profile 只放用户可以保存、复制和审计的值,例如 provider 名称、模型 ID、超时和路径:
export interface ExampleProfile {
/** 选择 Plugin 使用的实现。 */
provider?: "local" | "remote";
/** 远程服务地址。 */
endpoint?: string;
}运行态对象必须在 Plugin 内部创建:
export class ExamplePlugin extends BasePlugin {
readonly name = "example";
private readonly client: ExampleClient;
constructor(profile: ExampleProfile = {}) {
super();
this.client = new ExampleClient({
provider: profile.provider ?? "local",
endpoint: profile.endpoint,
});
}
}不要把下面这些对象放入 profile:浏览器实例、数据库连接、队列、函数回调、Agent 实例或 City/Embassy 实例。
Plugin 类与入口
import { BasePlugin, create_action } from "@downcity/agent";
import { z } from "zod";
export class ExamplePlugin extends BasePlugin {
readonly name = "example";
readonly title = "Example";
readonly description = "An example Plugin.";
constructor(profile: ExampleProfile = {}) {
super();
// 在这里校验 profile,并创建 Plugin 自己拥有的运行态依赖。
}
readonly actions = {
status: create_action({
description: "Return the current Plugin status.",
input_schema: z.object({}),
execute: async ({ context, execution }) => ({
success: true,
data: {
agent_id: context.agent_id,
workspace_id: context.workspace_id,
call_id: execution.call_id,
},
}),
}),
};
}第三方入口必须导出 plugin(profile):
import type { JsonObject, Plugin } from "@downcity/agent";
export function plugin(profile: JsonObject): Plugin {
return new ExamplePlugin(profile as ExampleProfile);
}Loader 会对 profile 做 Schema 校验,然后为每个 Agent 创建一个独立实例。
PluginContext
Action、system 和生命周期钩子可以使用 PluginContext:
interface PluginContext {
agent_id: string;
workspace_id: string;
workspace_path: string;
data_path: string;
files: FileSystem;
data_files: FileSystem;
logger: Logger;
ai?: PluginAiServices;
web?: PluginWebServices;
sessions: AgentSessions;
plugins: AgentPlugins;
workspace_env: Readonly<Record<string, string>>;
instructions: readonly string[];
}files 用于用户 Workspace;data_files 用于 Agent 私有持久化数据。不要把私有数据库、缓存或凭证写入用户 Workspace。
使用 Agent 能力
宿主在构造 Agent 时提供服务:
const agent = new Agent({
id: "assistant",
plugins: [new ExamplePlugin({ provider: "local" })],
});Plugin 只读取最小接口:
const models = await image_ai.catalog();
if (!models) return { success: false, error: "AI service is unavailable" };这让同一个 Plugin 可以在 CLI、Desktop 或嵌入式宿主中运行,而不依赖某个宿主的完整对象。
Action
每个 action 都应该有明确输入 Schema、稳定结果和可取消的异步执行:
run: create_action({
input_schema: z.object({ value: z.string().min(1) }),
execute: async ({ input, execution }) => {
execution.abort_signal.throwIfAborted();
const result = await client.run(input.value, {
signal: execution.abort_signal,
});
return { success: true, data: result };
},
})长任务必须响应 execution.abort_signal,并在外部请求中传递 AbortSignal。超时和取消由调用层控制,Plugin 不应自行阻塞 Agent。
生命周期
生命周期资源只由 Plugin 自己创建和释放:
readonly lifecycle = {
start: async (context: PluginContext) => {
await this.client.initialize({ agent_id: context.agent_id });
},
enter_workspace: async (context: PluginContext) => {
await this.client.select_workspace(context.workspace_path);
},
leave_workspace: async () => {
await this.client.clear_workspace();
},
stop: async () => {
await this.client.dispose();
},
};Agent 注册 Plugin 后启动生命周期,卸载或停止时调用 stop。stop 应该幂等,且释放浏览器、连接、定时器和队列等所有长期资源。
配置文件与装配
本地宿主可以保存多个 profile:
schema_version = 1
[profiles.default]
provider = "local"
[profiles.remote]
provider = "remote"
endpoint = "https://example.com"Agent 绑定 profile 名称后,Loader 会读取 profile、校验 Schema,并执行:
registration.create(profile)内建 Plugin 与第三方 Plugin 使用同一条路径。宿主负责配置来源和凭证,Plugin 只接收已经解析好的 profile。
数据边界
- 用户创建和编辑的文件:
context.files。 - Plugin 私有索引、缓存和状态:
context.data_files。 - Workspace 级资源:由
enter_workspace/leave_workspace管理。 - Agent 级资源:由
start/stop管理。
Plugin 不应修改其他 Plugin 的私有目录,也不应保存宿主的完整配置对象。
测试
测试重点是 profile、action、服务缺失、取消和生命周期:
it("uses the profile to select the provider", async () => {
const plugin = new ExamplePlugin({ provider: "local" });
expect(plugin.name).toBe("example");
});
it("returns a stable error when AI is unavailable", async () => {
const agent = new Agent({ id: "test", plugins: [new ExamplePlugin()] });
const result = await agent.plugins.call("example", "models", {});
expect(result.success).toBe(false);
});不要通过 Agent.ai 传递通用 AI 对象。Plugin 只接收自身需要的窄服务接口,测试时传入该接口的测试实现。
发布检查清单
plugin.json的id、version、entry和config.schema正确。- 入口导出
plugin(profile),且每次调用都会创建新实例。 - profile 不包含连接、实例、回调和宿主对象。
- 所有长期资源都在
stop中释放。 - action 输入经过 Schema 校验,异步任务响应取消信号。
- 私有数据使用
data_files,用户文件使用files。 - CLI、Desktop 和嵌入式宿主都能通过同一 profile 构造 Plugin。