OpenWork 开源桌面应用:技能、插件与 MCP 各在哪一层起作用

2026-08-04

本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。

在 OpenWork(different-ai/openwork 这个开源桌面应用)里,技能、插件、MCP 不是三个同义词,是三个层:MCP 决定代理能不能够到某个系统,技能决定够到之后该按什么规矩用,插件只决定这堆东西怎么打包、怎么装、怎么卸。层选错了,代价不是风格问题,是你会为此多写一大批本可以不写的代码——最常见的一种,是为了让代理知道”我们公司提工单必须填哪四个字段”,去从零写一个 MCP 服务器。

OpenWork 是 Different AI 的开源桌面应用,仓库地址是 https://github.com/different-ai/openwork。名字容易被读成”开放工作”这类泛指,也容易和同名的职场点评网站混掉,下文出现的 OpenWork 一律指这个项目。仓库规模大致是这样:全仓 3490 个受版本控制的文件,apps/ 下 4 个应用,packages/ 下 12 个包,另有 ee/apps/ 10 个与 ee/packages/ 3 个;文档 packages/docs/ 57 份 mdx,其中 model-context-protocol/ 是 10 份面向不同客户端的接入指南;架构文档 docs/ 20 份 md,evals/ 26 份流程 md;服务端 apps/server/src/ 顶层就有 138 个 .ts 文件;packaging/ 提供三种分发方式。

站内已有几篇相邻的文章可以先垫底:Agent 技能机制三体对比 横着比几家平台的技能机制差异,MCP 与 Function Calling 的区别 讲协议层的取舍,Claude Code 的 Skills 怎么用 讲单个客户端里技能怎么写;本篇不重复这些,只做一件事——把 OpenWork 这一个仓库里的三层分工读出来,并给出”这个需求该落到哪一层”的判断依据。

一、三条线各自站在哪一层

文档 packages/docs/start-here/do-work-with-it/skills-plugins-and-mcp.mdx 开头给了一句很省事的定位:连接器负责够到系统,技能是写下来的做法,插件是打包。同一份文档明确说,MCP 是许多连接器底下的开放标准,但日常你按”应用、连接器、技能、插件”来想就够了。它还给了一句更关键的话:底层一切都是插件——你让 OpenWork 创建一个技能,它实际上会建一个只装着这一个技能的小插件,所以”这该算技能还是插件”这个问题,大多数人不需要回答。

把这句话翻译成工程语言:技能和插件不是同一维度的对立选项,插件是技能的容器;真正和技能对立的是 MCP。判断路径因此只有两步——先问”代理现在能不能在那个系统里执行动作”,不能就补连接;能了再问”它执行得对不对、顺序对不对、字段全不全”,不对就补技能。至于要不要打成插件,等你需要把好几样东西一起交给别人时再说。

