跳到主要内容

技能间协作:call_skill、通知、消息

什么时候读:技能内要调别的技能(含 datatable)、发通知、发消息给某人、或设计多步链时。

目录​

  1. call_skill 签名与规则
  2. 四条硬约束
  3. 通知与消息投递(notify / send_message)
  4. 其它串联方式

1. call_skill​

技能是独立执行单元,不得互相 import。要调另一个技能用 ctx["call_skill"]。

result = await ctx["call_skill"](
"<skill_name>",
{"字段": "值"}, # 显式入参
source=上一步产出, # 可选,供 template / passthrough 取值
template={"目标字段": "文案 {源字段}"}, # 可选
passthrough=False, # 可选,整份 source 透传
其它字段="值", # 可选,等价于写进第二个参数
)
项规则
入参优先级显式 dict / kwargs > template > passthrough > 被调技能自身配置
配置注入被调技能的 skill_configs 按 role_id + 被调技能名 重新查库注入,不继承调用方
透传的上下文白名单(平台常量 _CALL_PASS_BASE + ACTOR_KEYS + EXTRA_KEYS)= role_id session_id user_id team_id org_id team_role is_admin channel chat_mode workspace_dir channel_registry db_factory abort_signal allow_sql + 十个 actor_* + actor_id actor_is_admin。调用方的 skill_configs / skill_args / 扁平参数、词表外自造键(如 emp_no)不透传。workspace_dir 虽在白名单里,但上游从不注入(见 pending-issues ISSUE-1),子技能同样按 _workspace(ctx) 自推
db每次调用新开独立会话,子技能出错不污染调用方事务
返回成功 = 被调技能的 output dict 原样(不包 data;datatable 的 data 是它自己的信封)。失败(执行失败,或被调技能自报 ok/success 为 False)= {"ok": False, "error": <被调方 error 或 msg,都没有则「技能自报失败」>, "skill": 名, "output": <被调方原始信封>}(executor _make_call_skill)。所以 datatable 的 user_message / need_param / available_tables 在 r["output"] 里。不抛异常
层数最多 3 层;不得直接自调

参数由技能作者在代码里写死,不经过模型——零 token、不漂移、不编造。这是 call_skill 相对「让模型连调多个工具」的价值。

2. 四条硬约束​

  1. 调用前先 commit。 子技能是另一个 session,调用方未提交的写入它看不到。直连库用 code-facts.md §3.7 / templates/handler_async.py 的 _db(commit=True) 现开现还写法时天然满足(每次写即已提交);ctx["db"] 是遗留注入,新技能别用。
  2. 只有进程内 async Python 能用。 CLI / NODE / SHELL / GO 的 SKILL_CONTEXT 里没有这个键。
  3. async: true 的技能不要用 call_skill 调。 会同步执行并占用父技能的墙钟。
  4. 产物后处理不生效。 远程 URL 物化、附件登记、结果卸载都在对话层。子技能只回 URL 不会落盘;需要产物的场景由调用方保证文件落在工作区。

判失败看返回的 ok:

# 成功:就是子技能自己的信封,不额外包 data。
# datatable 成功 {"success": True, "data": {...}},query 结果在 r["data"]["list"],不是 r["list"]。
# 失败:call_skill 统一包成 {"ok": False, "error", "skill", "output": 子技能原始信封}。
r = await ctx["call_skill"]("xxx", {...})
if r.get("ok") is False or r.get("success") is False:
up = r.get("output") if isinstance(r.get("output"), dict) else r
return {"success": False, "upstream": up,
"error": f"xxx 失败:{up.get('error') or up.get('user_message') or r.get('error')}",
"need_param": up.get("need_param"),
"error_type": up.get("error_type", "recoverable")}

3. 通知与消息投递​

按收件人形态选,不按内容选:

技能用于target / to
notify单向渠道:固定群、Webhook、邮件、Apprise URLtarget="ops"(管理员配的别名)或裸 Apprise URL
send_message双向渠道:飞书/企微/钉钉/Telegram/web 的具体收件人to="feishu:ou_xxx",多人传列表
call = ctx["call_skill"]

await call("notify", {"message": "3号机维修完成,费用 1200 元", "title": "维修完成",
"target": "ops", "attach": [report_path]})

await call("send_message", {"message": "您报修的 3号机 已修复。",
"to": f"{order['origin_channel']}:{order['origin_openid']}",
"attach": [photo_path]})

await asyncio.gather(
call("notify", {"message": text, "target": "ops"}),
call("send_message", {"message": text, "to": ["feishu:ou_a", "feishu:ou_b"]}),
)

收件人必须来自业务数据,不能来自 actor_*。 actor_* 是「本轮说话的人」;维修工回「完成了」时 actor 是维修工,报修人是另一个人。报修人的渠道和 ID 在建单时就存进工单表(origin_channel / origin_openid / origin_session_id),不能事后从当前会话推断。

send_message 的 to 支持四种形态(按 handler _norm_targets 实测):"feishu:ou_xxx"、 "ou_xxx"(配 channel= 补前缀)、{"channel": "feishu", "contact": "ou_xxx"}、以上的列表。

冒号后那段填什么,按渠道分两类(handler 里 web 走的是另一条路):

目标渠道to 里的 ID建单时存什么
第三方渠道(飞书/企微/钉钉/Telegram)渠道 openid = ctx["actor_key"]origin_channel=ctx["channel"], origin_openid=ctx["actor_key"]
web / web_adminsession_id(不是 openid)origin_channel=ctx["channel"], origin_openid=ctx["session_id"]

web 分支 handler 用冒号后段当 session_id 去 db.get(ChatSession, ...) 并写 ChatMessage + WS 推送; 拿 web 登录用户的 actor_key(那是 ChatUser.id)当 to 会查不到会话、静默失败。所以:

if ctx["channel"] in ("web", "web_admin"):
origin = {"origin_channel": ctx["channel"],
"origin_openid": ctx["session_id"]} # web:冒号后是 session_id
else:
origin = {"origin_channel": ctx["channel"],
"origin_openid": ctx["actor_key"]} # 第三方:冒号后是渠道 openid
origin["origin_session_id"] = ctx["session_id"]
# 之后:to = f"{origin['origin_channel']}:{origin['origin_openid']}"

actor_is_admin=True 时 actor_key 被清空(管理员代填),此时建单要另外要求填写报修人,不能留空。

send_message 返回 {"ok", "sent": [...], "failed": [...], "message"}, ok = bool(sent) and not failed——部分成功时 ok 是 False 但 sent 非空。 要判「有没有发出去」看 sent,判「是否全部成功」才看 ok。notify 同理,判据是 sent。

r = await call("send_message", {...})
if not r.get("ok"):
logger.warning("通知失败:%s", r.get("error")) # 通知失败不回滚业务

投递不等于等回复。 两个技能发出即结束。要「发出去 → 等对方回一句 → 继续」,走审批那套(落 pending 记录,入站消息命中后反向驱动),不要在技能内 await 人的回复。

4. 其它串联方式​

  • 多步链(chain 型技能 / 工作流执行技能 / skill_schedule steps):引用语法、失败策略、并行分层见 chain.md。链内删除注意 datatable.md §8(>10 行会卡在确认)。
  • 形态拿不到 db 时(CLI / 其它语言 / 文档型),把「取数」和「落库」拆成两个技能,落库写成进程内 async Python,由取数那步用 call_skill 调。