代码核对事实(pipeline.py / compat.py / actor_ctx.py / datatable handler.py / backend/api/routers)
什么时候读:想知道某条规则在源码哪里、默认值是多少、为什么这么写时。每条注明来源文件与函数。 本文记录的出入都已并入
spec-revised.md(标【订正】),规范与代码现在一致;日常写技能以 spec-revised 为准,本文是它的出处。 唯一例外是平台尚未实现的规范项(如display_only),spec-revised 已标明「未实现」,写技能时按现状处理。 §1–§4 为 2026-08 按 core 层核对(其中有误的已在原处改正);§5 为 2026-10 按 routers 与平台核心层(executor / pipeline / compat / actor_ctx)核对。
1. 三个此前待实测的问题,已有答案
1.1 call_skill 返回不额外包层;data 是 datatable 自己的信封
pipeline.execute_skill 把 executor.execute(...).output(即 handler 的 return dict)原样放进
SkillRunResult.output,不加包装。datatable handler 成功时统一 return {"success": True, "data": data}。
所以:
r = await dt(ctx, "query", table_name="客户")
rows = r["data"]["list"] # query
total = r["data"]["pageInfo"]["totalRows"]
tables = r["data"] # list_tables → [{id,title,schema,qualified}]
失败时 datatable 返回 {"success": False, "user_message", "need_param"?, "error_type"?, "non_retriable"?, "available_tables"?, "redirect"?};
call_skill 层失败(执行失败,或被调方自报 ok/success 为 False)统一返回 {"ok": False, "error": <被调方 error/msg,否则「技能自报失败」>, "skill", "output": <被调方原始信封>}——datatable 只给 user_message 时 error 就是「技能自报失败」,真因在 r["output"]。判失败看 ok is False(executor _make_call_skill,2026-10 核对)。
(来源:pipeline.py execute_skill/_run;datatable handler.py 末尾 return {"success": True, "data": data})
1.2 create_table.fields 形状
[{"title": "商铺号", "type": "文本", "required": true, "unique": true},
{"title": "租金", "type": "金额"},
{"title": "状态", "type": "单选", "options": "招租中,已租,停用"}]
- 必填
title+type;可选required/unique(bool)、options(字符串,非数组)。 type枚举:文本 长文本 数字 整数 金额 百分比 日期 日期时间 时间 勾选 单选 多选 邮箱 电话 链接。 没有JSON和高精度数字(规范 §7.5 列了,schema 枚举没有)。要存 JSON 用长文本。add_field.field同单项形状;update_field.patch只传要改的:new_title / type / required / unique / options。 (来源:datatable handler.pyINPUT_SCHEMA["fields"])
1.3 send_message.to 的 openid = ctx["actor_key"]
actor_key 来自 ChatUser.contact_key,而 contact_key 的定位规则(actor_ctx.locate_chat_user):
| 渠道 | actor_key 的值 |
|---|---|
| 第三方渠道(飞书/企微/钉钉/Telegram) | session.peer_user_id,否则 session.external_id —— 就是渠道 openid |
| web 登录用户 | ChatUser.id(从 external_id 的 web_u_<cu_id>__… 解出) |
| web 匿名访客 | "" |
| 管理员代填 | ""(被 ADMIN_SKIP 清空) |
所以建单时存 origin_channel = ctx["channel"]、origin_openid = ctx["actor_key"]、
origin_session_id = ctx["session_id"],之后 to = f"{origin_channel}:{origin_openid}"。
actor_key 为空(匿名/代填)时必须另外要求填写报修人,不能留空。
SkillRunContext.external_id 存在但 不注入 runtime_context,技能读不到,别用。
(来源:actor_ctx.py locate_chat_user / _ACTOR_CU_MAP;pipeline.py build_runtime_ctx)
1.4 send_message 的 to(handler 已核)
_norm_targets 认四种:"feishu:ou_xxx" / "ou_xxx"(配 channel=)/ {"channel","contact"} / 列表。
冒号后段按渠道分两类:
- 第三方渠道:= 渠道 openid =
ctx["actor_key"]。 - web / web_admin:=
session_id。handler 的 web 分支不走 adapter,而是拿冒号后段 当 session_id 去db.get(ChatSession, ...)、写ChatMessage(source="skill_send")再 WS 推送。 拿 web 登录用户的actor_key(= ChatUser.id)当to会查不到会话、静默 failed。
返回 {"ok", "sent": [...], "failed": [...], "message": "已投递 N/M…"},
ok = bool(sent) and not failed。凭证由 ChannelGateway 按 role_id 解析,技能无配置参数。
attach 是本地文件路径列表,按扩展名走 send_image / send_video / send_file,渠道不支持则计入 failed。
(来源:send_message handler.py _norm_targets / _send_web / execute)
2. 原规范与代码的出入(均已按代码订正进 spec-revised)
本节里确定是文档错的几条已并入
spec-revised.md,标【订正 2026-08】/【订正 2026-10】。 原先待定的workspace_dir、结构类鉴权两项已于 2026-10 按 executor / nocodb 代码定案,见 pending-issues.md。
| 项 | 规范说 | 代码实际 | 来源 |
|---|---|---|---|
workspace_dir | §4.3 列为注入键 | 已注入,但位置不在 build_runtime_ctx:executor.execute 末段 merged_ctx.setdefault("workspace_dir", str(compat.workspace_dir(merged_ctx)))。所有线都经 executor,都有;仅 role_id 为空时缺。2026-08 的 grep 只搜了 "workspace_dir": / ["workspace_dir"] = 两种写法,漏了 setdefault,误判为不注入 | executor.py;compat.py workspace_dir |
allow_sql | §4.3 列为注入键 | 不注入;datatable ctx.get("allow_sql", True) | pipeline.py;datatable |
| 入口第二参数默认值 | §2.3「不得设默认值否则 ctx 丢失」 | 【非 bug·文档从严】 bind_entry_args 用 sig.bind 先试 (input, ctx),带默认值也拿得到 ctx,「否则丢失」已不必然。但约定维持「不得给」(保守、datatable 注释也要求),检查器 WARN。已在 spec-revised 标订正 | compat.py |
| 失败原因来源 | §6.1「失败必须给 error」 | normalize_result:ok=False 取 error→msg;success=False 取 stderr→error→msg→user_message。只给 user_message 也能当原因 | pipeline.py |
| 配置类错误文案 | — | 错误文本含 未配置/api_key/密钥/未授权/401/invalid api 时,平台截掉「。请先…」之后的引导并追加 _hint「改用其它技能」。技能的配置错误文案里不要写 save_skill_param 之类的操作指引,写了也会被剁 | pipeline.py _sanitize_config_error |
| 删除命中表清单 | §7.3 缺 table_id 返回清单 | _NEED_TABLE_ID 在 handler 里定义了三次,最后一份没有 delete_batch / delete_by,这两个缺 table_id 时拿不到清单,直接 KeyError→need_param=table_id(仍可自纠) | datatable |
| filters 残留引用 | §7.4「拒绝执行」 | 返回 non_retriable: True——会把 datatable 本轮拉黑。链里引用名打错的代价是整轮失去数据表 | datatable _check_unresolved_refs |
| 结构类鉴权 | §7.2 owner/admin | 正式规则 = owner/admin(已定案):Web 数据接口 nocodb.py 已执行;技能线 datatable handler 两处仍注释,列为平台落地项 | datatable;routers/nocodb.py |
3. 规范没写、代码有的
3.1 runtime_context 实际键集合(build_runtime_ctx)
role_id org_id team_role chat_mode(=True) session_id user_id team_id is_admin
+ fixed_ctx 全部:raw_input attachments actor_*×11(含 actor_department_id) channel triggered_at triggered_at_str actor_id actor_is_admin
db db_factory channel_registry request abort_signal
skill_configs skill_args + 平铺配置(含大写别名)
_meta: {skill_name, invoked_at}
+ ctx.extra(调用方附加,链不传)
_meta.skill_name 可用于日志;datatable 还读 _called_by(可能为空)。
3.2 配置三通道(compat.py)
- ctx:
skill_configs[skill][k]平铺到skill_configs[k]/skill_args[k]/ctx[k],并补 大写别名ctx["API_TOKEN"]。 - input 回填:
effective_input把配置setdefault进input_data,只回填 handler 代码里真会读的键(AST 扫描),模型显式传的优先。所以「只认 input_data」的老技能也拿得到配置。 - 子进程环境变量:原名 + 大写各一份;另有
SKILL_WORKSPACE(工作区绝对路径)、SKILL_DIR(技能目录)。
3.3 子进程 / CLI 约定(compat.py)
- argv 拼法:
True → --flag(False 不出现);list →--flag a b c;dict →--flag '<json>';key_name → --key-name;_开头键跳过(含_subcommand,由调用方 prepend 到 argv[0])。 - 位置参数:schema 里
x-cli-positional: <序号>;无 schema 时prompts/args/*_args当位置参数。 - 子命令字段:schema
x-subcommand: <字段名>;没有则「唯一 required 且带 enum 的字段」被猜成子命令。 - 占位符:入参里
{baseDir}/${baseDir}→ 技能目录绝对路径;{workspace}/${workspace}→ 工作区。带 assets/ 模板的技能靠这个。 - 产物行:stdout 里
MEDIA: /abs/path或MEDIA_URL: https://…,平台转成{ok, media, filename, local_path, urls, images|videos|audios|files}。 - 依赖:脚本含 PEP 723
# /// script块或技能目录有requirements.txt→ 自动建.venv装依赖(有uv则uv run --script)。SKILL_AUTO_DEPS=0关闭。容器侧按requires.pip/npm/network预置到.deps/<sig>,network: none且缺包直接结构化失败。
3.4 产物兜底采集的精确规则(backfill_artifacts)
- 只在返回里没有
filename/local_path时介入;技能自己报了就不插手。 - 只看工作区顶层文件(子目录不可见——所以中间文件放子目录就安全)。
- 跳过
toolout_*和.开头文件;.toolout/子目录是结果卸载收据的落点。 - 新文件全部登记,最新的那个当
filename/local_path。 - 技能目录会被 stage 到工作区
.skills/<name>/(隐藏子目录,不当产物)。
3.5 SkillRunResult.status 三态
done | partial | failed。partial = ok 但 output.completed is False 或有 output.warning。
技能想表达「做了一部分」:返回 success: True + completed: False 或 warning: "...",前端会显示为 partial。
3.6 其它
- 未注册技能名:
execute_skill直接返回「未知工具:X,请检查技能是否已安装」;datatable 名探测时匹配这句也算「未注册」(规范写的是「未注册」,实际文案是「未知工具」——探测用in同时匹配两个词)。 knowledge型在执行路径被拒:「是知识型技能,不可执行」。skill_md型:handler 返回含skill_doc键且无needs_config时触发子代理;链/定时任务(allow_skill_md=False)直接拒。- 异步:
_ASYNC_SKILLS = {kling, seedance, media_gen}白名单 + frontmatterasync都算;web / web_admin 渠道仍同步, 但external_id以orgdeleg:开头的组织委托会话按非 web 处理(转后台)。 code为非 0/200 整数只记日志不翻转失败——不要用code表达失败,用success: False。- 保留字段(
compat.RESERVED_CTX_KEYS):db session_id channel role_id user_id team_id org_id team_role emp_no actor_key skill_configs skill_args+ 全部触发字段 +actor_*前缀 +_前缀。链入参、HTTP 请求体里同名键被丢弃并告警。 - datatable 内联批量上限由
DATATABLE_INLINE_MAX(默认 100)控制;delete_by备份前查询 limit 10000。
3.7 直连库:db_factory 现开现还,ctx["db"] 是遗留预注入
merged_ctx["db_factory"]恒注入(runtime_context.get("db_factory") or _get_session_factory()), 进程内技能靠它现开短会话。ctx["db"]是遗留预注入:executor 的call_skill对不在SELF_ACQUIRE_SKILLS名单的被调技能, 旧路径async with factory() as _db: sub["db"] = _db预开一条注入;pipeline 对名单内技能也不开skill_scope(db=None)。 名单(2026-10 executor.py):datatabletrade-bottrade-hltradetrade-researchtrade-factortrade-regimecalctrade-macrotrade-okxbn-klinetrade-backtest。名单内技能让自己用db_factory开短 session。datatable handler 末尾据此分流:_injected_db = ctx.get("db"),有注入用注入(不在此 commit/close),没有就async with factory()自开、 成功commit、异常rollback。- 同步入口:
_exec_python对非 async 入口_ctx = {**_ctx, "db": None}——同步技能拿到的db是None(且 async 会话在同步线程里不可用)。 - 子进程:
db/db_factory是活对象 / callable,_json_sanitize丢弃,SKILL_CONTEXT里没有。 - 结论(规则已并入 spec §7A):新技能直连一律
db_factory现开现还、不读ctx["db"];ctx["db"]待各技能迁完后从call_skill预开分支移除。范例:trade-okxbn-kline的_db()。 (来源:executor.executemerged_ctx/_make_call_skill_SELF_ACQUIRE/_exec_python;datatable handler 末尾自开分支)
4. 工作区路径推导(技能侧代码)
import os
from pathlib import Path
def _workspace(ctx: dict) -> Path:
ws = ctx.get("workspace_dir") # executor 已注入,正常走这里
if ws:
return Path(ws)
root = os.getenv("AGENT_WORKSPACE_DIR", "data/workspaces")
p = Path(root) / (ctx.get("role_id") or "") / (ctx.get("session_id") or "shared")
p.mkdir(parents=True, exist_ok=True)
return p
与 compat.workspace_dir 和 datatable 的 _write_backup 同一口径,也与 routers 里所有定位会话工作区的代码同一口径。
5. routers 层核对事实(2026-10,backend/api/routers/*.py)
5.1 安装预检 skills._preflight_check
- 需要
skill.toml或SKILL.md;SKILL.md 以---开头、含name:/description:;name匹配^[a-z0-9][a-z0-9_\-]*$且等于目录名。 handler.py:AST 找execute;参数里须有input_data与runtime_context;须是async def。- 问题文本都不带「❌」,所以 zip / 目录安装只警告;
create_skill/edit_skill(roles_chat_skill_tools.py) 把除「将默认」外的全部问题当拦截,edit_skill另跑SkillValidator危险代码扫描、critical 阻断。 create_skill只写 SKILL.md / handler.py / skill.toml;edit_skill只能改落盘父目录名 == role_id 的技能, 支持replacements(old 必须逐字唯一)与full_files。
5.2 frontmatter 读取 _load_skill_md_meta / _build_market_row
yaml.safe_load;失败退回逐行k: v解析(嵌套块、列表全丢)。值里含「英文冒号+空格」不加引号就会失败。- 有
skill.toml时元数据以它为准([skill]/[market]段),SKILL.md 的同名字段不再读;否则读 SKILL.md。 - 市场行从顶层读
category、cost-tier(或cost_tier)、version、tags、skill_type、skill_md、name_zh/display_name_zh/description_zh;不读metadata下的同名键。 category归一只认:office finance ecommerce marketing promotion trading data crm channel media integration compliance general(外加少量别名),其余按名字/描述关键词猜,猜不到general。- 类型推断:有
handler.py→ 执行型(优先于声明);必填密钥 → 执行型;否则看skill_type声明;再看正文是否有可执行引用。 - 没声明
name或 name 不合法 → 用目录名的安全化结果。
5.3 配置项声明 _env_schema_from_dir
顶层 environment_variables / env / requires-env / required-env / secrets / inputs(及 skill.toml [requires].env),
结构化项 required 缺省 true、secret 缺省 false;纯名字列表 = 必填 + 密文;env: [] = 显式零配置;
全都没声明才按正文嗅探(标 source: sniff、非必填)。详见 config-secrets.md。
5.4 页面网关与面板(skills.py)
/skills/invoke:角色校验 → 技能可执行 → action 非空(40001)→action.enum白名单(40404)→ 剥身份字段 → 执行。x-actions/invocable_from/structural不再校验;_action_meta、_call_skill_capabilities、skills._resolve_team_role、_resolve_web_emp_no为无调用点的遗留函数。_build_skill_runtime_ctx:身份走actor_ctx.resolve_actor+build_ctx(与对话线同实现),只认属于当前用户的会话; 键 = role_id user_id session_id channel db skill_configs skill_args + actor 段 + team_id? org_id? + emp_no 别名; 无 team_role / is_admin / chat_mode;channel被 actor 段覆盖。/ui/asset免鉴权:有面板时从入口文件目录取,没有面板的技能退回整个技能目录——任何技能目录里白名单后缀 (.json .js .html .css 图片 字体…)的文件都能按技能名被匿名下载。技能目录里不得放含密钥 / 内部数据的此类文件。- 面板入口 / 资源 / JWT 细节见 ui-panel.md。
5.5 对话线执行(roles_chat_helpers.py)
| 项 | 值 |
|---|---|
| 墙钟 | 技能墙钟 pipeline.resolve_wall_sec:自声明 wall_sec/timeout_sec > policy > SKILL_WALL_DEFAULT 600,硬顶 SKILL_WALL_HARDCAP 1800;外层 TOOL_WALL_SEC 默认 1800;MAX_LOOP_SEC 默认 300,小于 TOOL_WALL_SEC 时抬到 +60;MAX_TOOL_ROUNDS 默认 8 |
| 失败预算 | FAIL_PER_TOOL 默认 4(GuardChain)、FAIL_TOTAL 默认 8;同参第 4 次调用判死循环 |
| 可恢复 | need_param 键,或 error_type ∈ {recoverable, retryable, need_input, need_param}:不计数,带 _hint 喂回 |
| 自动重跑 | Timeout / Connection / OSError / SA Operational / Interface 异常重跑至多 3 次,每次新会话;本轮已失败或近 30 分钟最近 2 次全失败(_skills_last_failed 熔断)的技能只跑 1 次 |
| 参数校验 | schema.required:缺键或空值("" [] {} 空白)都判缺,回错误让模型自纠 |
| 结果卸载 | 成功且 >12000 字符 → .toolout/toolout_<name>_<hex>.json + 收据(标量 / 小字段 / 列表 count+sample / 媒体字段 / display_md≤15000) |
| 直显 | 会话 channel ∈ web / web_admin 时追加 display_md;display_only 未实现 |
read_skill_file | 单次 40000 字符,脚本 120000 |
| attachments | _collect_session_attachments 收集(本轮 + 历史 10 + 生成物 10),再经 actor_ctx.build_ctx → norm_attachments 归一成 {type, att_type, filename, local_path, url} 才进 ctx |
| 异步后台 | _run_async_skill:同走 executor.execute;ctx 不显式写 user_id / team_id / request(team_role 是否随 fixed_ctx 带入不可依赖);结果经同一套假成功翻转与产物物化 |
5.6 文档型 / API 型执行(roles_chat_skill_runtime.py / roles_chat_api_skill_executor.py)
见 type-skill-md.md。要点:执行型子代理 12 轮 / 420 秒(样例脚本型 20 / 900);API 型 6 轮 / 90 秒、只支持 Bearer、
路径模板不替换、host 白名单;任何位置有 openapi.yaml|yml|json 或 endpoints_whitelist.yaml|yml 即判 API 型。
5.7 技能链(skills.py 内 skills_chain 段)
见 chain.md「保存校验」。链名 ^[a-z][a-z0-9_]*$;保存即注册为 skill_type="chain";/chain/test 用
_build_skill_runtime_ctx(channel web_admin,可指定 emp_no)。
5.8 Web 数据管理(nocodb.py,非技能线)
结构类 / 只读 SQL / 授权管理仅 owner/admin;非管理员按名下角色授权并集(默认全开,授权表是例外表)、行级锚点 User.emp_no;
可写 SQL 仅 SELECT / INSERT / UPDATE / DELETE / CREATE(TABLE / INDEX / VIEW 等),UPDATE / DELETE 须顶层 WHERE、禁多语句。
5.9 平台核心层核对(2026-10,executor.py / pipeline.py / compat.py / actor_ctx.py)
| 项 | 事实 |
|---|---|
workspace_dir | executor.execute setdefault 注入,见 §2 |
provider | executor.execute 按 role_id _role_provider 解析注入(带 TTL 缓存);_NO_LLM_SKILLS 名单内技能不取 |
| 固定上下文 | FIXED_CTX_FIELDS 16 项(含 actor_department_id)+ EXTRA_KEYS 2 项 = 18 键,恒存在;build_ctx 只认词表键 |
| 代填清空 | ADMIN_SKIP_ACTOR_KEYS = title / department / department_id / department_path / position / tags / key / id / emp_no |
team_role | 对话线解析真实值;call_skill 透传;SkillRunContext.team_role 原默认 member,2026-10 补丁改为空串并由 build_runtime_ctx 按 user_id 解析;runner 原先缺失时塞 member(已改);异步后台 fixed_ctx 不含它 |
| 入口绑定 | executor 认 execute / run / handler / main / handle;bind_entry_args 用 sig.bind 先试 (input, ctx);同步入口 db=None 走线程池;没有入口 → 尝试文档型兜底,否则报错 |
| 结果归一 | pipeline.normalize_result:只翻转显式 ok is False / success is False;只给 error 不给 False 的返回会被当成功 |
| 配置错误文案 | 含「未配置 / save_skill_param / api_key / api_token / apikey / 密钥 / 未授权 / 401 / invalid api」时截掉「。请先…」后的引导,换成换技能的 _hint |
| 子进程上下文 | SKILL_CONTEXT = runtime_context 去 _ 前缀键、去不可序列化对象;SKILL_WORKSPACE / SKILL_DIR 环境变量 |