ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenClaw架构与源码解读 · 第10章:一条消息的生命旅程——从 Slack 到技能调用再到回复(TaoToken 配置骨架)

OpenClaw架构与源码解读 · 第10章:一条消息的生命旅程——从 Slack 到技能调用再到回复(TaoToken 配置骨架) 1. 一条 Slack 消息在 OpenClaw 里到底经历了什么你在 Slack 里敲下「帮我清空收件箱里今天的 GitHub 通知顺便给我一个总结」按下回车。几秒后一条整理好的摘要回到同一个频道。这中间 OpenClaw 做了什么它怎么知道是你发的、该用哪个 Agent、要不要调工具、调完怎么把结果拼回一句话这一章就干一件事把这条消息的完整链路拆开从 Slack 事件进来到回复写回去每一步都给出接近源码的 TypeScript 骨架再配一份能直接跑的config.toml/settings.json和 TaoToken 统一 Key 接入示例。适合已经在读 OpenClaw 源码、或者准备自己接一个 Slack 机器人跑通端到端链路的开发者。读完你能本地启动、回放一条 Slack 事件、在日志里看到它一路走到 Skill 调用再回到 Slack。整条链路可以粗分成七步Slack 事件接入 → Channel 适配器转成InboundMessage→ Gateway 安全检查与 Session 解析 → Agent 路由与上下文加载 → Agent Runtime 多轮推理 → Skill 执行 → 回复流式写回并持久化。下面按这个顺序走最后给排障清单。2. 前置TaoToken 统一 Key 与 OpenClaw 配置骨架OpenClaw 的模型调用最终都收敛到modelClient.chat()它需要一个兼容 OpenAI 协议的 endpoint 和一把 Key。我用 TaoToken 做统一入口好处是 Agent、Skill 里所有模型请求共用一把 Key换模型只改配置不改代码。先去控制台拿 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面创建https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite base URL 用https://taotoken.net/api注意这个不带 UTM。OpenClaw 的配置分两层config.toml管 Gateway、Channel、Agent、Skill 这些运行时结构settings.json管模型凭据和会话默认值。下面这份骨架可以直接抄把sk-xxx换成你的 Key。# config.toml —— OpenClaw Gateway 主配置骨架 [gateway] host 127.0.0.1 port 8787 log_level debug # 排障期开 debug能看到链路每一步 [channels.slack] enabled true mode socket # 开发用 socket生产换 events bot_token xoxb-你的-bot-token app_token xapp-你的-app-token dm_policy pairing # open | pairing | allowlist [sessions] default_activation_mode passive context_window_tokens 4000 [[agents]] id inbox-assistant is_default true role 邮件与通知整理助手 model claude-sonnet # 逻辑名真实映射在 settings.json max_tool_turns 10 [[agents.routes]] match_channel slack target_agent_id inbox-assistant [skills] enabled [gmail-archive, summarize] sandbox docker # 生产建议 docker本地可先 host{ models: { provider: openai-compatible, base_url: https://taotoken.net/api, api_key: sk-你的TaoToken密钥, mapping: { claude-sonnet: claude-sonnet-4-20250514, gpt-fast: gpt-4o-mini } }, session: { default_agent_id: inbox-assistant, persist_messages: true }, security: { audit_enabled: true } }注意base_url结尾不要带/v1OpenClaw 的model-client会自己拼/v1/chat/completions。带错了会 404这是最常见的接入坑。3. 可复制配置Slack 事件接入与 TypeScript 路由分发3.1 Slack 事件接入Socket ModeOpenClaw 用slack/bolt封装两种接入方式统一成一个回调。开发阶段用 Socket Mode不用公网 URL。// src/slack/index.ts —— Channel 适配器入口 import { App } from slack/bolt; import { adaptSlackMessage } from ./inbound; import type { ChannelAdapter, InboundMessage } from ../types; export function createSlackChannel(config: { botToken: string; appToken: string; }): ChannelAdapter { let emitInbound: (msg: InboundMessage) void () {}; const app new App({ token: config.botToken, appToken: config.appToken, socketMode: true, }); app.message(async ({ event }) { const inbound adaptSlackMessage(event as any); emitInbound(inbound); }); return { connect: () app.start(), disconnect: () app.stop(), isConnected: () app.receiver?.isActive() ?? false, onMessage: (handler) { emitInbound handler; }, send: (msg) sendSlackMessage(app, msg), }; }3.2 格式转换Slack Event → InboundMessage这一步的意义是让 Gateway 完全不感知 Slack API 细节。转换后所有下游只认InboundMessage。// src/slack/inbound.ts import type { InboundMessage } from ../types; export function adaptSlackMessage(event: any): InboundMessage { return { id: slack:${event.ts}, channel: slack, peerId: slack:user:${event.user}, chatId: slack:channel:${event.channel}, text: event.text ?? , attachments: adaptAttachments(event), replyTo: event.thread_ts ? slack:${event.thread_ts} : undefined, timestamp: new Date(parseFloat(event.ts) * 1000), raw: event, }; }3.3 Gateway 安全检查与 Session 解析消息进来先过安全门。群聊直接放行私聊按dm_policy判断pairing模式下未配对用户会收到一个配对码消息不再往下走。// src/security/check.ts export async function checkInboundSecurity(msg, config, whitelist) { const policy config.channels[msg.channel]?.dmPolicy ?? pairing; if (isGroupChat(msg.chatId)) return { allowed: true }; const approved await whitelist.isApproved(msg.channel, msg.peerId); if (approved) return { allowed: true }; if (policy open) return { allowed: true }; if (policy pairing) return { allowed: false, reason: needs_pairing }; return { allowed: false, reason: not_in_allowlist }; }Session 解析负责找到或创建会话key 由 channel chatId 拼成保证同一频道同一会话。// src/sessions/manager.ts export async function resolveSession(msg, sessionConfig) { const key buildSessionKey(msg.channel, msg.chatId); let session await sessionStore.findByKey(key); if (!session) { session await sessionStore.create({ id: generateSessionId(), owner: msg.peerId, channel: msg.channel, chatId: msg.chatId, activationMode: sessionConfig.defaultActivationMode ?? passive, createdAt: new Date(), lastActiveAt: new Date(), }); } else { await sessionStore.updateLastActive(session.id, msg.timestamp); } return session; }3.4 Agent 路由与上下文加载路由按规则表匹配命中就用目标 Agent否则回落到默认 Agent。上下文加载把最近消息、记忆片段、用户画像拼成AgentContext。// src/agents/router.ts export function routeToAgent(msg, session, agents, rules) { for (const rule of rules) { if (matchesRule(msg, session, rule)) { const agent agents.find((a) a.id rule.targetAgentId); if (agent) return agent; } } return agents.find((a) a.id session.defaultAgentId) ?? agents.find((a) a.isDefault) ?? agents[0]; }3.5 Agent RuntimeReAct 多轮推理这是链路的心脏。模型不一定一问一答更常见的是多轮工具调用。runAgentLoop用异步生成器把每个 chunk 吐出来Gateway 实时转发。// src/agents/runtime.ts export async function* runAgentLoop(input, agentConfig, gateway) { let currentInput input; for (let turn 0; turn (agentConfig.maxToolTurns ?? 10); turn) { const out await modelClient.chat(currentInput); if (out.type text) { yield { type: text, text: out.text, done: true }; return; } if (out.type tool_calls) { yield { type: thinking, text: summarizeToolCalls(out.calls), done: false }; const results await executeToolCalls(out.calls, agentConfig.policy, gateway); currentInput appendToolResults(currentInput, out.calls, results); yield { type: tool_result, results, done: false }; } } yield { type: error, text: 超过最大工具调用轮次, done: true }; }3.6 Skill 执行不是函数调用是 bash这里要纠正一个常见误解OpenClaw 的 Skill 不是 TypeScript 模块而是SKILL.mdMarkdown 文件。加载阶段扫描已激活 Skill把内容注入系统提示词推理阶段模型看到说明后选择用 bash 工具执行命令执行阶段工具执行器在宿主机或 Docker 沙盒里跑命令把输出回传。# gmail-archive Skill 实际执行的命令 himalaya search from:notificationsgithub.com date:today is:unread --output json himalaya move INBOX archived-by-openclaw message_id_1 message_id_2工具执行前会过 Policy 检查禁止的工具直接拒绝需要确认的会先问用户。// src/agents/tool-executor.ts export async function executeToolCalls(calls, policy, gateway) { const results []; for (const call of calls) { if (policy.forbiddenTools?.includes(call.toolId)) { results.push({ toolId: call.toolId, error: Tool not allowed by policy }); continue; } if (policy.requireConfirmationFor?.includes(call.toolId)) { const ok await requestUserConfirmation(call, gateway); if (!ok) { results.push({ toolId: call.toolId, error: User declined }); continue; } } const result await gateway.skills.execute(call.toolId, call.arguments); results.push({ toolId: call.toolId, result }); } return results; }3.7 回复写回与持久化最终回复流式写回 Slack同时落库和记审计日志。// src/gateway/dispatch.ts const replyBuffer: string[] []; for await (const chunk of agent.handle({ msg, session, ctx })) { if (chunk.type text chunk.text) { replyBuffer.push(chunk.text); broadcastToWebClients({ type: agent:stream_chunk, payload: { sessionId: session.id, text: chunk.text } }); } if (chunk.done) { const finalText replyBuffer.join(); await channels[slack].send({ channel: slack, chatId: msg.chatId, peerId: msg.peerId, text: finalText, replyTo: msg.id, }); await sessionManager.updateLastActive(session.id, new Date()); } }4. 验证请求本地启动、事件回放与链路日志配置写完先本地跑起来。启动 Gatewayopenclaw gateway start --config ./config.toml --settings ./settings.json看到slack channel connected和gateway listening on 127.0.0.1:8787就说明接入成功。接着验证模型 Key 是否通直接打一次模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 确认返回正常再继续。不想真发 Slack 消息可以用事件回放。OpenClaw 支持把一条 Slack 事件 JSON 喂进 Gatewayopenclaw replay --event ./fixtures/slack-message.json --config ./config.tomlfixtures/slack-message.json长这样{ type: message, user: U12345, channel: C67890, text: 帮我清空收件箱里今天的 GitHub 通知顺便给我一个总结, ts: 1718000000.000100 }回放后看日志一条成功的链路应该依次出现这些行[slack] inbound adapted idslack:1718000000.000100 [security] check allowedtrue peerslack:user:U12345 [session] resolved idsess_abc123 modepassive [router] matched agentinbox-assistant [runtime] turn0 modelclaude-sonnet [runtime] tool_calls[gmail-archive] [skill] execute gmail-archive sandboxdocker [runtime] turn1 modelclaude-sonnet [gateway] reply sent len312 [audit] message_processed durationMs4820如果日志停在[security]且allowedfalse说明配对没过去跑openclaw pairing approve code。如果停在[runtime] turn0不动多半是模型 Key 或 base_url 有问题。5. 本篇常见错排查报错一401 Unauthorizedfrom model-client。九成是settings.json里api_key没填或填错或者base_url带了/v1导致路径重复。检查https://taotoken.net/api是否原样Key 是否从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 复制完整。报错二Slack 消息发了没反应。先看app.message回调有没有触发。Socket Mode 下app_token必须以xapp-开头bot_token以xoxb-开头混了就连不上。再看dm_policy私聊默认pairing没配对的消息会被静默拦截日志里只有needs_pairing。报错三Skill 执行报command not found。Skill 是 bash 命令依赖的工具比如himalaya必须装在执行环境里。sandbox docker时命令跑在容器内宿主机装了没用要在镜像里装。报错四回复被截断。Slack 单条消息上限 40000 字符sendSlackMessage会分块。如果分块后顺序乱了检查splitText是否按语义边界切别在代码块中间断开。报错五max_tool_turns超限。模型陷入工具调用循环通常是 Skill 返回的结果格式模型看不懂导致它反复重试。把log_level调到debug看tool_result的内容修正 Skill 输出格式。6. 把链路跑通之后这条链路把 Session、Agent、Channel、Skill、Security 几个抽象串成了闭环。真正上手时建议先把log_level开debug用事件回放跑通一条消息确认每一步日志都对再切到真实 Slack。模型侧统一走 TaoToken 的 KeyAgent 和 Skill 不用各自维护凭据换模型只改settings.json的 mapping。长期跑编码类 Agent 的话可以看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 把多轮工具调用的额度规划好避免跑到一半被限流打断链路。
返回列表