面向开发者的插件 API 参考手册。所有可用类型、方法、字段、错误码都列在这里。 用户向导请看 docs/channel-management.md。
weacpx 把消息频道做成了 npm 插件。一个频道插件就是一个 npm 包,默认导出一个 WeacpxPlugin,里面声明若干个 ChannelPluginDefinition。daemon 在启动时会读 ~/.weacpx/config.json 里的 plugins[],从 ~/.weacpx/plugins/node_modules/<plugin-name> 动态 import 插件包,注册其频道工厂和 CLI provider。
- 谁应该看这份文档
- 快速开始:最小可运行插件
- 工程结构
- 1. 插件入口:
WeacpxPlugin - 2. 频道注册:
ChannelPluginDefinition - 3. 频道工厂:
ChannelFactory - 4. 频道运行时:
MessageChannelRuntime - 5. 启动上下文:
ChannelStartInput - 6. 出站配额:
OutboundQuota - 7. 应用日志:
AppLogger - 8. 编排回调:
OrchestrationDeliveryCallbacks - 9. 消费者锁:
ConsumerLock - 10. CLI provider:
ChannelCliProvider - 11. CLI provider 辅助类型
- 12. 配置形态:
ChannelRuntimeConfig - 13. ChatKey 与 channelId 约定
- 14. 校验规则
- 15. plugin doctor 诊断
- 16. 端到端生命周期
- 17. 发布契约
- 18. 测试建议
- 19. 参考实现
- 想为 weacpx 增加一个新频道(飞书、Discord、Slack、微信公众号 …)的开发者
- 想把现有 IM 系统接入 weacpx 编排能力的工程
- 想在自己的私有部署里 fork / 扩展频道行为的人
如果你只是消费方(安装并使用别人写好的频道),看 docs/channel-management.md 就够了。
// src/index.ts
import type {
ChannelStartInput,
CoordinatorMessageInput,
MessageChannelRuntime,
WeacpxPlugin,
} from "weacpx/plugin-api";
class HelloChannel implements MessageChannelRuntime {
readonly id = "hello";
isLoggedIn(): boolean { return true; }
async login(): Promise<string> { return "hello credentials configured"; }
logout(): void {}
async start(_: ChannelStartInput): Promise<void> {
// 接收消息:调用 input.agent.handle(chatKey, text)
// 发送消息:保留 input.agent 的引用,由你的网关回调驱动
}
async notifyTaskCompletion(): Promise<void> {}
async notifyTaskProgress(): Promise<void> {}
async sendCoordinatorMessage(_: CoordinatorMessageInput): Promise<void> {}
}
const plugin: WeacpxPlugin = {
apiVersion: 1,
name: "weacpx-channel-hello",
minWeacpxVersion: "0.3.3",
channels: [
{
type: "hello",
factory: (options) => new HelloChannel(),
},
],
};
export default plugin;最小包结构:
weacpx-channel-hello/
├── package.json
├── tsconfig.json
└── src/
└── index.ts
在装有 weacpx 的环境里:
weacpx plugin add ./path/to/weacpx-channel-hello # 或者 npm 包名
weacpx plugin doctor
weacpx channel add hello
weacpx restart跑通这条链路后,再开始往 MessageChannelRuntime 里填业务逻辑。
推荐的目录与文件:
my-channel/
├── package.json # name, peerDependencies: { weacpx: ">=0.3.x" }
├── tsconfig.json # extends weacpx 顶层 tsconfig(一方包)或独立配置
├── README.md
├── src/
│ ├── index.ts # default export WeacpxPlugin
│ ├── channel.ts # implements MessageChannelRuntime
│ ├── cli-provider.ts # implements ChannelCliProvider(可选但强推)
│ ├── config.ts # 解析 / 校验 options
│ └── ... # 网关、签名、消息编解码等
└── dist/ # 发布产物(src 不发布)
├── index.js
└── index.d.ts
package.json 关键字段:
weacpx 声明为 peer,且 optional:开发时本地装一份,用户运行时由 weacpx 主体提供。所有 import 必须从 weacpx/plugin-api 走,禁止从 weacpx/dist/* 或 src/* 取符号——那些是内部实现,不属于稳定 API 表面。
import type { WeacpxPlugin } from "weacpx/plugin-api";
import {
WEACPX_PLUGIN_API_VERSION,
WEACPX_PLUGIN_MIN_CORE_VERSION,
} from "weacpx/plugin-api";
export interface WeacpxPlugin {
apiVersion: 1;
name?: string;
minWeacpxVersion?: string;
compatibleWeacpxVersions?: string;
channels?: ChannelPluginDefinition[];
}
const plugin: WeacpxPlugin = {
apiVersion: WEACPX_PLUGIN_API_VERSION, // 当前固定为 1
minWeacpxVersion: WEACPX_PLUGIN_MIN_CORE_VERSION, // 例如 "0.3.3"
channels: [/* ... */],
};
export default plugin;| 字段 | 必填 | 说明 |
|---|---|---|
apiVersion |
是 | 当前必须是字面量 1。weacpx 后续 breaking change 会升 API 版本。从 WEACPX_PLUGIN_API_SUPPORTED_VERSIONS 可读到当前 weacpx 接受的版本集合。 |
name |
否 | 显式声明插件名。如果填了,必须等于安装时的 npm 包名(含 scope),否则启动校验会拒绝。 |
minWeacpxVersion |
推荐 | 该插件能正常工作的 weacpx 核心最小版本(如 "0.3.3")。当前 weacpx 低于这个版本时,插件加载会失败并提示 upgrade weacpx。第一方插件必须声明;第三方插件强烈建议声明。 |
compatibleWeacpxVersions |
否 | 显式 weacpx 兼容范围;支持 x.y.z / >=x.y.z / ^x.y.z。和 minWeacpxVersion 同时声明则两者都需满足。 |
channels |
否 | 频道定义列表。允许为空(保留给未来非频道扩展点)。 |
约束:
- 必须用默认导出(
export default plugin)。命名导出无效。 - 模块在 daemon 进程里只 import 一次;不要在顶层有副作用(计时器、全局监听器等)。
加载/校验时可能产生的兼容性错误及修复方向:
| 错误关键词 | 含义 | 用户应做的事 |
|---|---|---|
requires weacpx >=X.Y.Z; ... upgrade weacpx |
插件比当前 weacpx 新 | 升级 weacpx 到 ≥ 该版本,或换装与当前 weacpx 兼容的旧插件版本 |
apiVersion N; supported: ...; install a compatible plugin |
插件用的是 weacpx 不识别的 API 版本 | 升级或降级插件到与本地 weacpx 兼容的版本 |
invalid plugin metadata |
minWeacpxVersion / compatibleWeacpxVersions 字段非法 |
联系插件作者或检查发布元数据 |
weacpx plugin doctor 也会把这些错误以 ERROR <plugin>: ... 的形式打印出来,可以放在 CI 或发布流程里作为前置检查。
export interface ChannelPluginDefinition {
type: string;
factory: ChannelFactory;
cliProvider?: ChannelCliProvider;
}| 字段 | 必填 | 说明 |
|---|---|---|
type |
是 | 频道类型字符串,例如 "feishu"、"yuanbao"。同一进程内全局唯一。 |
factory |
是 | 工厂函数,daemon 启动时调用,用于实例化 MessageChannelRuntime。 |
cliProvider |
否 | weacpx channel add <type> 的解析与提示逻辑。不提供时用户必须手改 ~/.weacpx/config.json。强烈建议提供。 |
type 约束:
- 非空,且不能含
:(chatKey 用:分隔)。 - 不能与已注册类型重复(
weixin始终被内置占用)。 - 不能与
cliProvider.type不一致(如果声明了 cliProvider)。
export type ChannelFactory = (
options: Record<string, unknown> | undefined,
deps?: CreateChannelDeps,
) => MessageChannelRuntime;
export interface CreateChannelDeps {
mediaStore?: RuntimeMediaStore;
allowedMediaRoots?: string[];
}参数:
| 参数 | 含义 |
|---|---|
options |
channels[].options,由用户配置或 cliProvider.buildDefaultConfig 写入。未经类型校验,工厂内部要自己 parse。 |
deps.mediaStore |
weacpx 提供的临时媒体落盘工具。处理图片/文件附件时用。 |
deps.allowedMediaRoots |
已注册 workspace 的 cwd 集合。决定哪些目录允许把 agent 输出的本地文件作为出站附件。 |
工厂应当在这一步只做参数解析与状态初始化:不要打开网络连接、读外部 token。所有副作用留到 start()。这样可以让 doctor / dry-run 安全 import。
例:
factory: (options) => new MyChannel(options)
class MyChannel implements MessageChannelRuntime {
private readonly config: MyConfig;
constructor(options: Record<string, unknown> | undefined) {
this.config = parseMyConfig(options); // throw 不合法配置
}
// ...
}export interface MessageChannelRuntime {
id: string;
isLoggedIn(): boolean;
login(): Promise<string>;
logout(): void;
start(input: ChannelStartInput): Promise<void>;
createConsumerLock?(options?: ConsumerLockOptions): ConsumerLock;
configureOrchestration?(callbacks: OrchestrationDeliveryCallbacks): void;
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
}频道实例的唯一 id。weacpx 当前要求 id === type,因此一般写成 readonly id = "<type>"。日志里会用它来标记上下文。
同步、纯函数。返回当前是否拥有可用凭据。daemon 启动前会调用一次决定是否需要走 login()。
非交互式频道(OAuth、appKey/appSecret)通常返回一段提示信息:
async login(): Promise<string> {
if (this.isLoggedIn()) return "credentials configured";
throw new Error("Provide options.appKey and options.appSecret in channels[].options");
}交互式频道(微信扫码)才需要在这里执行二维码流程并阻塞到登录成功。如果你的频道永远不需要交互式登录,把 cliProvider.supportsLogin 设为 false。
释放凭据、断开持久连接、清空内存里的会话。必须可重入——daemon shutdown / 重新登录都会调到。
频道开始接收消息的入口。详情见 §5。
要求:
- 把消息推送给
input.agent.handle(chatKey, text)。 - 监听
input.abortSignal,收到 abort 后干净地停掉网关、关闭长连接、清队列。 - 调发送类操作前先用
input.quota预留配额(详见 §6)。 - 任何外发都通过
input.logger记录关键事件,方便用户用weacpx doctor --verbose/app.log排查。
start() 通常是个长运行 promise——返回时意味着你已经 wire 好回调,但消息循环可以是后台异步。
可选。如果你的频道需要整机互斥(同一个微信号不能被两个 weacpx 进程同时连),实现这个方法。详见 §9。
可选。daemon 在 wire 编排服务时调用,给你两个回调:markTaskNoticeDelivered 和 markTaskNoticeFailed。如果你的频道支持任务完成通知(notifyTaskCompletion),需要保存这两个回调,在送达成功 / 失败时调用以更新 orchestration 状态。详见 §8。
被编排服务调用,通知用户某个 worker 任务已完成。task.chatKey 是路由目标。如果送达成功,调用 markTaskNoticeDelivered(task.taskId, accountId);失败调用 markTaskNoticeFailed(task.taskId, errorText)。
实现要点:
- 如果
task.chatKey不属于你的频道(看 prefix 判断),直接返回,不要报错。daemon 会广播给所有频道。 - 内容生成可借用 weacpx 的
renderTaskCompletion(如果暴露了)或自己拼。 - 注意配额:
notifyTaskCompletion算 final 出站,建议先quota.reserveFinal(chatKey)。
任务心跳通知(默认 60s 一次)。语义同上,但受 progressHeartbeatSeconds 控制,且通常不预留 final 配额。如果你的频道不支持中间心跳,写空实现即可。
export interface CoordinatorMessageInput {
coordinatorSession: string;
chatKey: string;
accountId?: string;
replyContextToken?: string;
text: string;
}编排服务向 coordinator 会话所在频道发文本时调用。语义类似 notifyTaskCompletion 的简化版。replyContextToken 是回复上下文(飞书/yuanbao 用来 quote 父消息),可忽略。
export interface ChannelStartInput {
agent: ChatAgent;
abortSignal: AbortSignal;
quota: OutboundQuota;
logger: AppLogger;
}| 字段 | 用途 |
|---|---|
agent |
weacpx 路由器入口。你收到一条文本消息后,调 agent.handle(chatKey, text) 把它喂给命令路由。 |
abortSignal |
daemon shutdown 信号。监听 aborted 事件,停掉所有长连接和定时器。 |
quota |
出站速率/总量配额,详见下节。 |
logger |
结构化日志器,详见 §7。 |
ChatAgent 接口本身在内部,但通过 MessageChannelRuntime 的契约只要求你把入站文本 await agent.handle(chatKey, text) 即可。返回不带数据;agent 会在自己的回调链里调你的发送方法。
重要:你的频道要持有一份
agent/quota/logger引用直到logout()或abortSignal触发。start()返回后这些不会再传一次。
export interface OutboundQuota {
onInbound(chatKey: string): void;
reserveMidSegment(chatKey: string): boolean;
reserveFinal(chatKey: string): boolean;
finalRemaining(chatKey: string): number;
hasPendingFinal(chatKey: string): boolean;
drainPendingFinalUpToBudget(chatKey: string, available: number): PendingFinalChunk[];
prependPendingFinal(chatKey: string, chunks: PendingFinalChunk[]): void;
enqueuePendingFinal(chatKey: string, chunks: PendingFinalChunk[]): void;
clearPendingFinal(chatKey: string): void;
}来源:微信公众号 24 小时主动消息上限的抽象。其它频道(飞书、yuanbao)配额无限,但 weacpx 对所有频道用同一套门面,便于 orchestration 调度。
最常用的两个:
onInbound(chatKey):用户发了一条消息进来时调用。重置该 chatKey 的 24h 窗口。reserveFinal(chatKey):发"最终回复"前调用,返回true才能发;返回false表示配额耗尽,应该 enqueue 等下一个 inbound 触发后再发。
非微信频道一般可以直接:
async sendFinalText(chatKey: string, text: string) {
if (!this.quota?.reserveFinal(chatKey)) {
this.quota?.enqueuePendingFinal(chatKey, [{ text }]);
return;
}
await this.gateway.sendText(chatKey, text);
}详细语义见 src/weixin/messaging/quota-manager.ts 的注释。
AppLogger.info / .warn / .error 都是异步的,签名形如:
await logger.info(eventCode: string, message: string, fields?: Record<string, unknown>): Promise<void>约定:
eventCode用<channel>.<area>.<verb>风格,如"feishu.inbound.message"、"yuanbao.gateway.connected"。便于聚合查询。fields不要塞密钥/PII。appSecret、用户 token 必须显式过滤。- daemon 已经帮你打时间戳和 pid,不要重复。
日志最终落到 ~/.weacpx/runtime/app.log,并由 weacpx doctor --verbose 抓取。
export interface OrchestrationDeliveryCallbacks {
markTaskNoticeDelivered: (taskId: string, accountId: string) => Promise<void>;
markTaskNoticeFailed: (taskId: string, errorMessage: string) => Promise<void>;
}daemon 在 buildApp 阶段调用 configureOrchestration(callbacks) 把这两个函数交给你。意义:
- 当你成功把"任务完成通知"投递到 IM 平台后,调
markTaskNoticeDelivered(taskId, accountId)。orchestration 服务会把 task 的noticeSentAt落盘,避免重启后重复投递。 - 投递失败(接口报错、配额超限),调
markTaskNoticeFailed(taskId, errorMessage)。orchestration 会在下一次 inbound / 重启后 replay。
未实现 configureOrchestration 的频道,所有 task 通知都会被认为"未送达",可能导致重复投递。如果你的频道支持 notifyTaskCompletion,强烈建议同时实现 configureOrchestration。
export interface ConsumerLockMetadata {
pid: number;
mode: "foreground" | "daemon";
startedAt: string;
configPath: string;
statePath: string;
hostname?: string;
}
export interface ConsumerLock {
acquire(meta: ConsumerLockMetadata): Promise<void>;
release(): Promise<void>;
}
export interface ConsumerLockOptions {
lockFilePath?: string;
onDiagnostic?: (event: string, context: Record<string, string | number | boolean | undefined>) => void | Promise<void>;
}什么时候要实现:你的频道用单点凭据连接到一个长会话网关,多个 weacpx 进程同时连会被对端踢下线(典型:微信 web 协议)。
不需要实现的情况:纯 HTTP webhook、有独立 bot id 的应用(飞书自建应用、yuanbao 多 bot)。
实现要点:
- 用文件锁(
proper-lockfile/ 自家 fcntl)做物理互斥。 acquire失败时抛带元信息的错(参考ActiveWeixinConsumerLockError),让 daemon 能在日志里告诉用户"另一个进程持有锁,pid=xxx"。release必须幂等。
参考实现:src/weixin/monitor/consumer-lock.ts。
export interface ChannelCliProvider {
type: string;
displayName: string;
supportsLogin: boolean;
parseAddArgs(args: string[]): ChannelCliParseResult;
buildDefaultConfig(input: ChannelCliInput): ChannelRuntimeConfig;
validateConfig(config: ChannelRuntimeConfig): ChannelCliValidationIssue[];
renderSummary(config: ChannelRuntimeConfig): string[];
promptForMissingFields(input: ChannelCliInput, io: ChannelCliIo): Promise<ChannelCliInput>;
}weacpx channel add <type> 的全部行为由 cliProvider 决定。每个方法的契约:
type必须等于ChannelPluginDefinition.type。displayName用于交互式提示,例如"Feishu"。
true:需要weacpx login走交互式凭据获取(目前仅微信)。false:所有凭据通过channels[].options配置。
把 weacpx channel add feishu --app-id x --app-secret y 中 --app-id x --app-secret y 这一串解析成 ChannelCliInput(key/value 字典)。返回:
| { ok: true; input: ChannelCliInput }
| { ok: false; message: string } // 用于直接打到 stderr要求:
- 未识别的 flag 立刻
{ok: false}。 - 布尔类 flag 用
parseBooleanFlag(value, flagName)(参考 yuanbao-provider 写法)。 - 不要在这里 throw —— 错误必须用
ok:false报。
把 ChannelCliInput(已含交互补全的字段)转成 ~/.weacpx/config.json 写入用的 ChannelRuntimeConfig:
{
id: "feishu",
type: "feishu",
enabled: true,
options: { appId: "...", appSecret: "...", domain: "feishu", requireMention: true }
}注意:id 必须等于 type(多实例当前未支持)。
不抛错,返回 issues 数组。两类:
| { kind: "missing-required-field"; flag: string; message: string }
| { kind: "invalid-config"; message: string }missing-required-field.flag 是缺哪个 CLI flag(如 "--app-id"),CLI 会用它提示用户该补什么。
返回展示用的多行字符串,比如:
type: feishu
appId: cli_xxx
appSecret: *** ← 必须脱敏
domain: feishu
requireMention: true
weacpx channel show <type> 会调用它。密钥字段必须显示成 *** 或省略后缀,不要原样输出。
只在 io.isInteractive() 为真时被调到。利用 io.promptText / io.promptSecret 把缺失字段补全。promptSecret 不会回显,用于密钥。
export type ChannelCliInput = Record<string, string | boolean | undefined>;
export interface ChannelCliIo {
print: (line: string) => void;
stderr: (text: string) => void;
isInteractive: () => boolean;
promptText: (message: string) => Promise<string>;
promptSecret: (message: string) => Promise<string>;
}parseBooleanFlag(value, flagName) 和 takeFlagValue(args, index, flagName) 这两个常用解析工具暂时没有在 weacpx/plugin-api 里以运行时形式导出。一方包 @ganglion/weacpx-channel-yuanbao / @ganglion/weacpx-channel-feishu 都各自复制了一份私有实现——参考 packages/channel-yuanbao/src/yuanbao-provider.ts 顶部 10 行直接抄。
export interface ChannelRuntimeConfig {
id: string;
type: string;
enabled: boolean;
options?: Record<string, unknown>;
}约束:
id === type(多实例未来才支持)。enabled: false的频道不会被 daemon 实例化,但仍出现在weacpx channel list。options任意 JSON 对象,由你的factory解析。建议在频道包里专门写一个parseMyConfig(options): MyConfig函数,先 throw 给出可读错误,再让构造函数信任结果。
~/.weacpx/config.json 顶层结构:
{
"plugins": [
{ "name": "@scope/weacpx-channel-my", "version": "0.1.0", "enabled": true }
],
"channels": [
{ "id": "weixin", "type": "weixin", "enabled": true },
{
"id": "my",
"type": "my",
"enabled": true,
"options": { "appKey": "...", "appSecret": "..." }
}
]
}chatKey 是 weacpx 路由里的会话标识,跨频道全局唯一。约定:
<channelId>:<channel-internal-id>
例:
- 微信:
weixin:wxid_abc123(注意微信兼容旧格式wxid_abc123,等价于weixin:wxid_abc123) - 飞书:
feishu:oc_xxxx - 元宝:
yuanbao:<account>:<conv>
你的频道必须:
- 入站消息时构造
<type>:<...>形式的 chatKey 并传给agent.handle(chatKey, text)。 - 出站消息时从 chatKey 反向解析回内部 id,注意 strip 掉
<type>:前缀。 notifyTaskCompletion等回调里检查task.chatKey是否以<type>:开头,不是就直接返回。
channelId 不能含 :。registerChannelFactory 会强制校验,未通过的会在 daemon 启动时报错。
daemon 在 import 插件后做以下检查(src/plugins/validate-plugin.ts)。任意一条失败会拒绝注册并打印 actionable 错误:
| 检查项 | 失败动作 |
|---|---|
apiVersion === 1 |
报 unsupported plugin apiVersion |
name(如有)必须等于 npm 包名 |
报 plugin name does not match package name |
每个 channel 的 type 非空、不含 : |
报 channel type must be non-empty / must not contain ":" |
单个插件内 type 不重复 |
报 plugin registers duplicate channel type |
同一进程里 type 不被多个插件同时注册 |
报 channel type ... is already provided by ... |
不允许覆盖内置类型 (weixin) |
报 channel type is already registered: weixin |
CLI 不会自动 disable 出错的插件——需要用户手工 weacpx plugin disable <name> 或修复后 weacpx plugin doctor。
weacpx plugin doctor 的输出由 src/plugins/plugin-doctor.ts 产出。常见 issue 及含义:
level |
message 模式 |
含义 / 用户该做什么 |
|---|---|---|
error |
package not installed in plugin home; run weacpx plugin add <name> |
配置里写了 plugin,但 ~/.weacpx/plugins/node_modules 里没装。重装。 |
error |
failed to import plugin: ... |
npm 包能装上但 import 报错。看错误里堆栈,多半是依赖版本冲突或缺 dist。 |
error |
unsupported plugin apiVersion 等 |
校验失败。看 §14。 |
error |
channel type X is already provided by ... |
两个 plugin 同时声明同一 type。卸载其中一个。 |
error |
channel X is configured but no enabled plugin provides it |
channels[] 有 X 但没有相应插件 enabled。weacpx plugin add 或 weacpx plugin enable。 |
warn |
plugin is installed and valid but disabled; run weacpx plugin enable |
装好了但 enabled: false。 |
error |
channel X is configured but provider plugin is disabled |
频道已配但提供方插件被禁用——daemon 启动会失败。plugin enable 或 channel disable。 |
ok |
plugin is installed and valid; channels: ... |
健康。 |
写插件时可以借这个表反推:保证你的插件能稳定走到 ok,再进入 weacpx restart。
| 阶段 | 命令 | 副作用 |
|---|---|---|
| 安装 | weacpx plugin add <pkg> [--version <v>] |
bun add / npm install 到 ~/.weacpx/plugins,import + validate,写 plugins[] |
| 升级 | weacpx plugin update <pkg> [--version <v>] weacpx plugin update --all |
重新 install 同名包,再 import + validate;--version 时同步写回 plugins[].version |
| 校验 | weacpx plugin doctor [<pkg>] |
不修改任何状态,只汇报每个插件 / 每个频道的健康状态 |
| 停用 | weacpx plugin disable <pkg> |
仅把 plugins[].enabled = false,不卸包 |
| 重启 | weacpx plugin enable <pkg> |
enabled = true |
| 卸载 | weacpx plugin remove <pkg> (rm 别名) |
卸 npm 包 + 从 plugins[] 移除(不会自动 channel rm) |
| 频道 | weacpx channel add/rm/enable/disable/show/list <type> |
改 channels[];走插件提供的 cliProvider(如有) |
| 生效 | weacpx restart |
daemon 重新 import 所有 enabled 插件 |
每条插件命令都接受 --restart / --no-restart,默认在交互式终端里询问。详见 docs/channel-management.md#插件管理。
1. main() 启动
2. 读 ~/.weacpx/config.json
3. plugin-loader 遍历 plugins[].enabled === true:
3.1 import("<plugin-home>/node_modules/<name>")
3.2 validateWeacpxPlugin
3.3 registerChannelPlugin —— 注入 factory + cliProvider
4. createMessageChannels 遍历 channels[].enabled === true:
4.1 channelFactories.get(type)
4.2 factory(options, deps) → MessageChannelRuntime
5. runConsole(...):
5.1 channel.configureOrchestration?.(callbacks)
5.2 consumer lock acquire(可选)
5.3 channel.start({ agent, abortSignal, quota, logger })
6. 收消息:channel → agent.handle(chatKey, text) → router
7. 出消息:orchestration → channel.notifyTaskCompletion / sendCoordinatorMessage
8. SIGTERM / SIGINT:abortSignal aborted → channel 自己 cleanup → channel.stopAll? → daemon exit
logout() 只在 weacpx logout 显式调用时被触发;正常退出走 abortSignal。
- daemon 在 §16.2 第 3 步把每个插件
import()一次,模块对象在 daemon 进程生命周期内被缓存。weacpx plugin update只改磁盘,不会让运行中的 daemon 看到新代码。 - 因此 update 后必须
weacpx restart。CLI 默认会问;写脚本时建议显式--restart。 - 反过来,
weacpx plugin add/update/remove这些 CLI 命令自己走的是一个独立短生命周期 Node 进程,校验时跑的import()用的是磁盘新版本。所以"装好但没重启"的窗口期里:CLI 校验通过 ≠ daemon 也加载了新版本。这是weacpx plugin doctor始终报"装好了"但 daemon 表现不变的根因。
| 失败点 | 表现 | 行动 |
|---|---|---|
plugin add 时 import 失败 |
CLI 立即报错,不写 config | 修包,或换版本 --version 重试 |
plugin add 时 validate 失败(apiVersion 不匹配、name 与包名不一致、单插件内 type 重复、factory 缺失等) |
CLI 立即报错,不写 config | 看错误,修包元信息 |
plugin add 时跨插件 type 冲突 |
不会在 add 阶段发现,只能在 plugin doctor 或 daemon 启动时发现 |
装完跑 weacpx plugin doctor 复核 |
plugin update 时 import / validate 失败 |
CLI 报错;如果原来 plugins[].version 有值,会自动 npm install 回滚到该版本;否则提示用户手动重装 |
看错误,必要时手动 weacpx plugin add <pkg> 回到 latest |
| daemon 启动时插件 import 失败 | daemon 进程退出,错误进 ~/.weacpx/runtime/app.log |
weacpx plugin doctor 看 ERROR 行;常见手段是 plugin disable <name> 暂时绕开 |
channel add 时 cliProvider validate 失败 |
CLI 报缺哪个字段 | 按提示补全 |
一方包路径:packages/channel-<type>/,发布名 @ganglion/weacpx-channel-<type>。第三方可任意命名,但若设了 WeacpxPlugin.name,必须等于 npm 包名。
weacpx plugin known 只列举随当前 weacpx 版本一起发布的官方频道插件(src/plugins/known-plugins.ts):
官方插件:
- feishu @ganglion/weacpx-channel-feishu 飞书频道
- yuanbao @ganglion/weacpx-channel-yuanbao 腾讯元宝频道
安装:
weacpx plugin add <package>
第三方插件的发现走 npm 自身(npm search / GitHub / README),不会出现在 plugin known 里。weacpx 不做 marketplace、不做 npm 索引、不做自动安装;用户只需要:
weacpx plugin add <你的-npm-包名>如果你写了一个第三方频道插件,建议在自己仓库 README 里直接给出这个 plugin add 命令,而不是依赖 weacpx 去做发现。
发布前检查:
dist/含.js和.d.ts。package.json的peerDependencies.weacpx用>=x.y而非^x.y,避免锁死小版本。peerDependenciesMeta.weacpx.optional = true,否则用户安装时 npm 可能在~/.weacpx/plugins里要求装一份 weacpx,浪费空间。- 发布产物里只导入
weacpx/plugin-api。可以用bunx publint验证。 - 全 ESM,
"type": "module"。
发布命令、preflight、dry-run、版本号选取(patch / minor / major)见 docs/release.md。
最少应有:
- 单元层(不依赖 weacpx):parse / validate config 函数、消息编解码、签名算法、chatKey 构造与解析。
- CLI provider 单元测试:用
parseAddArgs喂各种参数组合,断言ChannelCliInput;用validateConfig喂故意缺字段的 config,断言 issues。 - 频道契约测试:实例化
MyChannel(options),注入 fakeChannelStartInput(自己写一个OutboundQuota/AppLoggermock),断言一次入站消息会走到 fakeagent.handle。 - 集成层(可选):在测试里跑
runCli(["channel", "add", "<type>", ...]),断言~/.weacpx/config.json被正确写入。
参考 packages/channel-yuanbao/src/access/__tests__(如有)和 tests/unit/channels/*。
| 包 | 路径 | 看什么 |
|---|---|---|
@ganglion/weacpx-channel-feishu |
packages/channel-feishu/ |
标准 OAuth2 / 自建应用、HTTP webhook、@ 提及、群单聊路由 |
@ganglion/weacpx-channel-yuanbao |
packages/channel-yuanbao/ |
长连 WebSocket、自定义签名、消息去重、心跳通知 |
内置 weixin |
src/channels/weixin-channel.ts |
唯一一个走 supportsLogin: true + ConsumerLock 的频道 |
每个一方包都有 src/index.ts(plugin 入口)+ src/channel.ts(runtime)+ src/<type>-provider.ts(CLI provider),对照看最快。
- 用户向频道管理:docs/channel-management.md
- 配置文件全字段:docs/config-reference.md
- 发布 / 版本流程:docs/release.md
- Code wiki / 模块地图:docs/code-wiki.md
{ "name": "@scope/weacpx-channel-my", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "files": ["dist", "README.md"], "peerDependencies": { "weacpx": ">=0.3.3" }, "peerDependenciesMeta": { "weacpx": { "optional": true } } }