1. 这不是“又一个开放平台”,而是个人开发者真正能跑通 Agent 的第一块跳板
WorkBuddy 开放平台最近在技术圈里被反复提起,但多数人点开文档后很快关掉——不是因为没兴趣,而是发现它不像传统 API 平台那样“调个接口就能出结果”。它背后绑着 MCP(Model Control Protocol)协议、Agent 执行生命周期、Skill 编排逻辑、上下文状态管理这些新概念。我去年底开始接入,前两周几乎卡在“为什么我的 Skill 总是返回 400”上,后来才发现问题根本不在代码,而在对 WorkBuddy 整体执行模型的理解偏差。它不提供“HTTP 请求 → JSON 响应”的线性路径,而是一套带状态、可中断、支持多轮决策的 Agent 运行时环境。你提交的不是一次性的请求,而是一个可被平台调度、暂停、重试、回溯的执行单元。
核心关键词 workbuddy、开放平台、REST API、MCP、Agent 其实构成了一个三层嵌套结构:最外层是 WorkBuddy 提供的 REST API 接口层(用于注册、鉴权、触发、查询);中间层是 MCP 协议定义的通信语义(比如execute_skill、stream_output、pause_execution);最内层才是你写的 Agent 逻辑本身——它可能调用本地 Python 函数、调用第三方 API、甚至启动一个小型 LLM 微服务。这三层必须对齐,缺一不可。很多人失败,是因为只写了第三层(Agent 逻辑),却没按第二层(MCP 消息格式)封装,更没通过第一层(REST API)正确注册和触发。我见过太多人把 Skill 当成普通 Webhook 写,结果平台收不到execution_id,直接判定为非法调用。
这个路径适合三类人:一是想快速验证自己 Agent 构思的独立开发者,不需要搭整套基础设施;二是正在学习 Agent 架构的学生或转行者,WorkBuddy 的沙箱环境比自己从零部署 LangChain + FastAPI + Redis 省掉至少 80% 的运维成本;三是已有成熟工具链(比如内部知识库、CRM 系统)的小团队,想用最小成本把已有能力包装成可被 AI 调用的 Skill。它不是替代你现有架构的方案,而是给你加一层“AI 可理解”的语义适配层。我自己的第一个上线 Skill 是“会议纪要自动归档到 Notion”,整个开发+调试+上线用了 3 天半,其中 2 天花在读透 MCP 的execution_context字段含义上——这个字段决定了你的 Skill 是单次执行还是支持多轮交互,而文档里只用了一句话带过。
2. 整体设计思路:为什么 WorkBuddy 不让你直接写 endpoint,而要走 MCP 封装?
2.1 传统 REST API 与 MCP 驱动型 Agent 的本质差异
你习惯的 REST API 是“请求-响应”模型:客户端发 POST /v1/translate,带 body{ "text": "hello", "to": "zh" },服务端返回{ "result": "你好" }。整个过程无状态、无上下文、不可中断。而 WorkBuddy 的 Agent 执行模型是“任务-生命周期”模型。当你调用/v1/skills/{skill_id}/trigger,平台不是立刻转发请求,而是先创建一个execution_id,然后按 MCP 协议向你的 Skill 服务发送execute_skill消息,里面包含完整的执行上下文(用户输入、历史对话、可用工具列表、超时设置、重试策略)。你的 Skill 收到后,可以:
- 立即返回结果(简单场景);
- 返回
{"status": "running", "progress": "30%"}并保持长连接,后续由平台推送stream_output; - 返回
{"status": "paused", "reason": "awaiting_user_confirmation"},等待用户点击“确认继续”后再恢复; - 甚至主动调用平台提供的
request_tool_use接口,申请调用另一个 Skill(比如先查天气,再生成穿衣建议)。
这种设计不是为了炫技,而是解决真实 Agent 场景中的三个硬伤:
- 长耗时任务无法友好反馈:比如处理 100 页 PDF,传统 API 只能超时或轮询,而 MCP 支持流式进度推送;
- 多步决策缺乏状态锚点:用户说“帮我订明天下午三点去机场的车”,Agent 需查航班、查路况、比价、确认支付,每一步失败都需要回退或提示,MCP 的
execution_context就是这个状态快照; - 工具调用权限需集中管控:你不能让每个 Skill 随意调用数据库或发邮件,平台通过 MCP 的
tool_request和tool_response机制统一审计和限流。
我最初尝试绕过 MCP,直接用 Flask 写了个/api/translateendpoint,然后在 WorkBuddy 后台填这个 URL。结果平台调用时始终报错invalid_mcp_message_format。查日志才发现,WorkBuddy 的请求体根本不是标准 JSON,而是带mcp_version: "1.2"、message_type: "execute_skill"的 MCP 格式消息。它不接受“裸”HTTP 接口,只认 MCP 协议——这是设计底线,不是可选项。
2.2 为什么选择 REST API + MCP 组合,而不是纯 WebSocket 或 gRPC?
WorkBuddy 开放平台没有采用 WebSocket 或 gRPC,是有明确取舍的。WebSocket 适合高频双向通信(如实时聊天),但对 Skill 开发者来说,意味着必须维护长连接、处理心跳、应对断连重连,增加了 70% 的基础代码量。gRPC 虽高效,但要求开发者安装 protoc、编译 .proto 文件、处理二进制序列化,对 Python/JS 主力军不友好。而 REST + MCP 的组合,本质是“用最通用的传输层(HTTP),承载最灵活的语义层(MCP)”。
具体到实现,WorkBuddy 的 REST API 只做四件事:
POST /v1/applications:注册应用,获取client_id和client_secret;POST /v1/skills:注册 Skill,提交元信息(名称、描述、图标、MCP 兼容版本);POST /v1/skills/{id}/trigger:触发执行,返回execution_id;GET /v1/executions/{id}:查询执行状态(含日志、输出、错误堆栈)。
所有业务逻辑、状态流转、工具调用,都通过 MCP 消息在 Skill 服务和平台之间完成。这意味着你的 Skill 服务可以是任何语言写的 HTTP 服务,只要它能解析 MCP 消息、按规范返回响应即可。我用 Python FastAPI 实现的第一个 Skill,核心逻辑只有 47 行代码,其中 32 行是解析execution_context和构造execute_skill响应,剩下 15 行才是真正的业务(调 Notion API)。如果换成 gRPC,光是生成 client stub 就得 200 行起步。
2.3 Skill 架构分层:从“函数”到“可调度单元”的跃迁
很多开发者以为写个 Skill 就是写个函数,比如def translate(text, to_lang)。但在 WorkBuddy 体系里,Skill 必须是一个可被平台识别、调度、监控、计费的独立运行单元。它有明确的三层结构:
- 接入层(Adapter):负责接收平台 HTTP 请求,解析 MCP 消息,校验签名,转换为内部调用参数。这一层必须严格遵循 MCP v1.2 规范,字段名、类型、必选/可选属性都不能错。比如
execution_context中的user_id是字符串,但如果你传了数字,平台会直接拒绝。 - 逻辑层(Core Logic):这才是你熟悉的业务代码。但它不能直接操作 HTTP 响应,而要返回一个标准化的
SkillResult对象,包含output(最终结果)、intermediate_steps(中间步骤,用于调试)、tools_used(调用的外部工具列表)。 - 适配层(Tool Adapter):当 Skill 需要调用其他系统(如数据库、邮件服务)时,不能硬编码连接字符串,而要通过平台提供的
tool_call接口申请。比如你要发邮件,得先返回{"tool_request": {"name": "send_email", "parameters": {...}}},平台审核通过后,再向你的服务推送tool_response。
我踩过最大的坑是在逻辑层直接用了requests.post("https://internal-api.example.com/notify")。测试时一切正常,但上线后发现平台防火墙屏蔽了该域名,且日志里没有任何错误提示——因为请求根本没发出,被网络层拦截了。后来改成用tool_call申请http_request工具,平台自动注入代理和认证头,问题瞬间解决。这说明 WorkBuddy 的设计哲学是:把基础设施依赖显式化、可控化、可审计化,而不是让开发者在黑盒里瞎猜。
3. 核心细节解析:从注册应用到 Skill 上线的 7 个关键实操节点
3.1 应用注册:别只盯着 client_secret,redirect_uri的坑比想象中深
注册应用看似简单:填名称、描述、官网,拿到client_id和client_secret。但redirect_uri这个字段,90% 的新手填错。它不是你前端页面的地址,而是 WorkBuddy 在 OAuth 流程中回调的后端地址。比如你前端在https://myapp.com/login触发登录,WorkBuddy 会重定向到https://api.myapp.com/auth/callback,这个https://api.myapp.com/auth/callback才是redirect_uri。
常见错误:
- 填成
https://myapp.com/callback(前端地址,平台无法访问); - 填成
http://localhost:3000/callback(本地开发用,上线必须换); - 填多个 URI 用空格分隔(正确方式是用英文逗号
,分隔); - URI 末尾带
/(平台校验严格匹配,https://api.com/callback和https://api.com/callback/视为不同)。
我第一次填错,导致用户授权后页面白屏,控制台报invalid_redirect_uri。查文档才发现,WorkBuddy 的 OAuth 2.0 实现要求redirect_uri必须精确匹配注册值,且必须是 HTTPS(除非 localhost)。解决方案是:开发阶段用https://localhost:8000/callback,配合 mkcert 生成本地证书;上线后用 Nginx 反向代理,确保redirect_uri指向你的 API 服务,而非前端。
提示:
redirect_uri一旦注册无法修改,只能删掉重建应用。所以建议首次注册时就规划好环境:dev、staging、prod各建一个应用,对应不同的redirect_uri。
3.2 Skill 元数据注册:icon_url 不是摆设,它影响用户信任度
注册 Skill 时,除了name、description、endpoint_url,icon_url是最容易被忽略但最关键的字段。它不只是显示在 WorkBuddy 工作台上的小图标,更是平台判断 Skill 可信度的信号之一。WorkBuddy 会检查icon_url是否满足:
- 必须是 HTTPS 协议;
- 图片尺寸必须是 64x64 像素(非此尺寸会被拉伸变形);
- 文件大小不超过 100KB;
- 不能是 base64 编码的 data URL(平台不支持)。
我最初用 Figma 导出的 PNG,尺寸是 128x128,上传后图标在工作台显示为模糊马赛克。后来用 ImageMagick 压缩并裁剪:convert icon.png -resize 64x64 -quality 85 icon_64.png,问题解决。更隐蔽的坑是 CDN 缓存:我更新了图标,但用户看到的还是旧版。解决方案是在 URL 后加时间戳参数:https://cdn.example.com/icon.png?v=20240520。
另外,description字段有 200 字限制,但很多人写成“本 Skill 用于翻译文本”。这毫无竞争力。更好的写法是:“一键将会议录音转文字并翻译成中文,支持保留说话人标记和时间戳,输出 Markdown 格式”。前者是功能描述,后者是场景价值,用户一眼就知道能解决什么问题。
3.3 MCP 消息解析:execution_context里的session_id是状态管理的钥匙
当你收到平台发来的execute_skill请求,body 类似这样:
{ "mcp_version": "1.2", "message_type": "execute_skill", "execution_id": "exec_abc123", "execution_context": { "user_id": "usr_xyz789", "session_id": "sess_def456", "input": "把这份合同翻译成英文", "history": [ {"role": "user", "content": "帮我查下张三的合同"}, {"role": "assistant", "content": "已找到,合同编号 CT2024-001"} ], "available_tools": ["notion_read", "translate_api"], "timeout_ms": 30000 } }其中session_id是关键。它标识了用户本次会话的上下文,不是每次请求都变。比如用户连续问:
- “把这份合同翻译成英文”
- “再把翻译结果发给李四”
- “顺便告诉他 deadline 是下周三”
这三个请求的session_id相同,但execution_id不同。这意味着你的 Skill 可以基于session_id缓存一些中间结果(比如第一次翻译后的英文文本),避免重复调用翻译 API。我做的“合同处理”Skill 就利用这点:第一次收到翻译请求,调用 DeepSeek API 得到结果,并存入 Redis(key 为session:{session_id}:translation);第二次收到“发给李四”,直接从 Redis 读取,省去 2 秒 API 延迟。
注意:
session_id由平台生成,你不能自己生成或修改。缓存时务必加上过期时间(建议 30 分钟),避免内存泄漏。
3.4 Skill 响应构造:status字段决定你是“执行者”还是“协调者”
MCP 规范定义了 Skill 响应的status字段有四个合法值:
"success":任务完成,返回最终结果;"running":任务进行中,需后续流式推送;"paused":需要用户干预,比如确认敏感操作;"failed":执行出错,附带error_code和error_message。
很多人只用"success"和"failed",错过了 Agent 的核心能力。比如“发送邮件”Skill,如果直接返回"success",用户不知道邮件是否真发出去;如果返回"running",你可以后续推送{"progress": "50%", "message": "正在连接 SMTP 服务器..."},最后再发一次"success"。
更高级的用法是"paused"。我做的“财务报销”Skill,当检测到报销金额超过 5000 元时,返回:
{ "status": "paused", "pause_reason": "amount_exceeds_approval_limit", "required_action": { "type": "confirm", "message": "报销金额 ¥5,200 超过部门审批限额(¥5,000),是否提交至总监审批?", "options": ["是", "否"] } }平台会弹出确认框,用户点击“是”后,再向你的 Skill 发送resume_execution消息,携带用户选择。这时你才真正调用财务系统 API。这种设计把风控逻辑从代码里抽出来,交由平台 UI 统一处理,既安全又一致。
3.5 工具调用(Tool Calling):不是“调 API”,而是“申请权限”
当 Skill 需要调用外部服务,不能直接发 HTTP 请求,而要通过 MCP 的tool_request。例如,你想查用户在 Notion 中的待办事项:
{ "status": "success", "tool_request": { "name": "notion_list_tasks", "parameters": { "database_id": "db_abc123", "filter": {"property": "Status", "equals": "To Do"} } } }平台收到后,会:
- 校验
notion_list_tasks是否在available_tools列表中; - 检查当前用户是否有该工具的调用权限(管理员可配置);
- 注入认证凭据(如 Notion 的 integration token);
- 代为调用,并将结果封装成
tool_response发回你的 Skill。
这个过程的关键是:你永远看不到原始 API 密钥。所有敏感凭据由平台托管,Skill 只通过抽象的工具名操作。我曾试图在tool_request的parameters里硬编码 token,结果平台直接返回invalid_tool_parameters错误。后来才明白,parameters只能传业务参数(如database_id),认证信息由平台自动注入。
3.6 本地调试:用workbuddy-cli模拟平台请求,比 Postman 高效 10 倍
WorkBuddy 官方提供了workbuddy-cli工具(npm install -g workbuddy-cli),它能模拟平台所有请求,无需部署到公网。调试流程是:
- 启动你的 Skill 服务(如
uvicorn main:app --host 0.0.0.0 --port 8000); - 运行
workbuddy-cli trigger --skill-id sk_123 --input "hello world" --env dev; - CLI 自动构造符合 MCP v1.2 的请求,发送到
http://localhost:8000/mcp; - 显示完整请求/响应日志,包括 HTTP 状态码、MCP 字段校验结果。
比 Postman 高效的地方在于:
- 自动生成
execution_id、mcp_version、message_type; - 自动计算并添加
X-WorkBuddy-Signature签名头(需配置client_secret); - 内置 MCP 字段校验器,比如告诉你
"execution_context.user_id is required but missing"。
我用它调试execution_context解析逻辑时,发现history字段有时为空数组[],有时为null。文档没写清楚,但 CLI 的错误提示明确说"history must be an array",让我立刻补上默认值处理。
3.7 上线前必做三件事:签名验证、超时设置、错误分类
上线前,务必验证以下三点,否则生产环境会频繁失败:
- 签名验证:WorkBuddy 所有请求都带
X-WorkBuddy-Signature头,格式为sha256=<hex_digest>,其中 digest 是body + client_secret的 SHA256。必须验证,否则恶意请求可伪造execution_id。我用 Python 的hmac模块实现:
import hmac import hashlib def verify_signature(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature.split('sha256=')[1], expected)超时设置:Skill 服务的 HTTP 超时必须小于平台设置的
timeout_ms(默认 30s)。我在 FastAPI 中设timeout=25,留 5 秒缓冲。如果设成 35s,平台会在 30s 后取消执行,但你的服务还在跑,造成资源浪费。错误分类:不要所有错误都返回
{"status": "failed", "error_message": "unknown error"}。WorkBuddy 期望具体的error_code,如"notion_api_rate_limit"、"translate_quota_exceeded"。平台会根据 code 做不同处理(如限流时自动重试,配额超限时提示用户升级)。
4. 实操过程:从零搭建一个“会议纪要智能归档”Skill 的完整记录
4.1 需求拆解:用户要的不是“转文字”,而是“可追溯的归档动作”
用户需求原文:“把会议录音自动转文字,提取关键结论,存到 Notion 指定数据库,并生成摘要发 Slack”。表面看是语音转文字 + NLP + API 调用,但深入分析发现三个隐藏需求:
- 可追溯性:用户要能查到“哪次会议、谁发起、何时归档、用了哪个模型”;
- 可干预性:如果 NLP 提取的结论有误,用户应能手动编辑再保存;
- 可审计性:管理员要能看到所有归档记录的操作日志。
这意味着 Skill 不能是黑盒流水线,而要有明确的状态节点。我设计了四阶段执行流:
transcribe_audio:调用 Whisper API,返回原始文字;extract_conclusions:用 LLM 提炼 3-5 条结论,返回结构化 JSON;create_notion_page:在 Notion 数据库创建页面,填入文字和结论;post_to_slack:发摘要到 Slack 频道。
每个阶段都对应一个 MCP 的status状态,用户可在任一阶段暂停或重试。
4.2 环境准备:用 Docker Compose 一键启动开发环境
不用折腾本地依赖,我用 Docker Compose 管理所有服务:
# docker-compose.yml version: '3.8' services: skill-app: build: . ports: ["8000:8000"] environment: - WORKBUDDY_CLIENT_SECRET=your_secret - NOTION_INTEGRATION_TOKEN=secret_xxx - SLACK_BOT_TOKEN=xoxb-xxx depends_on: [redis] redis: image: redis:7-alpine ports: ["6379:6379"] ngrok: image: wernight/ngrok:latest command: ngrok http --domain=your-subdomain.ngrok.io 8000 ports: ["4040:4040"]ngrok服务自动暴露本地8000端口为公网 URL,填入 WorkBuddy 后台的endpoint_url。redis用于缓存session_id关联的中间结果。整个环境docker-compose up -d一条命令启动,比手动配环境快 5 倍。
4.3 核心代码实现:FastAPI + MCP 适配器的 62 行主逻辑
以下是main.py的核心部分(已脱敏,保留关键结构):
from fastapi import FastAPI, Request, HTTPException from pydantic import BaseModel import hmac import hashlib import json import redis import os app = FastAPI() r = redis.Redis(host='redis', port=6379, db=0) class MCPRequest(BaseModel): mcp_version: str message_type: str execution_id: str execution_context: dict @app.post("/mcp") async def handle_mcp(request: Request): body = await request.body() signature = request.headers.get("X-WorkBuddy-Signature") # 1. 签名验证 if not verify_signature(body, signature, os.getenv("WORKBUDDY_CLIENT_SECRET")): raise HTTPException(401, "Invalid signature") # 2. 解析 MCP 消息 try: data = json.loads(body) req = MCPRequest(**data) except Exception as e: raise HTTPException(400, f"Invalid MCP format: {e}") # 3. 提取关键上下文 session_id = req.execution_context.get("session_id", "unknown") user_input = req.execution_context.get("input", "") # 4. 从缓存读取或初始化执行状态 state_key = f"state:{req.execution_id}" state = r.hgetall(state_key) or {} # 5. 根据当前状态执行对应逻辑 if not state: # 首次执行:转文字 transcription = await transcribe_audio(user_input) r.hset(state_key, mapping={"transcription": transcription, "step": "transcribed"}) return {"status": "success", "output": {"transcription": transcription}} elif state.get(b"step") == b"transcribed": # 第二步:提取结论 conclusions = await extract_conclusions(state[b"transcription"].decode()) r.hset(state_key, mapping={"conclusions": json.dumps(conclusions), "step": "concluded"}) return {"status": "success", "output": {"conclusions": conclusions}} elif state.get(b"step") == b"concluded": # 第三步:存 Notion page_id = await create_notion_page( state[b"transcription"].decode(), json.loads(state[b"conclusions"].decode()) ) r.hset(state_key, mapping={"page_id": page_id, "step": "notioned"}) return {"status": "success", "output": {"notion_page_id": page_id}} else: # 最后一步:发 Slack await post_to_slack(state[b"transcription"].decode()) r.delete(state_key) # 清理状态 return {"status": "success", "output": {"done": True}} def verify_signature(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(signature.split('sha256=')[1], expected)这段代码体现了 WorkBuddy Skill 的典型模式:状态驱动、分步执行、缓存协同。62 行代码覆盖了签名、解析、状态管理、分步逻辑,比写一个纯 REST API 精简得多。
4.4 MCP 消息构造细节:execution_context字段的实测兼容性表
execution_context是 Skill 的输入核心,但不同场景下字段存在性不同。我实测了 200+ 次触发,总结出字段兼容性:
| 字段名 | 是否必填 | 类型 | 说明 | 实测备注 |
|---|---|---|---|---|
user_id | 是 | string | 用户唯一标识 | 平台保证非空,长度 12-32 字符 |
session_id | 是 | string | 会话 ID | 同一会话内所有请求相同 |
input | 是 | string | 用户原始输入 | 可能含 emoji、换行符,需 utf-8 处理 |
history | 否 | array | 对话历史 | 首次请求为[],后续为[{"role":"user","content":"..."}] |
available_tools | 否 | array | 可用工具列表 | 若未配置工具,此字段不存在 |
timeout_ms | 否 | number | 超时毫秒数 | 默认 30000,可被平台覆盖 |
特别注意history字段:当用户首次发起请求时,history是空数组[],不是null。如果代码里写if context['history']:会误判为 False,导致跳过历史分析逻辑。正确写法是if context.get('history') and len(context['history']) > 0:。
4.5 上线发布:WorkBuddy 后台的 5 个发布检查点
在 WorkBuddy 开放平台后台发布 Skill,必须通过以下检查:
- Endpoint 可达性测试:平台会向你的
endpoint_url发送GET /health请求,必须返回200 OK和{"status": "healthy"}。我一开始返回 HTML,结果卡在“健康检查失败”。 - MCP 兼容性扫描:平台用内置解析器检查你的响应是否符合 MCP v1.2 schema。比如
status字段值必须是枚举值,不能是"ok"。 - 图标合规性检查:自动下载
icon_url,验证尺寸、格式、大小。PNG/JPEG 支持,GIF 不支持。 - 权限声明审查:如果你的 Skill 声明需要
notion_write权限,平台会检查你是否在available_tools中列出了notion_write。 - 沙箱环境执行测试:平台用预设用例(如
input: "test")触发 Skill,验证能否在 10 秒内返回status: "success"。
我第 3 次发布才通过,原因是icon_url的图片用了 WebP 格式(平台只支持 PNG/JPEG),改用 PNG 后立即通过。
5. 常见问题与排查技巧实录:那些文档里不会写的实战经验
5.1 问题速查表:高频错误代码与根因定位
| 错误代码 | HTTP 状态码 | 典型现象 | 根本原因 | 解决方案 |
|---|---|---|---|---|
invalid_mcp_message_format | 400 | 平台日志显示“MCP 解析失败” | execution_context缺少必填字段,或status值非法 | 用workbuddy-cli检查请求体,对照 MCP v1.2 schema |
signature_verification_failed | 401 | 所有请求都被拒 | X-WorkBuddy-Signature计算错误,或client_secret配错 | 确保 body 是原始字节(非 decoded JSON),secret 无空格 |
execution_timeout | 408 | 技能执行一半中断 | Skill 服务 HTTP 超时 > 平台timeout_ms | 设 Skill 超时为timeout_ms - 5000 |
tool_not_available | 403 | tool_request被拒绝 | available_tools未声明该工具,或用户无权限 | 在 Skill 注册时勾选所需工具,在后台分配权限 |
rate_limit_exceeded | 429 | 突然大量失败 | 平台对 Skill 的 QPS 限流(默认 5 QPS) | 在后台申请提额,或加本地缓存减少调用 |
我遇到过一次rate_limit_exceeded,原因是用户批量上传 100 个音频文件,触发 100 次 Skill。临时方案是加 Redis 计数器,1 秒内只允许 5 次调用;长期方案是联系 WorkBuddy 运营提额。
5.2 日志调试黄金法则:三段式日志结构
WorkBuddy 的执行日志只显示最后 100 行,且不区分服务端/客户端。我强制在 Skill 里用三段式日志:
# 格式:[EXECUTION_ID] [STAGE] [MESSAGE] logger.info(f"[{execution_id}] TRANSCRIBE_START Input length: {len(user_input)} chars") logger.info(f"[{execution_id}] TRANSCRIBE_END Result: {transcription[:50]}...") logger.error(f"[{execution_id}] NOTION_ERROR Status: {resp.status_code}, Body: {resp.text}")这样在平台日志里搜索exec_abc123,就能串起完整执行链。比用print()好 10 倍。
5.3 签名验证的五个致命陷阱
- Body 必须是原始字节:
json.loads(request.body())后再签名是错的,必须用await request.body()的原始 bytes。 - Secret 末尾换行符:复制
client_secret时可能带\n,用strip()清理。 - Signature 头格式:必须是
sha256=abcdef123...,不能是SHA256: abcdef。 - HMAC 比较用
hmac.compare_digest:防止时序攻击,不能用==。 - 大小写敏感:
X-WorkBuddy-Signature不能写成x-workbuddy-signature。
我栽在第 2 条:client_secret复制时带了换行,导致签名永远不匹配。用repr(secret)打印才发现'\n'。
5.4 本地开发与生产环境的三大差异
| 差异点 | 本地开发 | 生产环境 | 应对方案 |
|---|---|---|---|
| 网络可达性 | localhost可访问 | 平台无法访问localhost | 用 ngrok 或云服务器部署 |
| SSL 证书 | mkcert 生成自签名 | 平台要求有效 HTTPS | 用 Let's Encrypt 或云厂商免费证书 |
| 环境变量 | .env文件 | 平台后台配置 | 代码中os.getenv("KEY", "default"),避免崩溃 |
特别提醒:WorkBuddy 生产环境强制 HTTPS,HTTP 的endpoint_url会被拒绝。我上线前忘了配 Nginx SSL,结果所有请求 502。
5.5 Agent 执行终止的三种真实场景与对策
agent execution terminated due to error.这个错误很宽泛,实际分三类:
- 平台侧终止:如用户取消、超时、配额用尽。此时
execution_id仍有效,可查GET /v1/executions/{id}获取termination_reason。 - Skill 侧终止:你的代码抛出未捕获异常。WorkBuddy 会捕获并记录堆栈,但
status仍为"failed"。对策:全局异常处理器,返回结构化错误。 - 网络侧终止:Skill 服务宕机或网络中断。平台会重试 3 次,间隔 1s。对策:确保服务高可用,用 PM2