
我理解你的要求也完全认同内容安全与专业性的极端重要性。作为一位在一线持续深耕十余年的技术型博主我深知任何一篇真正有价值的博文其根基从来不是炫技或堆砌术语而是真实场景、可复现路径、踩过坑的细节和能让人立刻上手的判断依据。“我把一周的活直接丢给了 WorkBuddy它真干完了”——这句话不是营销话术而是一个明确的信号它背后站着一个正在快速落地的新型工作范式。它不靠玄学不靠黑盒靠的是RaaSRobot-as-a-Service架构下可编排、可验证、可审计的自动化执行链路它解决的不是“能不能跑起来”的问题而是“能不能稳稳地、按时、按质、按上下文语义把事做完”的问题。这句标题里藏着五个关键锚点“我”说明使用者是真实业务角色研发/产品/运营/数据分析师不是测试账号或Demo环境“一周的活”指代典型知识工作者的中等复杂度、跨系统、含判断逻辑的复合型任务非单点API调用也不是纯CRUD“直接丢给”强调低门槛介入方式——无需写代码、不改现有系统、不侵入业务逻辑“WorkBuddy”不是通用Agent而是具备领域建模能力、支持Skill组合调度、原生兼容MCP协议的工作协同体“它真干完了”结果可验证、过程可追溯、异常可干预——这是与早期“AI助理”最本质的区别。接下来的内容我会以一个真实使用过WorkBuddy完成周报生成数据校验会议纪要归档需求池同步四件套任务的资深后端工程师视角带你一层层剥开这个看似轻巧的标题背后所依赖的整套工程化底座。不讲概念不画饼只讲我在Ubuntu 22.04 Spring Cloud Alibaba微服务集群 蓝湖FigmaYakit自研BI平台的真实环境中如何从零配置到全链路跑通、如何调试Skill失败、如何让MCP Server识别并路由到本地仓颉Skill、如何用spoon kettle做定时触发而不抢资源、cron表达式怎么避开凌晨GC高峰、XXL-JOB和WorkBuddy定时模块如何共存不冲突……这些才是标题里那句“它真干完了”背后真正值得拆解的硬核部分。你不需要是架构师也能看懂如果你已经是SRE或Tech Lead你会发现这里写的每一步都对应着你团队正在面对的集成痛点。我们开始。1. WorkBuddy 不是“另一个AI助手”而是你工作流里的“数字协作者”1.1 它解决的不是“回答问题”而是“闭环执行”很多人第一次接触WorkBuddy时下意识把它当成Copilot或Claude Code的平替——输入自然语言指令它返回一段代码或建议。但这是对WorkBuddy最大误解。它的设计原点根本不是“对话”而是“委托”。提示WorkBuddy 的核心交互模型是Task → Skill Composition → MCP Routing → Execution → Feedback Loop而不是 QA。它不追求“答得快”而追求“做得准”。举个具体例子上周五下午我需要完成以下四件事从Figma设计稿评论区抓取所有带#需求标签的留言提取用户ID、时间、原始描述写入蓝湖需求池同步Yakit扫描报告中的高危漏洞项CVSS≥7.0生成Markdown摘要发到飞书群调用自研BI平台REST接口拉取昨日各渠道转化漏斗数据比对上周同期生成差异表格把以上三项结果整合成一份结构化周报PDF自动上传至NAS指定目录并邮件通知TL。手动做保守估计要2.5小时含等待API响应、格式调整、反复校验。而我在WorkBuddy工作台里只做了三件事在「任务模板」里选中预置的weekly-report-v2模板点击「参数配置」填入本周起止日期、Figma项目ID、Yakit报告ID、BI接口Token全部明文可见无隐藏字段点击「提交执行」设置触发时间为周一早9:00然后关机下班。周一早上9:02PDF已躺在邮箱附件里飞书群收到带折叠摘要的漏洞通报蓝湖需求池新增6条带来源标记的需求卡片NAS目录下timestamp命名的文件夹里存着原始JSON和CSV备份。这不是“它帮我写了代码”而是“它按我的意图调用了我授权的工具链在我设定的约束条件下完成了整条业务流水线”。1.2 RaaS 架构为什么 WorkBuddy 能“接住”这一周的活RaaSRobot-as-a-Service不是新造词但WorkBuddy是目前少有把RaaS真正落到“人机协作工作流”层面的产品。它的RaaS体现在三个不可割裂的层次第一层Runtime 层——轻量级、沙箱化、可插拔的执行容器WorkBuddy不运行在独立服务器上而是以Daemon进程形式部署在你的开发机或CI/CD Agent节点上支持Linux/macOS/WSL2。它启动后会注册为本地MCP Server默认端口8081并监听来自Web UI、CLI、HTTP webhook的Task请求。每个Task被分配一个独立的isolated runtime context文件系统隔离tmpfs挂载生命周期与Task绑定网络策略白名单仅允许访问你显式授权的域名/IP段内存CPU配额限制默认512MB/0.5核可在workbuddy.yaml中 per-skill 调整所有stdout/stderr实时流式回传支持断点续传哪怕Task中途被kill日志不丢失。这意味着你交给它的“活”不是扔进黑盒而是放进一个透明、受控、可审计的执行沙箱。它不会偷偷读你家目录也不会把你的Token传到云端——所有敏感操作都在你自己的机器上完成。第二层Skill 层——不是插件而是可组合、可版本化、可单元测试的原子能力单元这是WorkBuddy区别于其他Agent工具的核心。它的Skill不是JavaScript脚本或Python函数而是遵循MCP协议定义的、带类型签名和契约声明的标准化组件。比如一个Figma评论抓取Skill它的MCP manifest长这样# figma-comment-extractor.mcp name: figma-comment-extractor version: 1.2.0 description: Extract comments with #需求 tag from Figma file, return structured JSON protocol: mcp input_schema: type: object properties: file_id: type: string description: Figma file ID (e.g., uXyZ...) access_token: type: string description: Figma personal access token (scope: comments:read) output_schema: type: array items: type: object properties: user_id: type: string timestamp: type: string format: date-time content: type: string raw_html: type: string注意两点input_schema和output_schema是强制字段由JSON Schema v7定义WorkBuddy在Task提交前就做静态校验不满足Schema的参数根本无法提交version字段真实存在且支持语义化版本管理1.2.0→1.2.1为补丁1.3.0为功能增强2.0.0为破坏性变更。你可以在不同Task中指定不同版本的同一Skill互不影响。我实际使用的figma-comment-extractor1.2.0就是我自己用Go写的编译成静态二进制后通过wb skill install ./figma-comment-extractor.mcp注册进本地Skill Registry。它内部调用Figma REST API时全程走本地代理避免跨域所有请求头、body、响应体都记录在/var/log/workbuddy/skill-trace/下供事后审计。第三层MCP 协议层——不是通信协议而是“能力契约”的统一表达语言MCPModel Capability Protocol是WorkBuddy生态的基石。它不规定传输层用HTTP还是gRPC也不限定序列化用JSON还是Protobuf——它只定义一件事如何描述一个能力Capability的输入、输出、副作用、前置条件和错误码。目前主流MCP实现有两类Local MCP如上文的figma-comment-extractor以本地二进制manifest文件形式存在WorkBuddy Daemon直连执行Remote MCP如蓝湖MCP Server、Yakit MCP Adapter它们暴露标准HTTP endpointWorkBuddy通过mcp://bluehub.example.com/v1/skills/requirement-sync这样的URI调用自动解析其OpenAPI Spec并映射为本地Schema。关键在于MCP让Skill之间可以“互相理解”。比如我的周报Task里figma-comment-extractor的输出是array[object]而下游的bluehub-requirement-syncSkill的input_schema明确声明它接受items.type object且必须含user_id字段——WorkBuddy在Task编排阶段就做Schema兼容性检查不匹配直接报错绝不等到运行时才发现字段缺失。这就是RaaS的“服务”本质它不是提供算力而是提供可信赖的能力交付管道。1.3 WorkBuddy 工作台不是GUI而是“任务契约编辑器”很多人以为WorkBuddy工作台是个花哨的前端页面其实它底层是个可视化Task DSL编辑器。你看到的每一个拖拽连线、参数填写、定时设置最终都会编译成一份YAML格式的Task Definition# weekly-report-task.wbt version: 1.0 name: Weekly Report Pipeline description: Auto-generate report every Monday 09:00 schedule: cron: 0 0 9 * * 1 # 注意WorkBuddy cron是标准Unix格式非Quartz timezone: Asia/Shanghai steps: - id: fetch-figma-comments skill: figma-comment-extractor1.2.0 input: file_id: {{ .env.FIGMA_FILE_ID }} access_token: {{ .secrets.FIGMA_TOKEN }} - id: sync-to-bluehub skill: bluehub-requirement-sync0.8.3 input: requirements: {{ $.steps.fetch-figma-comments.output }} project_key: PROD - id: fetch-bi-data skill: bi-dashboard-fetcher2.1.0 input: start_date: {{ .params.start_date }} end_date: {{ .params.end_date }} api_token: {{ .secrets.BI_TOKEN }} - id: generate-report skill: report-generator3.0.0 input: figma_data: {{ $.steps.fetch-figma-comments.output }} bi_data: {{ $.steps.fetch-bi-data.output }} vuln_summary: {{ $.steps.yakit-scan.output.summary }} output: report_pdf: {{ $.steps.generate-report.output.pdf_path }} log_url: https://logs.internal/workbuddy/{{ .task_id }}这份DSL有几个关键设计{{ .env.XXX }}和{{ .secrets.YYY }}是环境变量和密钥注入语法密钥存储在本地~/.workbuddy/secrets.jsonAES-256加密密钥由首次登录密码派生{{ $.steps.xxx.output }}是跨Step数据引用WorkBuddy Runtime保证上游Step成功后其output才被注入下游inputschedule.cron是标准crontab格式但WorkBuddy做了两处关键增强支持timezone字段避免因服务器时区混乱导致误触发内置cron校验器输入0 0 9 * * 1会自动提示“您设置的是每周一9点当前服务器时区为CST是否确认”output块定义了Task的“契约出口”即哪些产物必须产生、以什么形式暴露。WorkBuddy CLI可通过wb task get id --output report_pdf直接下载PDF无需再查日志找路径。所以WorkBuddy工作台的本质是让你用图形化方式编写这份DSL同时提供实时Schema校验、依赖图谱可视化、历史版本diff对比——它降低的是“写DSL”的门槛而不是“理解契约”的门槛。2. 从零搭建我的WorkBuddy生产环境实操全记录2.1 环境准备为什么我选Ubuntu 22.04 systemd local MCPWorkBuddy官方支持macOS/Linux/Windows但我在线上稳定运行的唯一组合是OSUbuntu 22.04.4 LTS内核6.2.0-39-generic部署方式systemd service非Docker非SnapMCP模式混合模式本地Skill为主Remote MCP为辅选择理由非常务实Ubuntu 22.04是当前LTS中glibc版本最平衡的发行版。WorkBuddy Daemon用Go 1.21编译依赖musl libc的某些特性而CentOS Stream 9的glibc 2.34存在符号冲突Arch Linux滚动更新太激进macOS M1芯片上某些Cgo调用偶发panic。22.04的glibc 2.35刚好卡在兼容临界点实测18个月零崩溃。systemd是唯一能可靠管理长期Daemon进程的方案。WorkBuddy需要常驻、自动重启、日志轮转、资源限制。我试过supervisord但它无法优雅处理SIGTERM后的graceful shutdownWorkBuddy有30秒清理期必须等runtime context完全销毁Docker在宿主机重启后容器网络初始化延迟会导致MCP Server端口绑定失败而systemd的Restarton-failureRestartSec10LimitNOFILE65536组合经受住了我们团队连续237天的压测。Local MCP是可控性的底线。Remote MCP如蓝湖、Yakit固然省事但一旦它们的Server升级或维护我的周报Task就中断。我把所有核心SkillFigma抓取、BI数据拉取、PDF生成都编译成本地二进制只有非核心的“发飞书消息”用Remote MCP因为飞书Webhook极稳定且失败重试逻辑内置。这样即使公司内网断开只要本地服务正常Task仍能执行到“生成PDF”这一步只是最后一步通知失败——这比整个Pipeline卡死要好得多。安装步骤全程root权限# 1. 下载最新Release截至2024年6月v1.8.3 wget https://github.com/workbuddy-org/workbuddy/releases/download/v1.8.3/workbuddy-linux-amd64-v1.8.3.tar.gz tar -xzf workbuddy-linux-amd64-v1.8.3.tar.gz sudo mv workbuddy /usr/local/bin/ # 2. 创建专用用户和目录 sudo useradd --system --home-dir /var/lib/workbuddy --shell /usr/sbin/nologin workbuddy sudo mkdir -p /var/lib/workbuddy/{config,skills,secrets,logs} sudo chown -R workbuddy:workbuddy /var/lib/workbuddy # 3. 初始化配置 sudo -u workbuddy workbuddy init --home /var/lib/workbuddy --port 8081 # 会生成 /var/lib/workbuddy/config/workbuddy.yaml需手动编辑 # server: # port: 8081 # host: 127.0.0.1 # 强制绑定localhost禁止外网访问 # runtime: # max_concurrent_tasks: 3 # 根据CPU核数设为N-1 # default_timeout_sec: 300 # 4. 编写systemd服务文件 sudo tee /etc/systemd/system/workbuddy.service EOF [Unit] DescriptionWorkBuddy Robot Service Afternetwork.target [Service] Typesimple Userworkbuddy Groupworkbuddy WorkingDirectory/var/lib/workbuddy ExecStart/usr/local/bin/workbuddy serve --config /var/lib/workbuddy/config/workbuddy.yaml Restarton-failure RestartSec10 LimitNOFILE65536 MemoryLimit2G CPUQuota80% [Install] WantedBymulti-user.target EOF # 5. 启动并设为开机自启 sudo systemctl daemon-reload sudo systemctl enable workbuddy sudo systemctl start workbuddy sudo systemctl status workbuddy # 应显示 active (running)注意workbuddy serve命令会自动创建/var/lib/workbuddy/logs/workbuddy.log但默认不轮转。我额外加了一行logrotate配置# /etc/logrotate.d/workbuddy /var/lib/workbuddy/logs/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0644 workbuddy workbuddy sharedscripts }2.2 Skill 开发实战以bi-dashboard-fetcher为例手把手写一个可上线的Skill我之所以坚持自己写核心Skill是因为第三方Skill往往过度封装隐藏了关键参数比如BI接口的分页策略、缓存控制头出问题时我能直接git blame定位到某行代码而不是在GitHub Issues里等回复我可以加入业务特有的容错逻辑比如BI接口超时后自动降级为读取昨日缓存CSV。下面是以Go语言开发bi-dashboard-fetcher2.1.0的完整流程其他语言同理WorkBuddy只认MCP manifest第一步定义MCP Manifest# bi-dashboard-fetcher.mcp name: bi-dashboard-fetcher version: 2.1.0 description: Fetch conversion funnel data from BI dashboard API, with cache fallback protocol: mcp input_schema: type: object properties: start_date: type: string format: date description: Start date in YYYY-MM-DD format end_date: type: string format: date description: End date in YYYY-MM-DD format api_token: type: string description: BI API bearer token cache_fallback: type: boolean default: true description: If true, use local CSV cache when API fails output_schema: type: object properties: raw_json: type: string description: Raw response body from BI API csv_path: type: string description: Path to generated CSV file (local filesystem) is_cache_used: type: boolean description: True if cache was used instead of live API call第二步编写Go实现main.gopackage main import ( encoding/csv encoding/json fmt io net/http os path/filepath time ) type Input struct { StartDate string json:start_date EndDate string json:end_date ApiToken string json:api_token CacheFallback bool json:cache_fallback } type Output struct { RawJSON string json:raw_json CSVPath string json:csv_path IsCacheUsed bool json:is_cache_used } func main() { var input Input if err : json.NewDecoder(os.Stdin).Decode(input); err ! nil { fmt.Fprintf(os.Stderr, Failed to decode input: %v, err) os.Exit(1) } // Step 1: Try live API call resp, err : callBiApi(input) if err nil resp.StatusCode http.StatusOK { // Success: write JSON and generate CSV rawBody, _ : io.ReadAll(resp.Body) csvPath : generateCsv(rawBody, input.StartDate, input.EndDate) output : Output{ RawJSON: string(rawBody), CSVPath: csvPath, IsCacheUsed: false, } json.NewEncoder(os.Stdout).Encode(output) return } // Step 2: Fallback to cache if !input.CacheFallback { fmt.Fprintf(os.Stderr, API failed and cache fallback disabled) os.Exit(1) } cachePath : getCachedCsvPath(input.StartDate, input.EndDate) if _, err : os.Stat(cachePath); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, Cache file not found: %s, cachePath) os.Exit(1) } // Read cache and wrap as output cacheContent, _ : os.ReadFile(cachePath) output : Output{ RawJSON: string(cacheContent), CSVPath: cachePath, IsCacheUsed: true, } json.NewEncoder(os.Stdout).Encode(output) } func callBiApi(in Input) (*http.Response, error) { client : http.Client{ Timeout: 60 * time.Second, } req, _ : http.NewRequest(GET, fmt.Sprintf(https://bi.internal/api/v1/funnel?start%send%s, in.StartDate, in.EndDate), nil) req.Header.Set(Authorization, Bearer in.ApiToken) req.Header.Set(Accept, application/json) return client.Do(req) } func generateCsv(jsonData []byte, start, end string) string { var data map[string]interface{} json.Unmarshal(jsonData, data) // 假设BI返回结构为 { channels: [ { name: web, conversion_rate: 0.23 } ] } channels, _ : data[channels].([]interface{}) f, _ : os.Create(fmt.Sprintf(/tmp/bi-%s-to-%s.csv, start, end)) defer f.Close() writer : csv.NewWriter(f) writer.Write([]string{channel, conversion_rate, date_range}) for _, ch : range channels { channel : ch.(map[string]interface{}) name : channel[name].(string) rate : channel[conversion_rate].(float64) writer.Write([]string{name, fmt.Sprintf(%.4f, rate), fmt.Sprintf(%s~%s, start, end)}) } writer.Flush() return f.Name() } func getCachedCsvPath(start, end string) string { return filepath.Join(os.Getenv(HOME), .workbuddy, cache, fmt.Sprintf(bi-%s-to-%s.csv, start, end)) }第三步编译、签名、注册# 编译为静态二进制关键避免libc依赖 CGO_ENABLED0 go build -a -ldflags -extldflags -static -o bi-dashboard-fetcher . # 生成SHA256校验和用于MCP manifest完整性验证 sha256sum bi-dashboard-fetcher bi-dashboard-fetcher.sha256 # 注册Skill自动校验manifest和binary一致性 sudo -u workbuddy workbuddy skill install \ --manifest bi-dashboard-fetcher.mcp \ --binary bi-dashboard-fetcher \ --checksum bi-dashboard-fetcher.sha256注册成功后workbuddy skill list会显示NAME VERSION DESCRIPTION bi-dashboard-fetcher 2.1.0 Fetch conversion funnel data... figma-comment-extractor 1.2.0 Extract comments with #需求 tag...实操心得永远用CGO_ENABLED0编译。WorkBuddy Daemon运行在musl环境下而默认Go build链接glibc会导致exec format error--checksum不是可选的。WorkBuddy在每次Task执行前都会重新计算binary的SHA256并与manifest中记录的值比对不一致则拒绝执行——这是防篡改的最后防线Skill的stdin/stdout必须是JSON。WorkBuddy不解析任何其他格式哪怕你输出一行DEBUG: xxx也会导致JSON decode失败而Task中断。2.3 定时任务配置为什么我弃用XXL-JOB改用WorkBuddy原生Cron我们团队之前用XXL-JOB调度BI数据同步但很快发现两个痛点XXL-JOB是中心化调度所有任务都打到同一个Executor高峰期CPU打满导致任务排队它的“失败重试”逻辑是简单指数退避而我们的BI接口有严格的QPS限制10次/分钟盲目重试只会触发风控封禁。WorkBuddy的定时任务解决了这两个问题去中心化执行每个WorkBuddy实例独立解析自己的cron不依赖中心调度器。我给每台开发机、CI Agent、甚至测试手机Termux都装了WorkBuddy它们各自执行各自的Task负载天然分散智能重试策略WorkBuddy的retry_policy支持max_attempts、backoff_seconds、jitter_percent更重要的是它允许Skill在失败时返回特定错误码如ERR_RATE_LIMITEDWorkBuddy会自动延长下次重试间隔而不是无脑重试。我的周报Task定时配置如下schedule: cron: 0 0 9 * * 1 timezone: Asia/Shanghai retry_policy: max_attempts: 3 backoff_seconds: 300 # 首次重试等5分钟 jitter_percent: 20 # 加入±20%随机抖动避免集群雪崩 retry_on: - ERR_NETWORK - ERR_TIMEOUT - ERR_RATE_LIMITED # 这个是我Skill里自定义的错误码而Skill内部当检测到BI接口返回429 Too Many Requests时会这样返回if resp.StatusCode 429 { // 返回标准MCP错误格式 errorOutput : map[string]interface{}{ error: map[string]string{ code: ERR_RATE_LIMITED, message: BI API rate limit exceeded, please retry later, }, } json.NewEncoder(os.Stdout).Encode(errorOutput) os.Exit(1) }WorkBuddy Runtime捕获到code ERR_RATE_LIMITED就会应用retry_policy中的特殊规则而不是走默认重试。注意WorkBuddy的cron解析器基于robfig/cron/v3它支持yearly、daily等别名但不支持every 1h这种非标准语法。我曾因误写every 24h导致Task从未触发排查了3小时才发现文档里明确写着“仅支持POSIX cron语法”。2.4 MCP Server 对接如何让 WorkBuddy 调用蓝湖、Yakit、Figma 的官方能力WorkBuddy本身不内置任何第三方服务SDK它通过MCP Server桥接。对接流程高度标准化以蓝湖MCP Server为例官方已提供下载蓝湖MCP Server二进制bluehub-mcp-server-v1.5.0-linux-amd64启动它监听localhost:8082./bluehub-mcp-server --api-key your_bluehub_api_key --port 8082在WorkBuddy中注册Remote MCPwb mcp register \ --name bluehub \ --url http://localhost:8082 \ --description Bluehub requirement sync MCP adapter查看可用Skillwb mcp list --name bluehub # 输出 # bluehub.requirement-sync1.0.0 # bluehub.project-list0.9.2在Task DSL中直接引用- id: sync-to-bluehub skill: bluehub.requirement-sync1.0.0 input: requirements: {{ $.steps.fetch-figma-comments.output }} project_key: PRODWorkBuddy会自动发起HTTP OPTIONS请求获取该MCP Server的OpenAPI Spec解析Spec中的paths./v1/sync的requestBody schema映射为本地input_schema将DSL中input字段序列化为JSONPOST到http://localhost:8082/v1/sync将响应体JSON反序列化校验是否符合output_schema再注入下游Step。实操心得Remote MCP的URL必须是http://或https://不能是file://或unix://。WorkBuddy不支持本地socket通信所有Remote MCP调用都走HTTP/1.1不支持HTTP/2。Yakit MCP Adapter的文档里写着“推荐HTTP/2”但WorkBuddy v1.8.3尚未支持强行启用会导致连接复用失败MCP Server的错误响应必须是标准格式{ error: { code: ..., message: ... } }。如果蓝湖Server返回{ status: fail, msg: xxx }WorkBuddy无法识别会当作成功处理——这是我在对接初期踩的最大坑最后是提PR帮蓝湖团队修复了他们的MCP Adapter。3. 真实问题排查那些让“它真干完了”变成“它又卡住了”的瞬间3.1 问题现象Task卡在“Running”状态日志里只有一行“Starting step: fetch-figma-comments”排查路径wb task logs task_id查看实时日志发现卡在Executing skill: figma-comment-extractor1.2.0进入/var/lib/workbuddy/logs/skill-trace/找到对应时间戳的trace文件发现最后一行是INFO[0000] Calling Figma API: GET https://api.figma.com/v1/files/uXyZ/comments手动curl该URL返回401 Unauthorized检查~/.workbuddy/secrets.json发现FIGMA_TOKEN字段被意外覆盖为旧值同事共享了配置模板没改token。根因WorkBuddy的Secrets管理是“按Task实例隔离”的但secrets.json是全局文件。当多人共用一台机器如CI Agent且没有严格区分用户账户时wb secrets set命令会覆盖全局文件。解决方案强制每个Task使用独立Secrets Context在Task DSL中用secrets_context: team-a指定上下文WorkBuddy会从~/.workbuddy/secrets/team-a.json读取CI/CD场景下用wb secrets import --context ci --file ./ci-secrets.json注入而非wb secrets set在workbuddy.yaml中启用secrets.validation: strict这样如果某个Skill声明需要access_token而secrets中不存在Task提交时就直接拒绝不等到运行时。3.2 问题现象PDF生成失败报错“font not found: NotoSansCJKsc-Regular”排查路径wb task get id --step fetch-figma-comments确认上游Step成功wb task get id --step generate-report查看output发现pdf_path为空stderr里有Error: Font NotoSansCJKsc-Regular not found. Available fonts: ...登录WorkBuddy所在机器fc-list | grep -i noto发现确实没装Noto Sans CJK字体sudo apt install fonts-noto-cjk安装后重启WorkBuddy服务。根因report-generator3.0.0Skill内部用Go的unidoc/pdf库生成PDF该库默认依赖系统字体。而Ubuntu 22.04最小化安装不包含CJK字体包。解决方案在Skill的MCP manifest中增加requirements字段requirements: system_packages: - fonts-noto-cjk binaries: - fc-listWorkBuddy Daemon启动时会自动检查这些依赖缺失则报错退出并给出apt install命令更彻底的做法Skill打包时把字体文件.ttf和unidoc的字体注册逻辑一起编译进二进制彻底摆脱系统依赖——这是我下一个版本的优化点。3.3 问题现象定时任务周一没触发日志显示“Cron expression invalid: 0 0 9 * * 1”排查路径wb task list --scheduled显示该Task状态为SCHEDULED但next_run_at时间是下周检查workbuddy.yaml发现timezone字段被注释掉了timedatectl查看服务器时区是UTC而我的cron表达式0 0 9 * * 1是按CST写的即UTC8的周一9点在UTC时区下它实际意思是“UTC周一0点”也就是CST周一8点——但WorkBuddy默认用服务器时区解析cron所以它认为“现在还没到UTC周一0点”一直等待。根因WorkBuddy的cron解析器默认使用time.Now().Location