
1. 这不是“又一个开放平台”而是个人开发者真正能跑通 Agent 的第一块跳板WorkBuddy 开放平台最近在技术圈里被反复提起但多数人点开文档后很快关掉——不是因为没兴趣而是发现它不像传统 API 平台那样“调个接口就能出结果”。它背后绑着 MCPModel 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 或 gRPCWorkBuddy 开放平台没有采用 WebSocket 或 gRPC是有明确取舍的。WebSocket 适合高频双向通信如实时聊天但对 Skill 开发者来说意味着必须维护长连接、处理心跳、应对断连重连增加了 70% 的基础代码量。gRPC 虽高效但要求开发者安装 protoc、编译 .proto 文件、处理二进制序列化对 Python/JS 主力军不友好。而 REST MCP 的组合本质是“用最通用的传输层HTTP承载最灵活的语义层MCP”。具体到实现WorkBuddy 的 REST API 只做四件事POST /v1/applications注册应用获取client_id和client_secretPOST /v1/skills注册 Skill提交元信息名称、描述、图标、MCP 兼容版本POST /v1/skills/{id}/trigger触发执行返回execution_idGET /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_secretredirect_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_urlicon_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?v20240520。另外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 得到结果并存入 Rediskey 为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 devCLI 自动构造符合 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头格式为sha256hex_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 中设timeout25留 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 条结论返回结构化 JSONcreate_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_SECRETyour_secret - NOTION_INTEGRATION_TOKENsecret_xxx - SLACK_BOT_TOKENxoxb-xxx depends_on: [redis] redis: image: redis:7-alpine ports: [6379:6379] ngrok: image: wernight/ngrok:latest command: ngrok http --domainyour-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(hostredis, port6379, db0) 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, fInvalid MCP format: {e}) # 3. 提取关键上下文 session_id req.execution_context.get(session_id, unknown) user_input req.execution_context.get(input, ) # 4. 从缓存读取或初始化执行状态 state_key fstate:{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(bstep) btranscribed: # 第二步提取结论 conclusions await extract_conclusions(state[btranscription].decode()) r.hset(state_key, mapping{conclusions: json.dumps(conclusions), step: concluded}) return {status: success, output: {conclusions: conclusions}} elif state.get(bstep) bconcluded: # 第三步存 Notion page_id await create_notion_page( state[btranscription].decode(), json.loads(state[bconclusions].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[btranscription].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_format400平台日志显示“MCP 解析失败”execution_context缺少必填字段或status值非法用workbuddy-cli检查请求体对照 MCP v1.2 schemasignature_verification_failed401所有请求都被拒X-WorkBuddy-Signature计算错误或client_secret配错确保 body 是原始字节非 decoded JSONsecret 无空格execution_timeout408技能执行一半中断Skill 服务 HTTP 超时 平台timeout_ms设 Skill 超时为timeout_ms - 5000tool_not_available403tool_request被拒绝available_tools未声明该工具或用户无权限在 Skill 注册时勾选所需工具在后台分配权限rate_limit_exceeded429突然大量失败平台对 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 头格式必须是sha256abcdef123...不能是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用 Lets Encrypt 或云厂商免费证书环境变量.env文件平台后台配置代码中os.getenv(KEY, default)避免崩溃特别提醒WorkBuddy 生产环境强制 HTTPSHTTP 的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