跳到主要内容

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_signalasyncio.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)。

其余约束:

  1. 错误分类按 SQLSTATE,不按异常类名或英文文本。无 SQLSTATE = 没到达数据库,是参数编码问题。
  2. 不假设唯一约束存在;upsert 前查 pg_constraint、缺了先去重再补建(口径见 spec §7A.4)。
  3. 表名全限定 org_data_<org>."表"、标识符加引号避保留字;不用裸 SQL 建表(走 datatable create_table)。
  4. 返回可核验数字(inserted / updated / failed / verified_total),不返回无数据支撑的成功。
  5. 把 _db 做成唯一 seam,globals()["_db"]=_fake_db monkeypatch 即可离线自测。

墙钟​

不声明 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"]。