插件开发指南¶
本文是 BaseDataSource 规范的对外版(对外契约以本文为准);内部母版为仓库 doc/05-插件开发指南.md(含实时流式插件等内部扩展设计),两版需同步维护。新数据源上线四步:继承基类 → 放入 plugins/ → 后台审核 → 上线,全程不改框架代码、不重启服务、不升级设备固件。
1. 目录约定¶
plugins/
└── my_source/ # 目录名建议 = source_code(snake_case)
├── __init__.py # 必须:导出 DATASOURCE(类或实例)
├── fetcher.py # 可选:实现拆分
└── requirements.txt # 可选:插件私有依赖(部署侧安装,框架不自动装)
__init__.py必须暴露DATASOURCE(单版本)或DATASOURCES = [V1, V2](多版本并存);- 插件只能 import:标准库、平台放行的公共依赖(httpx / pydantic / aiomysql,见文末附录)、
core.plugins.base;禁止 import 框架其他模块、其他插件、全局单例; - 删除目录 = 移除插件(DB 注册行自动转“代码缺失”,禁止再上线)。
2. BaseDataSource 契约¶
| 声明属性 | 说明 |
|---|---|
source_code / version |
全局唯一,联合唯一 |
name / category |
展示名 / 分类(lottery、hotlist、weather…) |
cache_ttl / stale_ttl |
缓存 TTL 与降级窗口(秒,后台可覆盖) |
cache_scope |
global 全设备一份;params 按参数分缓存 |
cron |
5 段表达式(北京时间);仅 global 生效,用于定时预热 |
need_subscription |
是否需用户订阅后可见 |
source_name |
来源标注(合规必填,缺省审核不通过) |
params_schema |
JSON Schema draft-07 子集,网关据此校验 |
必须实现:
meta() -> dict:注册/文档元信息,含resp_example(返回示例,注册审核机器校验其存在且带source_name,并作为文档站数据源目录的维护基准);async fetch(params) -> dict:回源取数,返回 payload。失败时抛DataSourceError子类(推荐)或任意异常——框架统一按50401处理并尝试 stale 降级,插件异常永远不影响进程与其他请求。
可选钩子:health_check()(手动预热/健康检查)、on_enable(ctx) / on_disable()(资源清理)。
3. PluginContext(框架注入的运行时)¶
self.ctx.get_secret("weather_api_key") # 上游密钥明文(管理员后台加密配置)
self.ctx.http # 框架统一 httpx 客户端(超时/重试/UA 已配置),禁止自建无超时客户端
self.ctx.log("info", "fetched", n=10) # 结构化日志,自动带 source_code,密钥值自动脱敏
self.ctx.media_dir # M5 起提供:框架媒体目录(与 /media 静态挂载同源),
# 音频等二进制产物落盘用(如 news_hourly),禁止 base64 塞 payload
4. 数据返回约定(小屏友好)¶
| 项 | 约束 | 框架行为 |
|---|---|---|
| payload 序列化大小 | < 4KB | 超限判失败(不缓存),返回 50000 |
| 列表长度 | ≤ 10 条 | 超限截断并记 warning |
| 展示文本单条 | ≤ 30 字符(昵称类 ≤ 15) | 不截断,审核人工把关 |
| 字段稳定 | 同版本内字段名只增不改;废弃字段保留一个版本周期 | 文档站公示 |
5. 参数与 params_schema¶
- 支持
type/enum/minimum/maximum/minLength/maxLength/pattern/required/items/properties/default; - 校验失败返回
40001并指明首个错误字段;未声明的额外参数默认丢弃并告警; - 设备传参:平铺 query 优先(
&city=上海),复杂结构用params=<urlencoded JSON>。
6. 合规红线(注册审核项)¶
- 彩票类:仅官方公开开奖结果;不得出现投注/预测/导流链接/二维码;
source_name必须含“仅供信息展示”类免责表述; - 社媒类:仅公开榜单/公开页数据;不请求、不存储个人隐私内容;
meta().resp_example与文档必须写明数据来源;- 上游 Cookie/Key 一律走
ctx.get_secret(),严禁硬编码或写日志(CI 扫描 + 审核检查)。
7. 完整示例(weather_basic)¶
from app.core.plugins.base import BaseDataSource
from app.core.plugins.errors import UpstreamBadResponseError
class WeatherBasicSource(BaseDataSource):
source_code = "weather_basic"
version = "v1"
name = "基础天气"
category = "weather"
cache_ttl = 3600
stale_ttl = 7200
cache_scope = "params" # 按城市分缓存
need_subscription = True
source_name = "数据来自公开天气服务,仅供参考"
params_schema = {
"type": "object",
"properties": {"city": {"type": "string", "minLength": 2, "maxLength": 12}},
"required": ["city"],
}
def meta(self) -> dict:
return {
"name": self.name, "category": self.category,
"cache": {"ttl": self.cache_ttl, "scope": self.cache_scope},
"params_schema": self.params_schema,
"resp_example": {"city": "上海", "temp": 31, "text": "多云",
"wind": "东南风3级", "source_name": self.source_name},
}
async def fetch(self, params: dict) -> dict:
api_key = self.ctx.get_secret("weather_api_key")
if not api_key:
raise UpstreamBadResponseError("secret weather_api_key missing")
resp = await self.ctx.http.get("https://api.example.com/v1/now",
params={"city": params["city"], "key": api_key})
if resp.status_code != 200:
raise UpstreamBadResponseError(f"upstream http {resp.status_code}")
raw = resp.json()["data"]
return {"city": params["city"][:12], "temp": int(raw["temp"]),
"text": raw["text"][:6], "wind": raw["wind"][:8],
"source_name": self.source_name}
DATASOURCE = WeatherBasicSource
8. 注册 → 审核 → 版本管理¶
- 目录放入
plugins/→ 启动扫描或管理端“重新扫描” → 新行(待审核); - 管理员核对 meta、缓存/限流策略、schema、
source_name合规、依赖声明 → approve 上线(≤5s 生效); - 新版本:同目录暴露
DATASOURCES = [V1, V2]→ v2 审核上线后 v1 建议立即deprecate(默认日落 +90 天)→ 到期自动转已下线(50302); - 停用
disable(临时维护,可恢复)/ 下线offline(永久)均由管理端操作并记审计。
9. 开发自检清单(提交审核前)¶
- [ ] 目录内可独立 import,无框架内部依赖;
- [ ] 所有外部请求走
ctx.http且有超时;无阻塞调用(同步 IO 用asyncio.to_thread); - [ ] 密钥零硬编码、零日志输出;
- [ ] payload < 4KB、列表 ≤ 10、文本 ≤ 30 字;
- [ ] 同参数重复调用幂等;上游异常路径本地验证过(mock 或断网);
- [ ]
meta()与实际声明一致,resp_example真实可用。
附录 A:插件可用公共依赖¶
镜像内预装(插件 requirements.txt 只需声明额外依赖):httpx、pydantic>=2。其余第三方库由插件自声明,部署方评估后安装。
M3 起放行 aiomysql(随平台 [mysql] extra 预装):限数据库直连类插件使用,约束如下——
- 仅只读 SELECT,禁止插件内写库/建表/建连接池;短连接 +
connect_timeout兜底(回源频率由 global 缓存与 cron 预热压低); - 连接参数一律经
ctx.get_secret注入(如lottery_ssq的db_host/db_port/db_user/db_password/db_name,由scripts/seed_lottery_secrets.py加密写入),严禁硬编码; - 查询函数写成独立方法(如
_query_latest)作为测试注入点,单测 monkeypatch 不真连库。
M5 起放行 edge-tts(news_hourly 插件私有依赖,见其 requirements.txt,部署侧安装):限文字转语音类插件使用,约束如下——
- 走微软 Edge 朗读在线接口(websockets),非官方接口、免 Key,存在失效风险:TTS 封装须独立成文件并保持
synthesize(text, voice) -> bytes签名稳定,便于切换云厂商 REST TTS; - 仅在插件
fetch内延迟 import;依赖未安装时插件注册/扫描不受影响,合成时抛DataSourceError家族异常; - 合成产物落盘
{ctx.media_dir}/<子目录>/(框架媒体目录,与/media静态挂载同源),禁止 base64 塞进 payload(<4KB 硬约束); - 落盘必须临时文件 +
os.replace原子替换(下载端并发保护),归档文件由插件自行清理限量。