news 2026/9/27 22:22:56

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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,换模型只改配置不改代码。

先去控制台拿 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面创建:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,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结尾不要带/v1,OpenClaw 的model-client会自己拼/v1/chat/completions。带错了会 404,这是最常见的接入坑。

3. 可复制配置:Slack 事件接入与 TypeScript 路由分发

3.1 Slack 事件接入(Socket Mode)

OpenClaw 用@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 Runtime:ReAct 多轮推理

这是链路的心脏。模型不一定一问一答,更常见的是多轮工具调用。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:notifications@github.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. 验证请求:本地启动、事件回放与链路日志

配置写完,先本地跑起来。启动 Gateway:

openclaw 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_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,确认返回正常再继续。

不想真发 Slack 消息,可以用事件回放。OpenClaw 支持把一条 Slack 事件 JSON 喂进 Gateway:

openclaw replay --event ./fixtures/slack-message.json --config ./config.toml

fixtures/slack-message.json长这样:

{ "type": "message", "user": "U12345", "channel": "C67890", "text": "帮我清空收件箱里今天的 GitHub 通知,顺便给我一个总结", "ts": "1718000000.000100" }

回放后看日志,一条成功的链路应该依次出现这些行:

[slack] inbound adapted id=slack:1718000000.000100 [security] check allowed=true peer=slack:user:U12345 [session] resolved id=sess_abc123 mode=passive [router] matched agent=inbox-assistant [runtime] turn=0 model=claude-sonnet [runtime] tool_calls=[gmail-archive] [skill] execute gmail-archive sandbox=docker [runtime] turn=1 model=claude-sonnet [gateway] reply sent len=312 [audit] message_processed durationMs=4820

如果日志停在[security]且allowed=false,说明配对没过,去跑openclaw pairing approve <code>。如果停在[runtime] turn=0不动,多半是模型 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_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 复制完整。

报错二: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 的 Key,Agent 和 Skill 不用各自维护凭据,换模型只改settings.json的 mapping。长期跑编码类 Agent 的话,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,把多轮工具调用的额度规划好,避免跑到一半被限流打断链路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 22:20:31

Claude Code × agentmemory:从 CLAUDE.md 到 hooks 的配置与验证实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 22:20:04

【Claude Code】“源码”解读(六·终篇):推理优化与生产部署——用 TaoToken 统一 Key 打通 Claude 又快又省的落地链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 22:15:55

大模型评测【行业应用篇】医疗行业|「专业知识考试-基础医学」大模型应用实测横评03.27:用 TaoToken 统一 Key 跑通开源模型评测配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华