handler 型 · Python 进程内
什么时候读:技能要跑 Python 代码,尤其是要碰数据库或调别的技能时。 这是默认形态,也是唯一能直连库(
ctx["db_factory"])和用ctx["call_skill"]的形态。
目录
<skill_name>/
├── SKILL.md # 必需。frontmatter 见 templates/SKILL.md.tmpl
├── handler.py # 必需
├── schema.json # 可选,优先级高于代码内常量。见 references/input-schema.md
└── references/ # 可选
平台定位 SKILL.md 的顺序:<skill_dir>/SKILL.md → <origin_path>/SKILL.md → optional-skills/<name> → docs/<name> → README.md,取第一个存在的。
入口函数
async def execute(input_data: dict, runtime_context: dict) -> dict:
...
| 项 | 规则 |
|---|---|
| 写法 | 一律 async def execute(input_data, runtime_context)——函数名、两个参数名逐字照抄 |
| 为什么定死 | 安装预检 skills._preflight_check 用 AST 查 handler.py:找不到 execute、参数里没有 input_data / runtime_context、不是 async def,各报一条问题。create_skill / edit_skill 把这些问题全部当拦截(技能装不上、要求修正);zip / 目录安装时只是警告 |
| 执行器兼容 | executor 运行时认 execute / run / handler / main / handle(取第一个存在的),按必填位置参数个数绑定:0 → f();1 → f(input);≥2 → f(input, ctx)。这是给老技能的兼容,新技能别用 |
| 第二参数 | 不要设默认值。compat.bind_entry_args 路径能容忍默认值,但 executor 有两条绑定路径,不给默认值两条都安全 |
| 返回 | dict;非 dict 被包成 {"result": ...} |
| 异步 | 需要直连库(db_factory)/ call_skill 时必须 async def。平台按入口是否 async 决定能否 await db 操作;子进程型的 SKILL_CONTEXT 里没有 db / db_factory |
| 无入口函数 | 平台回退去找 SKILL.md,把技能静默降级为文档型。症状是「工具能选到但行为像在读文档」,不是报错 |
安装预检(_preflight_check,安装与 create_skill / edit_skill 共用)
| 检查 | 不过时 |
|---|---|
有 SKILL.md(或 skill.toml),SKILL.md 以 --- frontmatter 开头,含 name: 与 description: | 报问题 |
frontmatter 的 name 匹配 ^[a-z0-9][a-z0-9_\-]*$,且等于目录名(不一致会「以 name 注册」) | 报问题 |
handler.py 能 AST 解析;有 execute;参数含 input_data 与 runtime_context;是 async def | 报问题 |
skill.toml 的 [skill].name/description、[exec] 的 type 与 config 文件存在 | 报问题(缺 version 只是「将默认」提示,不拦) |
预检问题都不带「❌」前缀:zip / 目录安装照常装、只显示警告;对话里的 create_skill / edit_skill 除「将默认」类提示外一律拦截。
edit_skill 之后还会跑危险代码扫描(SkillValidator),命中 critical 直接阻断写入。
另:create_skill 只写入 SKILL.md / handler.py / skill.toml 三个文件。schema.json、references/、面板文件要随后用
edit_skill 的 full_files 补;只能改本角色专属(role 范围)技能,市场 / 共享 / 内置技能改不了。
平台怎么判定它是进程内而不是 CLI
exec.config 是 .py 且模块内有入口函数 → 进程内。
但若脚本同时含 argparse 与 __main__,或运行中抛 SystemExit,会被判成 CLI 型(见 type-handler-subprocess.md)。
进程内 handler 里不要顺手写 if __name__ == "__main__": argparse... 做自测,会改变执行形态。自测脚本放别的文件。
上下文取用要点
完整键表见 references/runtime-context.md。这里只列进程内特有的:
| key | 说明 |
|---|---|
db_factory | 直连库的唯一入口。收进单一 _db(),每次 async with db_factory() as s 现开一条短会话、用完即还,不横跨慢 I/O 持有。见下节 |
db | 遗留预注入连接,新技能不要用;不参与序列化。直连改用 db_factory |
abort_signal | asyncio.Event 或 None。用户点停止时被 set,长循环轮询它 |
call_skill | 调其它技能的 callable。见 references/call-skill.md |
channel_registry / request | 仅对话线有值;定时任务/异步线为空 |
固定上下文段的键恒存在(无值给空值),直接判空即可,不需要 .get(k, default)。
直连库:单一 _db(),每次现开现还
业务数据表一律走 datatable(见 datatable.md),不要对 org_data_* schema 直接读写。
下面适用于确实需要直连的场景:读平台档案表(ChatUser / OrgDepartment),或 datatable 表达不了的批量分页 / 多表 join。
只有一种姿势:库访问收进一个 _db() 小函数,每次从 ctx["db_factory"] 现开一条短会话,
async with 用完即还,commit= 按需提交,异常交给 async with 自动回滚+关闭。不用 ctx["db"]
(遗留预注入、横跨慢调用攥连接),不在 handler 顶上开一条会话用到底(会横跨慢 HTTP / 调模型)。
范例:trade-okxbn-kline 的 _db()。完整规矩见 spec §7A / references/code-facts.md §3.7。
async def _db(rc, sql, params=None, *, fetch=False, commit=False):
factory = (rc or {}).get("db_factory")
if factory is None:
raise RuntimeError("缺少 db_factory")
from sqlalchemy import text
async with factory() as session: # 一次调用 = 一条短会话 = 一个事务
result = await session.execute(text(sql), params or {})
rows = [dict(m) for m in result.mappings().all()] if fetch else None
if commit: # 写必须显式 commit,否则静默不落库
await session.commit()
return rows
为什么现开现还:① 两次 DB 操作间的慢 I/O 期间手里没连接;② 失败会话即弃、下次拿干净会话,
天然免 25P02 连坐;③ rollback / close 交给 async with,不用手写。跨语句要原子且中间无慢 I/O时,
在一个 _db() 调用里连着执行、最后 commit(此时才需手动 rollback,并提前把要用的名字取成普通变量防 ORM expire)。
其余约束:
- 错误分类按 SQLSTATE,不按异常类名或英文文本。无 SQLSTATE = 没到达数据库,是参数编码问题。
- 不假设唯一约束存在;upsert 前查
pg_constraint、缺了先去重再补建(口径见 spec §7A.4)。 - 表名全限定
org_data_<org>."表"、标识符加引号避保留字;不用裸 SQL 建表(走 datatablecreate_table)。 - 返回可核验数字(
inserted/updated/failed/verified_total),不返回无数据支撑的成功。 - 把
_db做成唯一 seam,globals()["_db"]=_fake_dbmonkeypatch 即可离线自测。
墙钟
不声明 async: true 的技能受 pipeline.resolve_wall_sec 约束:默认 600 秒(SKILL_WALL_DEFAULT),frontmatter 声明 wall_sec / timeout_sec 可放宽,上限 1800(SKILL_WALL_HARDCAP);外层还有对话线 TOOL_WALL_SEC(默认 1800)。超时中止。
页面面板(/skills/invoke)与试运行不经过这层墙钟(executor 自身另有无超时不在本次核对范围),实际受 HTTP 反向代理超时约束——面板动作要快进快出。
长循环轮询 ctx["abort_signal"],被 set 时尽快返回。
耗时更长的生成类任务见 references/output-envelope.md §异步技能。
动作型技能(以 action 分发)
datatable、各分析师技能都是这种。模型填参会系统性漂移,归一必须在技能侧做,文档只是补充。
规则和代码骨架见 references/input-schema.md §动作型技能入参归一 和 templates/handler_async.py。
不得互相 import
executor 按独立脚本加载每个技能,跨技能 import 运行时不成立。要复用另一个技能的能力用 ctx["call_skill"]。