组成部分它负责什么对应仓库位置你什么时候会碰到它
技能写下来的做法,任务匹配时被加载apps/server/src/skills.ts,工作区 .opencode/skills/同一类事要重复做,且做法有讲究
插件把技能、命令、子代理、MCP 打成一个可装可卸的单位apps/server/src/plugins.tsapps/server/src/cloud-plugins.ts几样东西要一起装、一起卸、一起给别人
从 GitHub 导入的插件包解析仓库里的清单并翻译成本地可安装形态apps/server/src/claude-plugin-bundle.ts想直接吃现成的插件仓库
MCP 服务器够到外部系统的连接方式,本地命令或远程 URLapps/server/src/mcp.tsapps/server/src/validators.ts代理需要在某个系统里真的执行动作
OpenWork Connect面向成员的服务登录与就绪状态packages/docs/start-here/connect-your-stack/connect-services.mdx要接组织已发布的邮箱、日历、协作工具
团队控制面发布能力、管访问、配连接ee/ 目录(按 ee/LICENSE要在组织范围内分发能力

二、技能:约定全都摆在磁盘上

apps/server/src/skills.ts 把技能的全部约定写死在了目录结构里,读一遍你就知道自己写的技能为什么没被认出来。

扫描范围分两块。项目级:从当前工作区目录开始,沿父目录一路往上,直到碰到 .git 为止,每一层都看 .opencode/skills.claude/skills。全局级(可选):~/.config/opencode/skills~/.claude/skills~/.agents/skills,外加一个历史遗留的 ~/.agent/skills。目录布局支持两种:扁平的 skills/<name>/SKILL.md,以及多一层分类的 skills/<domain>/<name>/SKILL.md——后者正是插件安装时用的布局,代码注释里也写明了这一点。

单个技能的解析规则更值得记:入口文件必须叫 SKILL.md;frontmatter 里的 name 如果和所在目录名不一致,这个技能会被直接丢掉(parseSkillEntry 里那句 if (name !== entryName) return null;);name 必须是 kebab-case、长度 1 到 64,description 长度 1 到 1024,这两条在 apps/server/src/validators.ts 里是硬校验,不合规同样返回空。触发条件的取法有三级兜底:先看 frontmatter 的 trigger,再看 when,都没有就去正文里找标题为 When to use 的那一节,取其下第一条有效内容。最后是去重——同名技能只保留第一个扫到的,而扫描顺序是项目目录在前、全局目录在后。

写入侧比读取侧简单得多。upsertSkill 只会写到 projectSkillsDir 指向的那一个位置,也就是 .opencode/skills/<name>/SKILL.md(见 apps/server/src/workspace-files.ts)。删除则要两步走:先按扁平路径找,找不到再回头列一遍技能、按项目作用域匹配、删掉它所在的目录——因为插件装进来的技能是嵌在命名空间文件夹里的。

这对你意味着什么:技能层没有任何运行时,它就是一份被读进上下文的 Markdown。凡是”规矩、顺序、字段、禁忌、什么情况下别做”这类知识,写在这里的成本接近于零,改起来也不需要发版;而同样一份知识如果硬要塞进代码,你得先有一个能被调用的入口,才轮到考虑内容对不对。

三、插件:分发单元,以及安装时替你做的翻译

插件在这个仓库里有两条并行的线,别混。

一条是运行时插件规格,在 apps/server/src/plugins.ts。它维护的是配置里的 plugin 列表,支持 file:http:https:git: 前缀和绝对路径;normalizePluginSpec 会把版本后缀截掉再比较,作用域式包名(以 @ 开头)单独处理。除了配置里列的,它还会扫工作区的 .opencode/plugins 目录,以及可选的 ~/.config/opencode/plugins,只认 .js.ts 文件。加载顺序它自己给了一份:config.globalconfig.projectdir.globaldir.projectaddPlugin 在发现同规格已存在时返回 false 而不是报错,这点在写自动化脚本时有用。

另一条是插件包的导入与安装,主体在 apps/server/src/claude-plugin-bundle.tsapps/server/src/cloud-plugins.ts。前者从一个 GitHub 仓库里找 .claude-plugin/plugin.json,连带 .mcp.jsonskills/commands/agents/ 一起解析成统一的中间结构;后者负责把这个结构真正写到磁盘上。这里的工程细节密度很高,挑几处对使用者影响最大的:

插件根目录的定位是”最浅的那个 .claude-plugin/plugin.json”,如果同一深度存在多个,会直接抛出 plugin_ambiguous,并提示你把插件目录写进 URL,形如 /tree/main/<dir>。分支名允许带斜杠,所以 /tree/ 后面的片段到底哪段是 ref、哪段是子目录本身是有歧义的,代码的做法是把候选逐个拿去试 trees 接口,第一个能解析的算数。

安装时会加命名空间:pluginNamespace 把插件名 slug 化,再统一补上 -plugin 后缀,于是技能落到 .opencode/skills/<namespace>/<name>/SKILL.md,子代理落到 .opencode/agents/<namespace>/<name>.md,命令落到 .opencode/commands/<namespace>/<name>.md。所有写入路径都必须以 .opencode/ 开头且不含 ..,否则报 invalid_cloud_plugin_path

frontmatter 会被翻译而不是照抄。子代理和命令的 model 只有满足”提供方/模型”这种带斜杠的形态才会被保留,否则丢弃;tools 支持逗号分隔字符串、数组、对象三种写法,统一转成小写键的布尔表。技能则更彻底:正文被剥出来,frontmatter 由安装侧按实际安装名和描述重新生成。

MCP 是唯一不落文件的成分。它走 addMcp 直接写进运行时配置,账本里记的路径是 opencode.jsonc#mcp.<name> 这种形式;卸载时按这个前缀反解出名字再调 removeMcp。这也解释了为什么插件里的 MCP 和你手动加的 MCP 在行为上没有区别——它们最终躺在同一个地方。

四、MCP:够到系统的那一层

apps/server/src/validators.ts 里的 validateMcpConfig 给了 MCP 配置最硬的边界:type 只能是 localremotelocal 必须给非空的 command 数组,每一项都得是非空字符串;remote 必须给 URL,且必须以 http 或 https 开头、能被 new URL() 解析、前后不能带空格。名字方面 validateMcpName 要求字母数字加下划线连字符,且不能以连字符开头。插件导入侧还有一层归一化:带 url 的走远程,带 commandcommandargs 的走本地,disabled 会被翻成 enabled 的反面,envenvironment 两种写法都认。

权限这一侧,apps/server/src/mcp.ts 对工具级的拒绝与放行做了多种写法的兼容匹配:tools.deny 列表、permission 配置,以及 mcp.<name>mcp:<name>:*tool.<toolId> 等一串候选模式,用 minimatch 逐个比对。也就是说”关掉某个 MCP 的某个工具”这件事有不止一种写法,排查时别只看一处。

面向组织的那条路在 connect-services.mdx 里说得很清楚,四个词各管一摊:Connect 是桌面应用里成员自己登录服务、看连接是否就绪的页面;Connections 是云端管理员发布服务、决定用谁的账号、授予成员或团队访问的后台;Connect MCP 是让外部客户端使用某个组织能力的托管端点;而”添加 MCP 服务器”是留给自定义或本地服务器的高级路径。文档里点名的可连服务包括 Gmail、Google 日历、Google Drive、Slack、Notion、Linear。

仓库 README 这样定位自己:一个用于共享 AI 工作流的免费开源桌面应用,并把自己称作 Claude Cowork 与 Codex 的开源替代,支持 macOS、Windows 和 Linux。这是项目自己的说法,不是本文的判断。README 同时给出了远程 MCP 端点的接法,并说明这个 MCP 对外只暴露两个工具:一个用于搜索可用能力,一个用于执行能力。apps/server/src/connect-skill-catalog.ts 印证了这条链路——它读的资源是 skill://index.json,索引里每个条目带一个 capability 字段,形如 skill:<名字>plugin:<插件>:<名字>,客户端侧对目录做 30 秒缓存。

这一层的判断因此很直接:需要”在系统里执行动作”才动 MCP;只是”把动作做对”,动技能就够了。选型标准可以参考 MCP 选型标准

五、边界与代价

这套设计明确放弃了一些东西,装之前该知道。

它不管的事。 插件清单里如果声明了 hooks,安装会跳过并给出一条警告,说明当前不支持。技能只安装 SKILL.md:如果一个技能目录里还带着脚本、模板、参考资料,这些附加文件会被跳过,只留一条列出目录名的警告——技能正文里如果引用了这些文件,装到本地就是断的。插件内的 MCP 如果在配置里引用了指向插件自身目录的那个变量(也就是依赖插件本地文件的命令型服务器),同样会被跳过并警告。

外部依赖。 从 GitHub 导入这条路依赖公网可达的 GitHub API 与 raw 端点,请求超时设为 20 秒,失败会抛出 502 加 plugin_fetch_failed。两个端点的基址可以通过环境变量 OPENWORK_GITHUB_API_BASEOPENWORK_GITHUB_RAW_BASE 覆盖,这是自建镜像时的唯一入口。如果解析下来一个成分都没有,会直接报 plugin_empty

审批不是默认拦。 apps/server/src/approvals.ts 里的审批服务,当配置的 mode 是 auto 时,请求会被直接放行;非 auto 模式下等待用户答复,超时则以拒绝收场。这意味着”有审批机制”和”你这台机器上现在真的会弹审批”是两件事,要看配置。

授权与数据流向要自己算账。 这类工具会在你机器上装一个桌面应用,代管模型服务商的凭据,并持有第三方服务的 OAuth 授权。文档写明有些连接由组织统一管理,管理员在云端后台决定这个连接用谁的账号、授权给哪些人。换句话说,凭据集中在一处带来的便利和暴露面是同一件事:一处被拿下,覆盖的是你接进去的全部服务。团队控制面能看到的范围,也应当在接入前问清楚而不是接完再问。这方面的通用判断可以看 MCP 的安全边界

许可证是分层的,别笼统说成”MIT 开源”。 仓库根目录的 LICENSE 写得很明确:/ee 目录下的所有内容按 ee/LICENSE 定义的许可证(根 LICENSE 把它括注为 Fair Source License,而 ee/LICENSE 文件自己的抬头是 Functional Source License, Version 1.1, MIT Future License,缩写 FSL-1.1-MIT);第三方组件各按其原始许可证;其余部分才是 MIT,Copyright 2026 Different AI。而团队控制面相关的应用与包正是放在 ee/ 下的。本文不提供法律意见,能不能商用、能不能改、改了能不能分发,一律以许可证原文为准。

六、上手与避坑清单

技能改名只改了一半。 会踩是因为目录名和 frontmatter 的 name 是两处,而不一致时解析函数直接返回空——没有报错,只是这个技能从列表里消失了。怎么避:改名当成原子操作,目录和 frontmatter 一起改,改完立刻在应用里确认它还在列表中。

名字不合 kebab-case,或描述写空了。 会踩是因为大写字母、下划线、中文名都过不了那条正则,而描述为空同样不合法,两种情况都在列举阶段被静默跳过。怎么避:技能名只用小写字母、数字和单个连字符,描述别留空也别写超长。

同名技能被吃掉。 会踩是因为项目目录、全局目录、插件命名空间目录都可能放着同名技能,而去重只保留第一个扫到的,顺序上项目优先。怎么避:个人技能加自己的前缀,别用 review、deploy 这种大路名字;发现行为不对时,先确认生效的是哪一份。

一个仓库里放了多个插件。 会踩是因为定位逻辑只认最浅的那份清单,同深度多份会直接报错拒绝安装。怎么避:按报错提示把子目录写进 URL,用 /tree/<分支>/<插件目录> 的完整形式。

技能带了附件却指望它们跟着装。 会踩是因为安装侧只取 SKILL.md,其余文件只换来一条警告,而技能正文里的相对路径引用在本地找不到落点。怎么避:把必须的内容内联进 SKILL.md;确实需要执行脚本的,把这部分改成一个 MCP 服务器提供,而不是塞在技能目录里。

指望插件里的本地命令型 MCP 直接可用。 会踩是因为引用插件自身目录的那类配置会被跳过。怎么避:改成远程 MCP,或者把命令做成系统 PATH 上可直接调用的程序,再在配置里写绝对可解析的命令。

以为重复添加同一个插件会报错。 会踩是因为添加逻辑在归一化后发现已存在时返回 false 而不是抛异常,脚本里如果只判断”没抛错就算成功”,会误以为新版本已经装上。怎么避:脚本里认真处理返回值,需要换版本时走先移除再添加。

手工挪动过插件装出来的文件。 会踩是因为卸载是按安装时记的路径逐个删的,MCP 则按记录里的名字反解后移除;路径对不上就会留下孤儿文件或孤儿 MCP 配置。怎么避:插件目录当只读区对待,要改就复制一份到自己的技能目录里改。

收个尾

真要落到一句可执行的自检,就三问:这件事代理现在能不能在目标系统里执行?不能,那是 MCP 层的缺口,写多少 Markdown 都补不上。能执行但结果不对、顺序不对、字段漏了?那是技能层的缺口,写代码属于用错工具。这些东西要不要一起交给别人?要,才轮到插件。

想继续往下读,路线是清楚的:先 apps/server/src/skills.ts,它决定你写的技能能不能被认出来;再 apps/server/src/claude-plugin-bundle.ts,它决定一个外部插件仓库能被翻译成什么;最后 apps/server/src/cloud-plugins.ts,它决定这些东西落到磁盘的哪个位置、卸载时又按什么删。三份文件读完,这套结构基本就没有黑箱了。涉及组织与控制面的部分,记得先看 ee/LICENSE 再决定怎么用。

本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用实操:把一次聊天变成可发布复用的技能给开源桌面应用 OpenWork 接外部服务:MCP 服务器、办公套件与搜索的授权边界

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。