跳到主要内容

代码核对事实(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.py INPUT_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)​

  1. ctx:skill_configs[skill][k] 平铺到 skill_configs[k] / skill_args[k] / ctx[k],并补 大写别名 ctx["API_TOKEN"]。
  2. input 回填:effective_input 把配置 setdefault 进 input_data,只回填 handler 代码里真会读的键(AST 扫描),模型显式传的优先。所以「只认 input_data」的老技能也拿得到配置。
  3. 子进程环境变量:原名 + 大写各一份;另有 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} 白名单 + frontmatter async 都算;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):datatable trade-bot trade-hltrade trade-research trade-factor trade-regimecalc trade-macro trade-okxbn-kline trade-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.execute merged_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_direxecutor.execute setdefault 注入,见 §2
providerexecutor.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 环境变量