git:20261010.94c871f to git:20261011.65f9b8c

19 added, 4 removed. Audit A to A.

---
name: movieclaw-plugin-dev
description: 用户想让 MovieClaw 拥有它现在没有的能力或对接时使用,不管用户有没有说「插件」。典型说法:「做一个企业微信 / 钉钉 / Slack / Bark 通道」「接入某某平台」「能不能支持某某站点」「下载完 / 入库后 / 删片后自动做某事」「按订阅规则组表达不了的规则淘汰种子」「定时去拉某个片单」「给 AI 助手加个查某某的能力」「给外部系统开个接口」。这类需求先判断现有功能能否做到,做不到的靠写插件(.mcplugin)实现;本技能覆盖从判断、开发、打包、安装、验证到卸载的全过程,也用于升级、回滚、排查已装的插件,以及回答「插件是什么、要批准什么、安不安全」这类问题。
---
# MovieClaw 插件开发
你要帮用户写一个 MovieClaw 插件,并在这台服务器上把它装起来、验证、必要时卸载。
插件是一个目录:一份清单 `movieclaw-plugin.toml` + Python 代码,打包成 `.mcplugin`(zip)后安装。
装上后默认在**独立的低权限进程**里运行,崩溃只伤它自己;装、升级、回滚、卸载都**不用重启**。
本文件是判断、流程与规范,**读完再动手**;第 2 节告诉你动手前还必须读哪份参考(路径以本技能目录为锚)。
## 0. 先判断:要不要写插件、做不做得到
**先分流**:
- 用户只是问插件是什么、要批准什么、安不安全、怎么卸载 → 读 `references/explaining-to-users.md` 照着答,不走开发流程。
+ - 用户要停用 / 启用 / 卸载 / 恢复某个插件(含官方插件)→ 第 3 节第 12 步,不走开发流程。
- 用户只是想「做一次某件事」(建订阅、删种子、扫媒体库)→ 直接用 `mclaw` 工具,不要写插件。
- 想要的效果**已有功能或设置**能做到 → 用现有的,不要写插件。先按下表查;查到「已有但只覆盖一部分」时,
先讲清已有的能做到哪、剩下的部分能不能用插件补。
- 需要**持续自动**地响应系统里发生的事、改变系统的决策、给系统加一种新实现,或开一个接口(给 AI 助手或外部系统用)→ 写插件。
确认系统里确实没有之后,不要只回答「目前不支持」:对照下面的能力地图和「做不到的」,能做就按本技能走,做不到就直说。
**先查系统里有没有**(都用 mclaw 工具):
| 需求 | 查什么 |
|---|---|
| 通道、消息推送 | `channels list`(输出很长,只看各项 `id` / `title`);已经会推哪些事件:`channels im push config get` |
| 下载器 | `dl list` 看已接入的;支持哪些类型看 `dl add --help`(只有 qBittorrent、Transmission) |
| 订阅只要 / 不要某些资源(制作组、分辨率、HDR、字幕、体积、免费) | `rules update --help`:订阅规则组都能设(如制作组白名单),规则组表达不了的才用钩子 |
| 站点 | `site list`、`site --help` |
| 播放器跳过片头、片尾、广告、预告 | 内置:剧集库的片段识别,`library update --help` 的 `--detect-media-segments` |
| 首页显示什么 | `ui prefs show`:首页行可以选某个库、合集或类型,每个库都有按入库时间排的「最近添加」 |
| 某件事发生后是否已有插件在处理 | `app plugins list -o table` 的 `durable` 一行看消费者 |
| 有没有装过某个插件 | `app plugins packages list` |
**能力地图**(逐项清单见 `references/extension-points.md`):
| 插件能做 | 靠什么 |
|---|---|
| 某件事发生后自动做点什么:下载完成、入库完成、删片 / 删文件 / 回收站、订阅新建 / 删除 / 开始下载 / 收齐 / 状态变化 | 可靠事件 |
| 改变系统的判断:订阅搜索词、淘汰或重排候选种子、选下载器、否决删种 | 决策钩子 |
| 加一种新实现:IM 通道(含只推送的,如 Bark)、站点类 / 站点数据包、定时任务(可整段替换内置的)、后台任务、入库流水线步骤 | 注册表 |
| 给 AI 助手加一个能力(查天气、查某个外部服务)、给外部系统开接口 | 插件接口:自动成为 mclaw 命令,AI 助手直接能调 |
| 接收外部平台推过来的消息(企业微信自建应用回调、GitHub / Slack 推送、Telegram Webhook) | 回调端点:宿主发地址,请求原样交给插件,插件自己验签 |
| 做事与存东西:调用系统操作(与 mclaw 同一份)、存状态、读写批准的目录、报告健康 | 服务 |
**做不到的**:给网页 / App 加界面、按钮或设置页(包括播放器里的按钮、首页卡片)、新的下载器类型、新的元数据来源、
新的 Webhook 格式、新的站点认证方式、替换播放 / 转码 / 媒体库扫描 / 刮削主流程、改 AI 助手本身。
**遇到要直说做不到**,并给替代办法:先给用户能直接用的现成功能(上表),再给插件能做的部分
(`references/extension-points.md` 第 5 节),不要先答应再做到一半。
**用户问「能不能……」「可以做……吗」时**,先给方案,不要直接动手:能不能做、怎么做、要申请什么权限、
用户要配合准备什么(账号、凭据、先接入的下载器 / 站点 / 媒体库等前置条件——先用 mclaw 查缺哪些)。
有要用户选的(接入方式、取舍、阈值)或前置条件不满足 → 到这里结束这一轮,等用户回答。
需求明确、只有一种合理做法时,可以先写好并检查、自测,再问要不要安装。
## 1. 原理速览
| 概念 | 一句话 |
|---|---|
| 插件 | 一个 `@plugin("<id>", ...)` 装饰的 `async def apply(ctx)`。`apply` 只做**登记**(监听事件、贡献注册表、挂路由、起后台任务),登记完就返回 |
| 条目 id | 全局唯一;小写字母开头,只用小写字母、数字和连字符,3~40 位,不含点,如 `hello-world`。清单的 `id` 必须与 `@plugin` 的名字**完全一致** |
| 契约 | 插件只能用**开放契约**:服务(`ctx.use`)、注册表(`ctx.contribute`)、事件与决策钩子(`ctx.on`)。清单见 `python <本技能>/scripts/contracts.py` |
| 可撤销 | 经 `ctx` 登记的一切在卸载时自动撤销;自己开的资源用 `ctx.effect(释放函数)` 登记,后台协程一律用 `ctx.task(...)` |
| 宿主操作 | 插件「做事」= 调用和 `mclaw` 同一份操作目录(如 `subscriptions.create`),清单 `permissions.operations` 申请,用户批准后才有 |
| 运行位置 | `runtime = "process"`(默认、推荐)独立进程;`inline` 在主进程里运行,需用户单独批准,除非用户明确要求否则不用 |
| 热插拔 | 插件包安装 / 升级 / 回滚 / 卸载都在运行中完成;新版本起不来时服务器**自动回到上一版** |
**组合与拆分**:一个插件可以同时用多个钩子、事件、注册表、路由;一个插件包只加载一个插件(名字 = 清单 id),
同生同死。一个插件 = 一件用户能说清楚的事;互不相关、权限差得多、稳定性差得多的事拆成多个插件。
替换内置东西:同 id 的插件包替换随带通道;注册表里 `override=True` 加原 id 替换某一项。详见 `references/composition.md`。
## 2. 动手前必须先读
源码根目录(下文记作 `$SRC`)= 本技能目录往上三级;或运行
`python -c "import movieclaw_sdk, pathlib; print(pathlib.Path(movieclaw_sdk.__file__).parents[1])"`。
第 0 节给方案时只需要 `references/extension-points.md`;下表是**动手写代码前**要读的。
示例插件都在本技能的 `references/examples/`(只有这一处,仓库和容器里没有别的副本)。它们是插件体系的验收插件,
CI 会把它们真实装进应用、跑完用户场景,写法可以照抄。示例大多是「本地插件」写法(`data/plugins.yaml`、从 `movieclaw_kernel` 导入),
做插件包时以 `templates/starter/` 为准,差别见 `references/examples/README.md`。
| 你要做的 | 先读(读完再写代码) |
|---|---|
| 任何插件 | `references/extension-points.md`(确认要用的契约存在、没落在「做不到」里)、`templates/starter/` |
| 事件 / 钩子类(删片联动、关键字规则、选下载器) | `references/recipes.md` 第 1~3 节,对应示例 `references/examples/{delete_cascade,keyword_rules}.py` |
| IM 通道 | `references/recipes.md` 第 11 节(**选接入方式**)、`references/examples/ntfy-channel/`、`$SRC/movieclaw_sdk/channels.py` 文件头;只推送的通道再看 `$SRC/movieclaw_plugins/feishu/`;靠平台回调收消息的看 `references/examples/wecom-channel/` |
| 定时 / 后台任务、入库流水线 | `references/recipes.md` 第 6~7 节、`references/examples/{watchlist_feed,cloud_strm}.py` |
+ | 用户正要用的先准备好(开始播放预取下一集、收藏 / 打开详情先处理、新文件入库后慢慢补) | `references/recipes.md` 第 7、16 节(实时事件 + 插件自己建后台任务),完整示例 `$SRC/movieclaw_plugins/strm_prefetch/` |
+ | 驱动 AI 助手(把外部协议 / 平台的请求转成一轮对话、自动化) | `references/recipes.md` 第 15 节(**先定对端怎么进来**:只聊天用公开区签名链接,要操作系统才用管理员区 + 令牌;先 `mclaw llm providers list` 确认接了模型);非 Python 写法看 `references/examples/go-a2a-bridge/` |
| 接收外部平台的回调 | `references/recipes.md` 第 14 节 |
| 开接口(含给 AI 助手加能力)、读写文件 | `references/recipes.md` 第 5、8、10 节、`templates/starter/`、`references/examples/cloud_strm.py` |
| 插件需要用户填的配置或凭据(账号、地址、Key) | `references/recipes.md` 第 13 节 |
| 站点 | `references/recipes.md` 第 12 节、`references/examples/site_pack/` |
| 一个插件里放好几件事、替换内置的东西 | `references/composition.md`(替换随带通道:把 `$SRC/movieclaw_plugins/<名>/` 复制到 `plugins/channel.<名>/` 再改) |
| 写清单、申请权限 | `references/manifest.md` |
| 用户问「插件是什么、为什么要批准、会不会搞坏」 | `references/explaining-to-users.md` |
| 装不上、起不来、没反应 | `references/troubleshooting.md` |
| 想看系统自己怎么做:内置插件、各模块、某个事件 / 钩子在哪被调用、字段实际填什么 | `references/architecture.md` 第 7 节(源码地图) |
## 3. 开发流程
工作目录就是 bash / mclaw 的当前目录。每个插件放在工作目录的 `plugins/<条目 id>/` 下。
**不要 `cd` 进技能目录或 `$SRC`**:脚本一律用绝对路径调用(`python <技能目录绝对路径>/scripts/xxx.py …`),
插件路径写相对工作目录的 `plugins/<id>`,否则插件会被生成进主程序源码里。
**mclaw 命令一律用 mclaw 工具执行**:bash 里没有登录令牌,在 bash 里跑 `mclaw …` 只会报未授权,
它的输出(包括配合 grep 得到的计数)不能当作验证结果。
1. **澄清需求**:触发时机(什么事发生时)、要做什么、影响哪些数据、要不要配置。说清楚你打算用哪种扩展形态
(`references/extension-points.md`),做不到的直说并给替代办法;有歧义先问。
**接外部平台(IM 通道、通知、外部服务)时**:MovieClaw 多半跑在家里的 NAS 上、没有公网地址,平台推不进来。
首选服务器主动连出去的方式(长轮询、长连接——如企业微信「智能机器人」长连接模式,只要 botId + secret),
其次是只推送的群机器人 Webhook,需要公网回调地址的方式放最后;给用户的方案里讲清「能不能对话、要不要公网地址、
绑定要填什么」。细节见 `references/recipes.md` 第 11 节。**只做用户要求的**:觉得还该多做点什么
(多加几个关键词、多记几个字段),作为建议提出来,用户同意再加。
2. **查契约**:`python <本技能>/scripts/contracts.py` 列出全部开放契约;`python <本技能>/scripts/contracts.py <名字>`
看某个事件 / 钩子的载荷与返回结构。只能用列出来的契约。要调用的宿主操作用 `mclaw <域> --help` 找,
操作 id 与命令一一对应(`mclaw subscriptions create` ↔ `subscriptions.create`)。
3. **生成骨架**:
`python <本技能>/scripts/new_plugin.py plugins/<id> --id <id> --title "<中文标题>"`
生成清单与入口模块,再按需求改。
4. **写代码**:遵守第 4 节规范。
5. **本地检查**:`python <本技能>/scripts/check_plugin.py plugins/<id>`。它用服务器同一套逻辑校验清单、契约、
宿主操作,并真的导入入口模块找 `@plugin`。**有 ✗ 必须先修好**;⚠ 要逐条判断。
**再自测判断逻辑**:把「该不该处理 / 怎么处理」写成模块顶层的纯函数(如 `verdict(title) -> 原因 | None`),
在 bash 里 `cd plugins/<id> && python -c "from <模块> import verdict; assert …"` 跑几条**正例和反例**
(反例 = 看着像但不该命中的,如片名里恰好含这几个字母)。钩子在热路径上、误判会直接影响下载,这步不能省。
6. **征得用户同意再安装**(第 5 节)。问完就结束这一轮、等用户回答;同一轮里提问又接着安装,不算征得同意。
7. **安装并加载**:`mclaw` 工具执行 `plugin dev plugins/<id> --once`。它会打包(版本自动加 `-dev.<时间戳>`)、
上传、按清单申请批准、当场加载,成功打印 `✓ … 已加载`。**必须带 `--once`**,否则它会一直监视文件、卡到超时。
8. **验证**(不验证不算完成):
- `mclaw app plugins packages list`:插件在 `installed` 里,版本对;
- `mclaw app plugins list`:找到条目 id(`id` 字段),`state` 应为 `active`,失败时看 `error`。
这条输出很长(全部内置条目),会被截断;截断提示里给了完整输出的文件路径,用 bash 筛:
`python -c "import json; d=json.load(open('<文件>')); [print(e['id'], e['state'], e['source'], e.get('error')) for e in d['plugins'] if e['source'] == 'package']"`
(第三方插件包 `source` 是 `package`,本地插件是 `local`;随带的通道插件 `tier` 是 `official`)。
也可以直接 `grep -n -A8 '"<条目 id>"' <文件>`;
- 调插件自己的接口:插件路由会进入 mclaw 的命令目录。装好后先随便执行一条 mclaw 业务命令(如上面的
`app plugins list`),看到「服务器接口目录已更新」后,下一次调用起就能用了:路由的 `operation_id`
按点拆成命令,`plugins.import-log.recent` → `mclaw plugins import-log recent`(参数见 `--help`);
- 通道插件:`channels list` 里出现新通道,它的 id 是 `<条目 id>:<贡献 id>`(如 `bark-channel:bark`;替换随带通道时沿用原 id);
绑定和试推送的命令看 `channels bindings start --help`、`channels im push --help`,凭据由用户提供;
- 触发它监听的事件并观察效果。事件不能安全地人为制造时(入库、删片这类会动真实数据的),
**不要为了验证去改动用户的数据**:确认 `app plugins list` 里它的监听器已挂上(可靠事件会显示消费者),
并告诉用户「下次真实发生时会怎样、去哪里看结果」;
- 日志:用 bash 在运行日志目录(系统提示词「环境」里给了路径)的当天文件里找条目 id,
如 `grep -n '<条目 id>' <日志目录>/movieclaw-$(date +%F).log | tail -50`;进程外插件的日志也汇总在这里。
9. **迭代**:改代码后重复 5 → 7,每次自动是新的开发版本。
**装不上时**:先按报错原文查 `references/troubleshooting.md`。同一个现象连续两次没装上,就停下来向用户报告
(报错原文、你的判断、建议的下一步),不要去翻宿主源码、模拟宿主的加载过程。
10. **正式安装**(用户要长期使用时):清单里定好正式 `version`,然后
`plugin pack plugins/<id>` → `app plugins packages upload --file <生成的 .mcplugin>` →
`app plugins packages approve <id> --version <版本> --operations-json '<与清单一致>' --paths-json '<与清单一致>' --yes`。
11. **卸载**:`app plugins packages uninstall <id> --yes`(默认保留插件数据;用户要求清干净才加 `--purge-data`)。卸载后再查一次 `app plugins list`,确认条目已消失;替换内置插件的包卸载后内置版本自动回来。
回到上一版:`app plugins packages rollback <id> --yes`。
+ 12. **停用 / 官方插件**:插件包与官方插件(随应用提供的,如通道、下载器、网盘规格预探)都能临时停用:
+ `app plugins switch <id> --enabled=false`(启用:`--enabled=true`;布尔参数必须带 `=`),运行中生效、重启后保持,
+ **停用会取消它排着的后台任务**(插件任务的类型以 `<id>:` 开头,停用前可用 `jobs list --active-only` 看有没有在跑的)。
+ 条目 id 从 `app plugins list` 的完整输出里筛:官方插件 `tier` 是 `official`、可停用的 `switchable` 为真
+ (`python -c "import json; d=json.load(open('<文件>')); [print(e['id'], e['title'], e['state']) for e in d['plugins'] if e.get('switchable')]"`)。
+ 网页上的位置:「设置 → 插件」,官方插件在最上面一组,点进去「操作」里有停用 / 卸载。
+ 官方插件还能卸载:`app plugins bundled uninstall <id> --yes`(加 `--purge-data` 连数据删),随时
+ `app plugins bundled restore <id>` 装回;已卸载的在 `app plugins list` 的 `removed` 里。被 `plugins.yaml` / 环境变量
+ 关掉的插件开关是锁住的(`locked_by` 写明原因),要用户去那里改。
命令参数以 `mclaw <命令> --help` 为准,报错时先看 `--help` 再重试。排查:`references/troubleshooting.md`。
## 4. 硬性规范
**必须**
- 清单 `id` = `@plugin` 名字;`entry` 是模块名(`hello.py` 或 `hello/__init__.py` 的 `hello`)。
- - `inject=(...)` **只写服务**(`HOST_OPS`、`PLUGIN_DATA`、`PLUGIN_FILES`、`PLUGIN_ROUTES`、`PLUGIN_HEALTH`、`DURABLE_EVENTS`)。
+ - `inject=(...)` **只写服务**(`HOST_OPS`、`PLUGIN_DATA`、`PLUGIN_FILES`、`PLUGIN_ROUTES`、`PLUGIN_HEALTH`、`PLUGIN_JOBS`、
+ `AGENT`、`DURABLE_EVENTS`)。插件包代码优先从 SDK 导入(`movieclaw_sdk.ops` 的 `HOST_OPS`、`movieclaw_sdk.jobs`、
+ `movieclaw_sdk.agent` 的 `AGENT`、`movieclaw_sdk.library` 的实时事件)。
注册表(如 `IM_CHANNELS`、`SCHEDULED_TASKS`)在 `apply` 里 `ctx.contribute`,事件 / 钩子用 `ctx.on`,**都不写进 inject**;
- 用到的事件 / 钩子 / 注册表写进清单 `[requires]`(如 `"library.ingest.imported" = "^1.0"`);只用服务时 `[requires]` 可以留空。
+ 用到的事件 / 钩子 / 注册表写进清单 `[requires]`(如 `"library.ingest.imported" = "^1.0"`);服务写不写进 `[requires]` 都行(写了就锁定版本,如 `"plugin-jobs" = "^1.0"`);只用服务时 `[requires]` 可以留空。
- 要调用的宿主操作同时写进 `@plugin(permissions=...)` 和清单 `permissions.operations`;
危险操作(`mclaw` 帮助里带 ⚠ 的)必须逐个列出,不能靠 `领域.*` 覆盖。
- 可靠事件监听器必须有稳定 `id`(`ctx.on(EVENT, fn, id="xxx")`),并且**幂等**(至少投递一次,同一事件可能来两次):
按 `ctx.delivery.event_id` 去重(`recipes.md` 第 1 节),或者做的事本身重复执行也没影响
(删除类:对象已经不在了按成功处理,见 `examples/delete_cascade.py`)。
- 后台循环用 `ctx.task(coro, name=...)`,循环里捕获异常并记日志后继续,`CancelledError` 要原样抛出。
- 持久状态存插件数据(`PLUGIN_DATA`),读写文件走文件接口(`PLUGIN_FILES`)并在清单 `paths` 里申请。
- 连外网走用户的代理设置:`httpx.AsyncClient(transport=movieclaw_sdk.net.http_transport("<服务名>"))`,清单写 `network = true`。
- 主程序自带的库可以直接 import(如 `httpx`、`websockets`、`pydantic`、`fastapi`、`cryptography`、`parsel`、`pyyaml`);
别的依赖只能随包放进插件目录的 `vendor/`(纯 Python);安装时不联网、不跑 pip。
- 日志用 `ctx.logger`,中文,带上关键 id。
- 按关键词匹配种子 / 发布名时:英文短词(TC、TS、CAM 这类)必须按**词边界**匹配,
如 `re.compile(r"(?<![A-Za-z0-9])TC(?![A-Za-z0-9])", re.I)`,否则 `ts` 会命中 Hits、`cam` 会命中 Cameron;
中文词用子串匹配即可。规则列表用元组 / 列表(保持顺序),不要用集合。
**禁止**
- 在决策钩子里产生副作用(发请求、写数据、调宿主操作)——钩子只做判断;副作用放到可靠事件或宿主操作里。
- 用内部服务(`DB`、`SECRETS`、`SETTING_STORE` 等未出现在契约清单里的)或直接 import 主程序的仓储层改数据库——
第三方插件会被拒绝,进程外也拿不到。
- 在 `apply` 里做耗时工作(网络请求、全量扫描):`apply` 有 30 秒上限,耗时工作放进 `ctx.task`。
- 修改 `$SRC` 下任何文件、修改本技能目录。插件只写在工作目录的 `plugins/` 下。
- 自行改成 `runtime = "inline"` 或加 `--allow-inline`。开放的扩展点(含 IM 通道)在独立进程里全部能用;
报错看着像在说「只能在主进程里跑」,是你的代码有错(多半是 inject 写错),改代码,不要换运行方式。
- 重启、停止应用(`app restart` 等)或改服务器上的文件。插件的安装、升级、卸载都不需要重启;
服务器上可能有人在播放、下载,重启会打断他们,也会打断你自己。觉得只有重启能解决时,停下来告诉用户。
- 为了让插件装上而删减用户要的功能(如去掉回调、去掉某种消息)。要删减先说明原因、等用户同意。
- 使用 `plugin dev` 时不带 `--once`;对不是开发用的服务器反复安装开发版本。
## 5. 安全与确认
批准安装 = 把清单里申请的宿主操作和路径授予这段代码,并在服务器上运行它。所以:
- 安装前向用户列出:条目 id、要做什么、申请的宿主操作(标出危险操作)、路径、是否联网、运行方式。
**用户明确同意后才执行第 3 节第 7、10 步**。用户在本次对话里已经明确说过「直接装」的,视为同意。
- 申请最小权限:只列真正会调用的操作;能用演练(`dry_run`)验证的危险操作先演练。
- 不可恢复、又有取舍的行为(删种时连数据文件一起删会影响做种和分享率、覆盖文件、批量改订阅)不要替用户定默认值,
列出选项请用户选。
- 装好后还要用户给凭据才能用的(通道绑定、外部服务的 Key),在征求安装同意的同一轮里一起说明要准备什么。
- - 卸载、回滚、清除数据:用户已经明确要求的,直接执行并在结果里说明影响,不要再问一遍;
- 是你自己提议的,先说明影响、等用户同意。
+ - 停用、卸载、回滚、清除数据:用户已经明确要求的(「帮我关掉 / 不想用了 / 卸了它」),直接执行并在结果里说明影响,
+ 不要再问一遍;只是问「能不能关、怎么关」的,讲清办法和影响,问要不要现在做。是你自己提议的,先说明影响、等用户同意。
+ 「暂时不用」做停用(随时启用),「不要了」才卸载。
- 不要把用户的凭据写进插件代码或清单;凭据写进配置模型(`SecretStr`),用户在插件的「设置」里填(`references/recipes.md` 第 13 节)。
- **绝不设法获取或截获登录凭据**:不读 mclaw 的配置 / 环境变量里的令牌,不用 strace、抓包、本地代理截请求头,
不绕过鉴权直接 curl 需要登录的接口。mclaw 调不到的接口,就换上面的正规验证方式,或请用户在网页里打开确认。
## 6. 参考资料
| 文件 | 内容 |
|---|---|
| `references/extension-points.md` | 全部开放契约(用途、导入、示例)与「还没开放的 + 替代办法」 |
| `references/composition.md` | 一个插件组合多个扩展点、何时拆分、替换内置、插件之间配合 |
| `references/recipes.md` | 每种扩展点的写法片段(第 11 节含 IM 通道的接入方式选择) |
| `references/manifest.md` | 清单字段、权限、版本区间、安装 / 升级 / 回滚 / 卸载命令 |
| `references/architecture.md` | 原理:生命周期、进程外运行、权限模型、源码位置 |
| `references/explaining-to-users.md` | 给用户解释插件、权限、安全、卸载的标准说法 |
| `references/troubleshooting.md` | 按报错查原因 |
| `references/examples/` | 6 个经过测试的真实插件(先读 README) |
| `templates/starter/`、`scripts/` | 骨架;生成骨架、查契约、安装前检查的脚本 |