cola-plugin-sdk
v0.4.0
Published
Plugin SDK for Cola channel plugins
Readme
cola-plugin-sdk
Cola 的 channel 插件 SDK。
Channel 插件让 Cola 能够在桌面端之外(微信、Slack、Telegram、Discord、自定义 HTTP 网关等)和用户对话:传输层交给插件,agent loop 留给 Cola server。
English: README.md
安装
pnpm add cola-plugin-sdk仓库内通过 pnpm workspace 解析 (workspace:*);独立仓库请依赖已发布版本。
快速开始
插件就是一个从 package.json#cola.channel.entry 指向的入口文件 default export 出来的对象。
// src/index.ts
import { defineChannel, createPollLoop, withBackoff } from 'cola-plugin-sdk'
type State = {
lastCursor: string | null
}
export default defineChannel<State>({
id: 'my-channel',
meta: {
label: 'My Channel',
description: '把 Cola 桥接到 my-platform.com',
},
capabilities: {
receive: { text: true, image: true },
send: { text: true, image: true, markdown: true, typing: true },
limits: { maxTextLength: 4096 },
},
// 'isolated'(默认):每个外部用户独立 session
// 'shared':所有消息都进桌面端 session
sessionMode: 'isolated',
gateway: {
async start(ctx) {
const state = ctx.state // 插件生命周期内共享可变状态
state.lastCursor = null
createPollLoop({
signal: ctx.abortSignal,
intervalMs: 1000,
fetch: async (signal) =>
withBackoff(() => fetchMessagesSince(state.lastCursor, signal)),
onMessages: async (items) => {
for (const msg of items) {
state.lastCursor = msg.id
await ctx.deliver({
channelUserId: msg.from,
message: msg.text,
attachments: msg.files,
})
}
},
onError: (err) => ctx.logger.warn('轮询失败', err),
})
},
async stop(ctx) {
ctx.logger.info('gateway 停止中')
},
getStatus(ctx) {
return { connected: true, configured: Boolean(ctx.config.get().apiKey) }
},
},
outbound: {
async sendText(ctx) {
await postMessage(ctx.channelUserId, ctx.text, ctx.config.apiKey as string)
},
async sendMedia(ctx) {
await postMedia(ctx.channelUserId, ctx.filePath, ctx.mediaType)
},
async sendTyping(ctx) {
await setTyping(ctx.channelUserId, ctx.active)
},
textChunkLimit: 4000,
},
auth: {
async login(ctx) {
const qr = await requestQrCode()
ctx.onQrCode?.(qr.dataUrl)
const session = await pollForLogin(qr.token)
ctx.runtime.logger.info(`登录成功: ${session.user}`)
// 凭证自行持久化,插件通常写到 `${colaDir}/plugins/<id>/accounts/`
},
},
commands: [
{
name: 'status',
description: '显示连接状态',
async execute(ctx) {
return { reply: `已连接: ${ctx.config.user ?? '未知'}` }
},
},
],
})目录结构
my-plugin/
├── package.json # 必须包含 "cola.channel" 字段(见下)
├── dist/ # 构建产物,对应 cola.channel.entry
│ └── index.js
└── src/
└── index.tspackage.json 必须声明 manifest:
{
"name": "cola-channel-my-platform",
"version": "0.1.0",
"type": "module",
"cola": {
"channel": {
"id": "my-channel",
"label": "My Channel",
"entry": "dist/index.js",
"minSdkVersion": "0.1.0"
}
},
"dependencies": {
"cola-plugin-sdk": "^0.1.0"
}
}Loader 读取 cola.channel.id / entry / minSdkVersion。minSdkVersion 高于当前 SDK 时会被跳过并 warn。
SDK API
工厂函数
| 导出 | 用途 |
| --- | --- |
| definePlugin(plugin) | 带完整 Plugin<State> 类型推断的恒等函数 |
| defineChannel(def) | 纯 channel 插件的简写 — meta 从 channel.meta 推断 |
Channel Adapters
ChannelDefinition 可以挑选下列 adapter 的任意子集:
| Adapter | 触发时机 | 主要方法 |
| --- | --- | --- |
| gateway | 插件启动 / 停止 / 重载 | start(ctx)、stop(ctx)、reload(ctx)、getStatus(ctx) |
| outbound | Agent 产生回复 | sendText(ctx)、sendMedia(ctx)、sendTyping(ctx) |
| auth | 用户执行 cola channel login <id> | login(ctx) |
| config | Server 读取配置 schema | schema |
gateway.start 收到的 GatewayContext<State> 包含:
config— 插件配置文件的冻结快照runtime— host 注入的 API(见下)state—State类型对象,插件可自由修改;跨start/stop存活abortSignal— server 关闭插件时触发deliver(payload)— 把入站消息交给 agent looplogger— 带命名空间的 logger
PluginRuntime
Host 会把这个对象注入到每个 adapter 的 context:
type PluginRuntime = {
identity: {
resolve(channelUserId): Promise<string | null> // channel 用户 → Cola 用户 id
bind(channelUserId): Promise<void> // 创建 / 刷新绑定
}
config: { get(): Readonly<Record<string, unknown>> }
logger: PluginLogger
offload<T>(fn: () => T): Promise<T> // 把 CPU 重活丢到 worker pool
version: string
reloadGateway?(): Promise<void>
}Commands
plugin.commands[] 暴露 slash 命令,CLI 和对话中均可触发:
commands: [{
name: 'bind',
aliases: ['link'],
description: '把当前 channel 用户绑定到 Cola 账户',
args: [{ name: 'token', description: '配对 token', required: true }],
async execute(ctx) {
await ctx.runtime!.identity.bind(ctx.args.trim())
return { reply: '已绑定。' }
},
}]Helpers
| Helper | 作用 |
| --- | --- |
| createPollLoop({ fetch, onMessages, intervalMs, signal, onError }) | 长轮询脚手架;signal.abort 后干净退出 |
| withBackoff(fn, { maxRetries, baseMs, maxMs }) | 指数退避 + ±25% jitter |
版本门槛
SDK 导出 SDK_VERSION。在 package.json#cola.channel.minSdkVersion 声明最低要求,loader 通过 satisfiesMinVersion(标准 semver)比对;需要更高 SDK 的插件会被跳过。
开发流程
1. 建项目
在 Cola 仓库之外(或内部贡献)新建一个包,按照上文的目录结构。
2. 构建
任何 ESM bundler 都行。仓库内约定使用 tsup:
pnpm tsup src/index.ts --format esm --dts --sourcemap产物路径需与 cola.channel.entry 一致(通常是 dist/index.js)。
3. 本地安装
快速调试最简方式:软链接到 Cola 插件目录:
ln -s "$(pwd)" ~/.cola/plugins/<your-plugin-id>重启 server:
cola server stop && cola server startcola plugin list 里会看到插件,source 为 local。
4. 配置
把配置写入 ~/.cola/plugins/<id>/config.json — 就是 runtime.config.get() 的返回值。
5. 迭代
- 改代码 → rebuild →
cola server stop && cola server start cola channel list— runtime 状态(connected / disconnected)cola channel login <id>— 触发auth.loginadapter- Server 日志:
~/.cola/logs/server.log
6. 发布到 Plugin Store(可选)
CLI 从 marswaveai/cola-plugins 的 registry.json 解析插件。提交 PR 新增:
{
"id": "my-channel",
"label": "My Channel",
"description": "…",
"version": "0.1.0",
"minSdkVersion": "0.1.0",
"downloadUrl": "https://…/my-channel-0.1.0.tar.gz"
}终端用户即可:
cola plugin search
cola plugin install my-channel
cola plugin update my-channel运行时模型
- Isolated(默认): 每个
channelUserId独立 Cola session scope;用runtime.identity.bind把外部用户绑定到 Cola 账户 - Shared: 所有入站消息都进桌面端 session。适合通知桥 / 需要和人类坐在电脑前共享上下文的助手场景
- 状态归属: 插件拥有
state;host 管身份、配置、agent loop。跨重启的持久化数据写到${getColaDir()}/plugins/<id>/ - 生命周期:
start每次插件启动执行一次;stop关闭或换配置时执行(换配置后紧跟一次新start);reload可选,热更新配置时优于 stop+start
声明 Channel 动作(message 工具)
Channel 可以通过共享的 message 工具向 Agent 暴露平台原生动作(react / edit / delete / send)。在 channel 上实现 messaging 适配器:
import { definePlugin, type ChannelMessageActionAdapter } from 'cola-plugin-sdk'
const messaging: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) => ({
actions: ['send', 'react', 'delete'],
schema: {
properties: {
myCustomParam: { type: 'string' },
},
},
}),
handleAction: async (ctx) => {
// ctx.action, ctx.params, ctx.toolContext.currentMessageId
return { ok: true, message: 'done' }
},
}
export default definePlugin({
id: 'my-channel',
meta: { label: 'My Channel', description: '...' },
channel: {
meta: { label: 'My Channel', description: '...' },
capabilities: { receive: { text: true }, send: { text: true } },
messaging,
},
})动作名必须来自 CHANNEL_MESSAGE_ACTION_NAMES 闭集;自定义参数通过 schema contribution 带入,不需要占用新的动作名。ctx.toolContext.currentMessageId 由 host 从入站事件注入,因此 react/edit/delete 即使不带参数也能作用于当前消息。
内部(非稳定)API
channel.agentPrompt 和 channel.agentTools 标记了 @internal — 可以让插件贡献 system prompt 片段和 agent 工具。不遵循 semver,随时可能变,使用需自担风险。
