输出:返回信封、直显块、产物、结果卸载、异步
什么时候读:写返回值、报错、要在前端直接显示表格/清单、产出文件、返回很大、或任务耗时很长时。
目录
- 返回信封(成功/失败字段)
- 失败预算:怎样会被拉黑
- 直显块 display_md / display_only
- 产物(文件、图片、视频)
- 中间文件不是产物
- 结果卸载(>12000 字符)
- 异步技能(async: true)
1. 返回信封
成功:
{"success": True, "data": {...}} # 或按技能语义组织字段
失败字段:
| 字段 | 语义 |
|---|---|
success / ok | False = 失败。平台据此翻转「假成功」;缺省或 None 不算失败 |
error | 面向模型的原因。失败必须给,为空会被平台补成无意义文案。平台取原因的顺序:success=False 时 stderr → error → msg → user_message;ok=False 时 error → msg |
user_message | 面向用户的说明,可直接转述 |
need_param | 缺失或形状错误的参数名 |
error_type | recoverable(改参数可成功)/ config(配置/授权)/ transient(抖动)。recoverable retryable need_input need_param 四个值(或带 need_param 键)= 可恢复:喂回模型自纠,不计失败预算 |
non_retriable | true = 同参数重试必然失败,平台把该技能本轮拉黑。只在「不可恢复的失败」上生效——同时标了可恢复类 error_type / need_param 时被忽略 |
redirect | 建议改用的 action / 技能 |
action_note | 做过 action/参数键别名归一时回写,成功失败都可带 |
约束:
- 可自纠的错误必须标
recoverable并给改法。不得写「内部错误 / 联系管理员」——模型会照抄给用户然后停手。 - 部分成功必须显式标注成功与失败条数,不得只返回
success=true。 non_retriable只给部署级问题(缺模块、配置错、授权不足)。连接抖动、对象过期不要标。- 每条错误信息过一遍「照着它排查会走到哪」。数字要能对账,不给过滤后的「共 0 行」。
- 自报失败时带上被调用方的原始信封(
upstream字段)。 - 配置类错误文案(含「未配置 / 密钥 / api_key / 401」)不要写操作引导(「请先 save_skill_param…」)——平台会截掉并换成「改用其它技能」的
_hint。 - 不要用
code字段表达失败:非 0/200 的code只记日志不翻转。 - 「做了一部分」用
success: True+completed: False或warning: "...",前端显示为 partial。 - 写入后读回核验,不信「写成功」。返回
verified_total。 - 别让超时 / 连接类异常裸抛到平台:对话线对
TimeoutErrorConnectionErrorOSError及 SQLAlchemyOperationalError/InterfaceError会自动整次重跑技能,最多 3 次(该技能本轮已失败过,或本角色近 30 分钟内最近 2 次调用都失败——熔断——则只跑 1 次)。 非幂等写操作要么做成幂等,要么自己捕获并返回信封。库结构类错误被平台标non_retriable。
2. 失败预算(roles_chat_helpers,核对 2026-10)
| 项 | 现行值 | 说明 |
|---|---|---|
| 单技能失败上限 | FAIL_PER_TOOL,默认 4 | 交给 GuardChain,到限即本轮封禁该技能(源码注释仍写「2 次」,以值为准) |
| 全局失败上限 | FAIL_TOTAL,默认 8 | 本轮累计到限强制收尾 |
| 死循环 | 同一 (工具, 参数) 第 4 次调用(计数 >3) | 直接停用该技能,返回 non_retriable |
| 立即拉黑 | 不可恢复失败 + non_retriable: true | 本轮不再给模型这个工具 |
| 不计数 | 可恢复失败(见 §1 error_type)、参数校验失败、守卫拦截 | 喂回模型自纠 |
技能要保证「同参数重试无意义」时明确报错(non_retriable 或 need_param),不要让模型原样重试。
3. 直显块
{"display_md": "**结果**\n\n| 列 | 值 |\n|---|---|\n| a | 1 |",
"display_only": True} # 可选
display_md 在富前端渠道(会话 channel 为 web / web_admin)追加到回复正文之后(模型正文里已逐字包含同一段时不重复追加),
文本渠道回退给模型转述。用于表格、地图链接、清单,避免模型复述时丢失或篡改数据。
display_only:现行对话线未实现。 规范定义它为「替换模型正文、只呈现 display_md」,但 roles_chat_helpers
里只留了一个未使用的占位变量,没有任何代码读这个键(核对 2026-10)。现状 = 无论写不写都只是追加。
-
可以照规范写(将来实现后自动生效),但不能依赖它去重或防篡改。
-
真要「逐字不许改」,在角色提示词里明令不复述该工具返回,或把这段文本做成工具动作的唯一输出。
-
仅富前端渠道生效。
-
只影响展示与落库正文,不影响工具执行。
-
结果被卸载(§6)时
display_md保留前 15000 字(TOOL_DISPLAY_KEEP_CHARS),超出截断并注明全文位置。
4. 产物
{"ok": True,
"filename": "report.xlsx",
"local_path": "/abs/workspace/report.xlsx",
"media": "file", # image / video / audio / file
"images": [{...}], "videos": [{...}], "files": [{...}], "audios": [{...}]}
平台后处理链(技能不用自己做):
- 远程 URL 物化:明确字段(
urlsimage_url(s)video_url(s)audio_url(s)file_url(s)output_urlresult_urloss_urlcdn_urldownload_urlabs_url)无条件下载;含糊字段(urllinkhrefsrcimagevideoaudiofile)和正文里的链接先探 Content-Type,网页/JSON 跳过,最多 8 个。单文件默认 200 MB 上限。已有local_path的条目跳过。 - 产物登记:工作区里的文件登记为会话附件,正文自动补下载链接。没在返回里声明的文件也兜底采集(
.py/.js/.sh/.log/.tmp/.srt/.pyc除外,除非用户本轮明确要脚本)。 - 渠道投递:按 media / 扩展名分发到 send_image / send_video / send_file。
技能只需保证文件真实落在会话工作区(ctx["workspace_dir"],由 executor 注入;子进程同名键或 SKILL_WORKSPACE),
或返回可下载 URL,不要自己拼下载链接或域名。
call_skill 调的子技能不经过这条链,见 call-skill.md。
5. 中间文件不是产物
兜底采集(backfill_artifacts)只看工作区顶层文件、只在返回里没有 filename/local_path 时介入、跳过 . 和 toolout_ 开头。它不区分「给用户的」和「技能自用的」。导入 JSON(*_import_*.json)、删除备份、临时拼接文件落在工作区根目录都会被当附件展示(踩坑 #63:20 个导入文件全被展示)。
规则:中间文件放工作区子目录;成功即删;失败保留供排查并在返回里点名。
6. 结果卸载(_offload_large_result)
成功且 JSON 序列化后超过 TOOL_RESULT_OFFLOAD_CHARS(默认 12000)字符的 dict 返回,整份写到
<工作区>/.toolout/toolout_<技能>_<8位hex>.json(隐藏子目录,不会被当产物),上下文里只留收据:
| 收据里有什么 | 规则 |
|---|---|
ok offloaded skill path chars schema_keys(前 20 个键) _hint | 固定 |
display_md | 保留,超 15000 字截断 |
| 顶层标量、≤500 字的字符串、序列化 ≤1000 字的小 dict | 原样透传 |
每个非空列表 k | 变成 k_count(条数)+ k_sample(前 3 条,每条截 300 字) |
filename local_path download_url abs_url media images videos files audios | 透传(媒体列表 <12000 字、其余 <2000 字时) |
所以大结果放列表字段(如 rows),关键少量字段(状态、计数、产物路径)放顶层标量。
失败返回不卸载;读取 / 翻页类内置工具不卸载,自带分页。
7. 异步技能
耗时超过对话可等待范围的生成类技能,frontmatter 声明 async: true:
- 非 web 渠道立即返回
{"ok": true, "async": true, "message": "..."},真实执行转后台,完成后按发起渠道主动推送产物。 web / web_admin 仍同步;但组织委托会话(external_id以orgdeleg:开头,渠道虽是 web_admin)按非 web 处理、转后台。 - 后台 ctx:
role_idorg_idchat_mode=Truesession_idis_admin+ 发起时的固定上下文段(只含词表键)db(懒会话)+db_factory+ 预取并平铺的配置 +abort_signal=None,再经 executor 补workspace_dir/provider/call_skill。 没有team_role(datatable 等按member处理)、request、channel_registry;user_id/team_id为空。
- 后台结果同样经「假成功翻转」;只回
filename时平台按<工作区>/<filename>补local_path;远程 URL 会物化。 没有任何媒体产物时推送message/result/text文本。 async: true的技能不要用 call_skill 调:会同步执行并占用父技能的墙钟。
不声明 async 的技能有两层墙钟(核对 pipeline.py / roles_chat_helpers.py):
- 内层 · 技能墙钟(
pipeline.resolve_wall_sec,对话线、链、定时任务都生效):技能自声明wall_sec/timeout_sec(或metadata.openclaw.timeout)> policy 值 > 默认SKILL_WALL_DEFAULT= 600 秒,一律不超过SKILL_WALL_HARDCAP= 1800。 超时返回「技能执行超过 N s 已中止」。不声明就是 600 秒——长任务要么声明,要么async: true。 - 外层 · 工具墙钟(对话线
TOOL_WALL_SEC,默认 1800 秒)。整轮循环MAX_LOOP_SEC会被抬到不小于它 +60,实际还受反向代理超时约束。 子进程另有自己的 60 秒默认超时。 长循环轮询ctx["abort_signal"]。别按固定秒数写死逻辑。