agentsclimarketplace

Create plugin

Skill oneworks-ai/app/packages/plugins/cli-skills/skills/create-plugin

当用户想创建或改造 OneWorks plugin,实现界面入口、按钮、tab、launcher 搜索、server 命令、scoped API 或本地服务时,先理解目标效果;需求不明确时列出不确定点让用户确认,再转成 manifest、前端入口、server 入口和验证步骤。From its SKILL.md

Install
npx -y skills add oneworks-ai/app --skill create-plugin

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

32.4 KB, ~10.2k tokens by cl100k_base, as published. Nobody here has run it

在用户描述“想创建一个插件”“想在界面里加一个入口 / 按钮 / tab / 搜索 / 调试面板 / 本地服务能力”,或明确要求“用 plugin 实现某个 UI 效果”时使用这个 skill。

工作目标

先理解用户真正想要的交互效果、入口位置和 runtime 能力,再把自然语言效果转换为一个可运行、可调试、可维护的 OneWorks plugin。优先落到 plugin manifest、前端 activatePlugin(ctx)、server activatePlugin(ctx) 和 scoped runtime 通道;不要把 plugin 需求改成宿主硬编码,除非现有 plugin 扩展点明显不足。

这是 OneWorks 内置的 create-plugin skill,只面向本仓库的 OneWorks plugin。不要套用 Codex 系统 $plugin-creator.codex-plugin/plugin.json.agents/plugins/marketplace.json~/.codex/skills 或 marketplace 规则,也不要修改全局 Codex skill。

先读这些

开始写或改 plugin 前,先读取当前仓库里的插件规则和示例:

  • .oo/docs/usage/plugins.md
  • .oo/docs/usage/plugins/ui-runtime.md
  • .oo/docs/usage/plugins/server-runtime.md
  • packages/plugins/demo/package.json
  • packages/plugins/demo/plugin.json
  • packages/plugins/demo/README.md
  • packages/plugins/demo/README.zh-Hans.md

如果用户是在改已有 plugin,先读目标 plugin 自己的 plugin.json、README 多语言文件、client/server/ 入口,再决定最小修改点。

先理解效果

先把用户诉求拆成这些问题;用户已经给出时不要重复问:

  • 入口在哪里:左侧导航、左侧更多菜单、chat header、下方面板 + 菜单、右侧/下方 workbench tab、launcher 搜索、独立 plugin route。
  • 点击后发生什么:打开页面、创建新 tab、执行命令、调用本地服务、跳转外链或展示状态。
  • 展示在哪里:plugin route、可关闭 workbench tab、右侧抽屉 tab、toast、菜单项、launcher result。
  • 是否需要 runtime:只需要前端 DOM,还是要 server command、scoped API、loopback 本地服务、文件监听或 manager / workspace runtime 通信。
  • server 入口运行在哪里:普通工作区能力默认 workspace;设备级、launcher 级或跨 workspace 协调才使用 manager。一台设备可以同时有多个 workspace server,不要假设只有一个 workspace endpoint。
  • 是否需要开发态热更新:本地开发优先放在 packages/plugins/<name> 并通过 .oo.config.json 显式声明;临时开发目录才放在 .oo/plugins.dev/<name>,开启 watch。本地路径 plugin 的 client source 会通过宿主 Vite dev server 加载,不需要单独启动插件 Vite dev server。

如果缺失信息会改变入口、交互、数据来源、权限、本地服务、scope 或验证方式,先列出“不确定点”让用户确认。每个不确定点都要写清它会影响什么;可以给出推荐默认值,但不要在关键需求模糊时直接替用户拍板。只有名称、图标、文案这类低风险细节缺失时,才采用保守默认并在结果里说明。

扩展点选择

按效果选择 manifest 字段:

  • 左侧固定入口:plugin.contributions.navItems。默认路径是 /plugins/<scope>/<id>;也可以给 routecommand
  • 左侧更多菜单:navMoreMenu,支持 commandroutehref
  • 左侧底部「菜单」上方区域:navFooterBefore,支持 commandroutehref
  • chat header 右上角按钮:chatHeaderActions,用 command 触发 plugin 命令。
  • chat header 更多菜单:chatHeaderMoreMenu
  • chat 新建面板默认页操作卡片:chatInteractionPanelEmptyActions,支持 commandroutehref,额外支持展示用 shortcut
  • 通用路由 header 按钮:routeHeaderActions,用 targetRoute / targetRoutes 限定宿主 route container,用 command 触发 plugin 命令。
  • 通用路由折叠态 window bar 按钮:routeWindowBarActions,通常和 routeHeaderActions 成对注册,也用 targetRoute / targetRoutes 限定宿主 route container。
  • 通用路由更多菜单:routeMoreMenuItems,支持 commandroutehref;其中 route 是点击后的跳转目标,不是宿主显示条件。
  • 通用路由侧栏入口 / 列表右键菜单:routeSidebarContextMenu,支持 commandroutehref;其中 route 是点击后的跳转目标,不是宿主显示条件。
  • 独立页面:routes + clientView,前端用 ctx.views.register(viewId, render) 提供内容。
  • 下方面板或右侧抽屉 tab:workbenchTabsworkspaceDrawerTabs,用 placement: "bottom" | "right" 决定位置。
  • 下方面板 + 菜单:workbenchAddMenu。如果要点击后创建 tab,设置 tab 指向 workbenchTabs[].id
  • launcher 搜索:launcherSearchProviders,桌面 launcher 使用 server-backed command;workspace client 可以再用 ctx.launcher.registerSearchProvider 注册本地搜索。
  • chat 工具调用展示:toolUsePresentations,用纯声明配置工具匹配、Material Symbol、国际化标题、摘要目标、输入字段和结果格式;不要为单个插件在聊天组件里新增 renderer 或注入 JSX/HTML。
  • 插件间扩展点:拥有方用 extensionPointsctx.extensionPoints.register 暴露 <scope>/<id>;其他插件用 ctx.extensionPoints.onAvailable('<scope>/<id>', point => ctx.extensionPoints.contribute(...)) 监听目标点出现后补充结构化能力。只有 manifest 里声明的静态贡献才用 extensionContributions
  • 插件间纯前端 API:拥有方用 ctx.pluginApis.register({ id, inputSchema, outputSchema, handler }) 暴露 in-client 过程调用;调用方用 await ctx.pluginApis.call('<scope>/<id>', input)。所有调用必须是 Promise,运行时会等待目标 API 注册并等待 handler 完成。
  • 本地能力:server ctx.registerCommandctx.registerApi;前端通过 ctx.commands.executectx.api.fetch 调用。
  • manager / workspace 通信:server ctx.runtime.registerChannelctx.runtime.invokeChannel;前端通过 ctx.runtime.invokeChannel 调用当前 scope 的 server runtime channel。

关键语义:

  • workbenchTabs 是“可创建 tab 模板”,不是启动后常驻 tab。
  • route 级贡献里的 targetRoute / targetRoutes 是宿主显示条件,菜单项里的 route 是点击后的导航目标,二者不要混用。
  • targetRoute / targetRoutes 支持 route key、当前 pathname 和 /* 后缀路径通配。路径可以写应用内路径(如 /plugins/*)或带 client basename 的路径(如 /ui/plugins/*),两者都会归一匹配。配置页的 settings / config 是互通 alias:targetRoute: "settings" 可以匹配当前 config route,targetRoute: "config" 也可以匹配旧的 settings route。
  • route 级 chrome 包括 header action、侧栏折叠态 window bar action、route more menu、route sidebar 入口 / 列表右键菜单。这些贡献会扩展宿主 route 已有 action,不会覆盖 route 自己声明的能力。菜单类贡献只注册结构化 item,由宿主通过通用 nav-rail-more-menu / overlay 结构渲染;不要让 plugin 或业务 route 自己拼菜单 DOM、局部 CSS 或 hover / selected / shortcut 样式。
  • route 级 chrome 的实际可见位置由宿主 route 已接入的 container 能力决定:header action、window bar action 和 route more menu 由 useRoutePluginChrome(routeKey) 统一安装;route sidebar 右键菜单只有在宿主 route 提供共享 sidebar 数据时才会显示。
  • route action 状态字段由宿主通用 chrome 统一渲染:active 切换 activeIcon / activeLabel / activeTitledisabled 禁用,danger hover/focus 使用危险色,shortcut 进入 tooltip;菜单型贡献额外支持 selected。旧 routeMoreMenu 只是兼容别名,新插件写 routeMoreMenuItems
  • chatInteractionPanelEmptyActions 是 chat 专属扩展点,只扩展新建面板默认页的 action card;它不进入通用 route container,也不替代 workbenchAddMenu。如果需要新增真实底部 / 右侧 tab,继续用 workbenchTabs + workbenchAddMenu
  • toolUsePresentations 默认只按 base name 匹配当前插件 scope 下的 OneWorks MCP 工具;只有明确要展示其他来源工具时才设置 origin: "any"。对象数组使用宿主 records 字段并声明 item 路径,短原子数组使用 chips;结果较大时优先用 result.mode: "declared" + result.fields 做渐进披露,不要默认铺完整协议 JSON。输入和结果只能选择宿主支持的结构化格式,不允许可执行模板、任意 HTML 或插件私有 React renderer。
  • 插件扩展点的边界是:通用布局交互沉淀到 route container / layout 组件;route 层负责业务状态、持久化和 slot 内容;plugin 只注册贡献、命令、view 和 slot 内容。现有扩展点不够时,优先补宿主结构化 API,不要把 header、window bar、右键菜单或 overlay 交互复制进 plugin。
  • 点击 workbenchAddMenu 创建的新 tab 应该可关闭;不要把普通 plugin tab 做成默认固定。
  • 需要默认固定或默认打开时,必须有显式配置语义,不要把所有 plugin tab 都自动打开。
  • plugin scope 是隔离边界。manifest 不声明 scope;scope 来自用户配置或运行时派生。
  • plugin 不注册顶层 /api/*;所有 API 必须在 /api/plugins/<scope>/* 下。
  • 有 server 入口的插件必须声明 plugin.server.rolespackage.jsonexports["./server"] 只补入口路径;未声明 roles 时宿主拒绝注册 server 入口并在 diagnostics 暴露错误。需要 manager 时必须显式写 roles: ["manager"]["manager", "workspace"]
  • 跨 runtime channel 调用必须明确目标。跨 manager / workspace 或跨多个 workspace 时,在 target 里传 roleserverBaseUrl;不要把当前 workspace server 当成唯一 server。
  • 插件间扩展点不是宿主固定 slot。只有当目标扩展点存在时才贡献;贡献项要保持结构化,至少写 idtitleI18n / descriptionI18niconcommand,由拥有方 view 决定如何渲染。

配置 Schema

如果 plugin 需要用户可编辑配置,必须优先在 manifest 里声明 config.schema,让插件详情页「配置」tab 自动渲染表单:

  • 根 schema 使用 type: "object"properties
  • 每个用户可见字段都写 titleI18ndescriptionI18n,至少覆盖 enzh-Hans;保留 title / description 作为兜底也可以,但不要只写英文标题。
  • 支持的常用类型:stringnumberintegerboolean、字符串数组、enumoneOf / anyOf 里的字符串 const 选项。
  • default 表达默认值;不要为了默认值提前写入 .oo.config.json
  • x-oneworks-ui.iconplaceholdercontrolsensitive 描述图标、占位符、textarea / 普通输入、敏感字段。
  • format: "password"writeOnly: truex-oneworks-ui.sensitive: true 用于 token、密钥等敏感配置。
  • 如果 JSON Schema 自动推断不够,再使用 config.uiSchema 直接声明配置页内部 ConfigUiObjectSchema;不要为普通字段手写自定义页面。

配置值读取规则:

  • 前端和 server 入口读取当前实例的 options,不要自己解析 .oo.config.json
  • 运行时没有配置值时,用 schema default 或代码里的保守默认兜底。
  • 保存配置只应更新当前 plugin 实例的 plugins[].options,不要污染其他 scope 或全局配置。
  • 独立 plugin view 里需要内联编辑配置时,使用 view.options.value 读取当前实例配置,并调用 await view.options.update(nextOptions) 写回当前实例;不要绕过宿主去直接请求顶层 /api/plugins/*/options

Manifest 与 I18n

每个用户可见贡献项都要写清楚名称和说明:

  • id 是稳定机器标识,短横线命名,不展示给用户当说明。
  • plugin 顶层 manifest 必须同时声明 displayNamedisplayNameI18n.endisplayNameI18n.zh-HansicondisplayName 是兼容旧宿主的英文兜底;宿主优先显示当前语言的 displayNameI18n
  • icon 必须指向 plugin 根目录内的相对资产(推荐 ./assets/icon.svg),禁止绝对路径和 .. 穿越。优先设计简洁、深浅主题都清晰的 SVG,不要用宿主固定的 extension 图标冒充插件自己的品牌图标。
  • title 是兜底文案;同时写 titleI18n.entitleI18n.zh-Hans
  • 宿主会统一把 contribution 的 titleI18n / descriptionI18n 解析成当前应用语言;不要在左侧导航、菜单、workbench 等宿主 slot 里自己判断浏览器语言。
  • descriptionI18n.endescriptionI18n.zh-Hans 描述这个入口点击后做什么、展示在哪里、是否调用命令或 server。
  • icon 使用 Material Symbols 名称,选择能表达入口语义的图标。
  • 不要依赖 UI 兜底生成“标识 xxx”“命令 xxx”这类说明;缺少描述时应补 manifest。

README 多语言规则:

  • 必须同时提供英文和中文 README;这是硬要求,不因用户只使用单一语言而省略另一种。
  • README.md 使用用户当前对话语言作为默认语言:用户用中文沟通时写中文,用户用英文沟通时写英文。
  • 另一种语言使用带 locale 后缀的 README 文件:默认是中文时补 README.en.md,默认是英文时补 README.zh-Hans.md
  • 两种语言的 README 都要说明 plugin 做什么、贡献了哪些入口、有哪些配置项、如何验证。
  • README 中相对链接和图片路径相对 plugin 根目录;不要写只有当前机器可用的绝对路径。
  • 如果 manifest、README 或示例配置变更了用户可见行为,要同步更新对应文档。

落地路径

优先复用已有 plugin;没有时创建可提交 package plugin:

packages/plugins/<plugin-name>/
  package.json
  plugin.json
  assets/icon.svg
  README.md
  README.zh-Hans.md
  client/src/index.tsx
  client/src/view.tsx
  client/src/i18n.ts
  client/src/styles.ts
  client/dist/index.js
  client/vite.config.ts
  server/src/index.ts
  server/dist/index.js
  server/tsconfig.json

package.json 最小骨架:

{
  "type": "module",
  "scripts": {
    "build": "pnpm run build:client && pnpm run build:server",
    "build:client": "tsc -p client/tsconfig.json --noEmit && vite build --config client/vite.config.ts",
    "build:server": "tsc -p server/tsconfig.json"
  },
  "exports": {
    "./client": {
      "source": "./client/src/index.tsx",
      "default": "./client/dist/index.js"
    },
    "./server": {
      "source": "./server/src/index.ts",
      "default": "./server/dist/index.js"
    },
    "./package.json": "./package.json"
  },
  "devDependencies": {
    "vite": "^5.4.8"
  }
}

plugin.json 最小骨架:

{
  "__oneWorksPluginManifest": true,
  "name": "@local/plugin-example",
  "displayName": "Plugin Example",
  "displayNameI18n": {
    "en": "Plugin Example",
    "zh-Hans": "插件示例"
  },
  "icon": "./assets/icon.svg",
  "version": "0.1.0",
  "config": {
    "schema": {
      "type": "object",
      "properties": {
        "greeting": {
          "type": "string",
          "default": "Hello from Plugin Example",
          "titleI18n": {
            "en": "Greeting",
            "zh-Hans": "问候语"
          },
          "descriptionI18n": {
            "en": "Text shown by plugin commands and views.",
            "zh-Hans": "插件命令和视图展示的文本。"
          },
          "x-oneworks-ui": {
            "icon": "waving_hand",
            "placeholder": "Hello from Plugin Example"
          }
        }
      }
    }
  },
  "plugin": {
    "server": {
      "roles": ["workspace"]
    },
    "contributions": {
      "routes": [
        {
          "id": "home",
          "title": "Plugin Example",
          "titleI18n": {
            "en": "Plugin Example",
            "zh-Hans": "插件示例"
          },
          "descriptionI18n": {
            "en": "Opens the standalone plugin route.",
            "zh-Hans": "打开独立插件页面。"
          },
          "clientView": "home"
        }
      ],
      "workbenchTabs": [
        {
          "id": "panel",
          "title": "Plugin Example",
          "titleI18n": {
            "en": "Plugin Example",
            "zh-Hans": "插件示例"
          },
          "descriptionI18n": {
            "en": "Renders a bottom workbench tab.",
            "zh-Hans": "渲染底部工作区标签页。"
          },
          "placement": "bottom",
          "clientView": "panel"
        }
      ],
      "workbenchAddMenu": [
        {
          "id": "open-panel",
          "title": "Plugin Example",
          "titleI18n": {
            "en": "Open Plugin Example panel",
            "zh-Hans": "打开插件示例面板"
          },
          "descriptionI18n": {
            "en": "Creates a new bottom workbench tab from the panel template.",
            "zh-Hans": "基于面板模板创建新的底部工作区标签页。"
          },
          "tab": "panel"
        }
      ]
    }
  }
}

路径规则:

  • 在 OneWorks 仓库或具备前端构建环境的项目里,可提交 plugin 优先放在 packages/plugins/<plugin-name>/,通过 package name / exports 和 .oo.config.jsonplugins 数组显式声明启用。
  • 本地开发和临时调试目录放在 .oo/plugins.dev/<plugin-name>/,它会自动发现并默认开启 watch。
  • 如果用户项目没有 packages/plugins 语义、不是前端工程,或只是想在普通项目资产目录快速放一个轻量插件,可以使用 .oo/plugins/<plugin-name>/,但需要在 .oo.config.json 里用显式目录路径声明;宿主不会再把 .oo/plugins/* 普通子目录自动当成 UI plugin 加载。
  • OneWorks plugin manifest 文件名是 plugin.json / plugin.yaml / plugin.yml,不放在 .codex-plugin/ 下面。
  • README 必须覆盖英文和中文;README.md 使用当前对话语言,另一种语言使用 README.en.mdREADME.zh-Hans.md。两个 README 描述同一组入口、配置项、命令、API 和调试方式。
  • 不要把用户配置值写进 manifest;具体值保存到项目配置里的 plugins[].options

前端实现规则

client/src/index.tsx 导出 activatePlugin(ctx),并由 Vite 编译到 client/dist/index.js

  • 新插件按 package.json exports 约定暴露入口:exports["./client"].source 指向 client/src/index.tsxexports["./client"].default 指向 client/dist/index.jsexports["./server"].source 指向 server/src/index.tsexports["./server"].default 指向 server/dist/index.js。server 入口的运行层级写在 manifest 的 plugin.server.roles;不要在 manifest 里重复写 plugin.client.entryplugin.client.rootplugin.server.entry
  • 本地路径 plugin 在 watch 开启或位于 .oo/plugins.dev/* 时,开发态宿主会把 exports["./client"].source 转成同源 /@fs/... entry,由宿主 Vite dev server 负责 TS / TSX 转译、source map、样式模块 HMR 和 React Fast Refresh;新插件不要配置 plugin.client.devServer
  • client/src/index.tsx 只做薄入口:加载子模块、注入样式、注册 view / command / launcher provider、集中 dispose。页面组件、i18n 文案、样式和数据模型拆到 client/src/view.tsxclient/src/i18n.tsclient/src/styles.ts 等 ESM 模块;不要把整套插件都写进一个入口文件。
  • 静态入口只加载浏览器可执行的 JavaScript,不会由宿主自动转译 TypeScript / TSX。发布 / 提交前运行 vite build --config client/vite.config.ts 生成 client/dist/index.js;不要把含 TS / TSX 语法的源码文件当作静态入口。
  • server 侧本地开发可以直接写 TS:watch 开启时宿主会加载 exports["./server"].source,并用 esbuild register 转译 .ts / .tsx / .mts / .cts。发布 / 提交时使用 exports["./server"].default 的 JS 产物。
  • .oo/plugins.dev/* watch、显式 watch: true 和插件详情页 watch 对 manifest、server、README、静态入口,以及 host Vite client source 下普通 TS / 数据 / i18n / model 模块变更提供 plugin scope 级 reload。TSX / JSX 组件和样式模块交给 Vite HMR / React Fast Refresh,入口文件变化仍走 plugin scope 级 reload。
  • ctx.views.register(viewId, { renderNode }) 注册 React view;manifest 的 clientView 必须能找到同名 view。无构建插件从 ctx.reactcreateElement / hooks;TSX 插件也必须复用宿主 React 单例,不要 bundle 第二份 React。ctx.views.register(viewId, render) 是兼容 DOM view,只用于简单无 React 插件。
  • view context 读取宿主状态:view.host.language 是当前界面语言,view.host.themeMode 是用户选择的 light | dark | systemview.host.resolvedThemeMode 是实际 light | darkview.host.isDarkMode 是深色状态,view.host.surface 表示当前挂载在 route | workbench | drawer
  • 插件自己的按钮、placeholder、菜单项、状态文案必须走 i18n:React view 内使用 view.i18n.resolveText({ en, "zh-Hans": "..." });前端命令、界面消息、launcher 本地搜索这类不在 view render 内执行的逻辑使用 ctx.i18n.resolveText(...)ctx.i18n.getLanguage()。不要用 view.host.language.startsWith('zh') 手写分支,也不要只写英文硬编码。
  • 需要复用宿主 UI 时,React view 优先用 view.ui.* 声明式组件,不要复制宿主组件 DOM。当前组件包括 IconButtonInputSegmentedSwitchSenderProjectFileTree,以及 overlay 系列的 OverlayDropdownOverlayMenuOverlaySearchMenuOverlaySearchRowOverlaySegmentedOverlaySelectLabelOverlayTree;菜单、搜索、树、确认态、danger 态、快捷键和嵌套 submenu 都应该走这些结构化 overlay props。需要真实弹出浮层时优先用 OverlayDropdown,不要把 overlay primitive 直接平铺在插件页面里。overlay 树需要独立浮层外观时传 surface: true,不要在插件内手写 panel 样式。Sender 默认套用 chat sender surface,并包含模型、权限、adapter / account 和 status bar 编排。sender 样式只能通过 surface: "chat" | "plain"density: "default" | "compact"showHeadershowStatusBarplaceholderinitialContentdefaultAdapterdefaultModel 这类结构化选项控制。DOM view 兼容路径才使用 view.components.render(component, container, props),返回的 { dispose() } 必须在 view 的 dispose() 里清理;需要示例时参考 .oo/docs/usage/plugins/ui-runtime.md 的“宿主组件用法示例”。
  • ctx.commands.register(commandId, handler) 注册前端命令。
  • ctx.commands.execute(commandId, payload) 调用前端或 server command。
  • ctx.extensionPoints.register({ id, title, description, contributionSchema }) 暴露插件自己的扩展点;贡献方用 ctx.extensionPoints.onAvailable('other-scope/point', point => ctx.extensionPoints.contribute('other-scope/point', contribution)),不要用激活时的一次性 has(...) 判断来决定是否贡献。React view 里用 view.extensions.getContributions('point') 读取当前插件扩展点的贡献。
  • ctx.pluginApis.register({ id, title, description, inputSchema, outputSchema, handler }) 暴露插件内过程调用;其他插件用 await ctx.pluginApis.call('other-scope/api-id', input) 调用。call 的 Promise 会等待目标 API ready,也会等待 handler 处理完成;必要时传 timeoutMs 明确失败边界。
  • ctx.api.fetch('apiId/path') 调 scoped API;不要传绝对 URL、协议相对 URL 或顶层 /api/*
  • ctx.runtime.invokeChannel(channelId, { payload, target }) 调用当前 scope 的 server runtime channel;跨 workspace / manager 调用时显式给 target.roletarget.serverBaseUrl
  • ctx.notifications.show({ title, description, actions }) 发布宿主级消息。宿主会自动显示插件来源、发送时间、markdown 描述、按钮回调和关闭 / 屏蔽插件这类通用操作。不要在插件里自己创建 fixed toast 或引入另一套通知 UI。
  • ctx.i18n.t({ en, "zh-Hans": ... }) / ctx.i18n.select(...) 渲染插件自带文案,跟随宿主应用语言;DOM view 需要在 ctx.i18n.subscribe() 里重绘,并在 dispose() 中清理订阅。
  • ctx.hot.accept()ctx.hot.reload() 支持本 plugin 的 scope 级调试重载;它不是 Vite Fast Refresh。React Fast Refresh 由宿主 Vite /@fs client source entry 提供,插件仍要复用宿主 React 单例、宿主组件继续走 view.ui.*
  • 所有 DOM 事件、style、timer、subscription 都必须在返回的 dispose() 里清理。
  • 前端 plugin 不直接访问文件系统;需要本地能力时交给 server entry。

插件界面设计标准

默认把 plugin UI 做成宿主里的功能界面,不做营销页或展示页:

  • plugin route、workbench tab、drawer tab 的根节点要直接填满宿主给的容器:box-sizing: border-box; width: 100%; min-width: 0; min-height: 0;,需要纵向布局时再加 display: flex; flex-direction: column;
  • 默认不要在 plugin 根节点加大块 padding、max-width、居中容器、渐变背景、装饰色块、hero、阴影卡片或“卡片套卡片”。宿主 route body / panel chrome 已经提供外层边界,plugin 内容应直接平铺在里面。
  • 首个内容块或 header 默认不要再加额外 top padding;内容应该从宿主提供的可用区域顶部开始。
  • 独立 plugin route 已经由宿主 route header 展示标题和图标时,plugin view 内不要再重复 h1、eyebrow 或解释性 header;直接呈现当前页面的工具栏、表单、列表、输出区域等核心内容。
  • 独立 plugin route 里的内容默认透明背景;只有表格、输出日志、表单组、可重复列表项、modal 等真正需要边界的局部元素才加 1px border 和小圆角。不要给整页加实色背景或大面积主题色。
  • 按钮用克制的工具按钮风格:小尺寸、图标加短文案、普通边框或透明背景;图标优先使用宿主已加载的 Material Symbols,不要把 action 做成大卡片或醒目 hero CTA。route header、window bar、更多菜单、侧栏右键这类 chrome action 必须通过 manifest 贡献给宿主结构化渲染,不在 plugin view 里手写一套 header。
  • 文案只保留任务所需信息。示例 / demo 页面可以说明来源和用途,但不要罗列所有扩展点或写大段介绍;把详细说明放 README。
  • 输出 JSON、日志、状态面板可以用 pre / 列表 / 表格展示,但应跟随父容器宽度,避免固定宽度导致右侧空白。
  • 使用宿主 token:var(--ant-color-text)var(--ant-color-text-secondary)var(--ant-color-bg-container)var(--ant-color-border)var(--ant-color-border-secondary)var(--ant-color-fill-quaternary)var(--primary-color)var(--primary-soft-bg)var(--primary-text-color)var(--bg-color)var(--sub-bg-color)var(--text-color)var(--border-color) 等;不要硬编码一套高饱和主题色。
  • chrome 尺寸用 var(--app-chrome-icon-size)var(--app-chrome-action-gap)var(--route-container-header-overlay-height)var(--route-container-header-padding-inline)var(--route-container-header-padding-block);不要在 plugin 里重新定义 header、tab、图标按钮高度。
  • 如果某个 view 看起来没有填满左右宽度,先检查宿主挂载点和 plugin 根节点是否都是 flex: 1 1 auto; width: 100%; min-width: 0;,不要用额外 wrapper 或固定 max-width 掩盖问题。

Server 实现规则

server/src/index.ts 或发布态 server/dist/index.js 导出 activatePlugin(ctx)

  • ctx.registerCommand(commandId, handler) 暴露 scoped command。
  • ctx.registerApi(apiId, { handler }) 暴露 scoped HTTP API,真实路径固定为 /api/plugins/<scope>/proxy/<apiId>/*。它就是插件的 route 注册工具;不要注册顶层 /api/*
  • ctx.registerApi(apiId, { proxy: { target } }) 只能代理到 loopback HTTP(S) 本地服务。
  • 每个 registerApi 都必须写清 titledescriptioninputSchemaoutputSchemaheaderSchema。旧插件缺失时运行时兼容但会产生 plugin_api_metadata_missing 诊断;新插件不要省略。
  • ctx.registerLocalService(serviceId, start) 管理随 plugin 生命周期启动/停止的本地服务。
  • ctx.runtime.role 标识当前入口运行在 workspace 还是 manager;不要用环境变量或 URL 自己判断。
  • ctx.runtime.registerChannel(channelId, handler) 注册当前 scope 的 runtime 通信通道。
  • ctx.runtime.invokeChannel(channelId, invocation) 调用当前 scope 的 runtime 通道;跨 runtime 时必须传明确目标。
  • ctx.dispose(callback) 清理 timer、server、watcher、临时资源。
  • 使用 ctx.workspaceFolderctx.projectHomectx.pluginRoot,不要猜路径。
  • registerApihandlerrequest.methodrequest.path 分发子路由;如果需要完整 Express / Hono / Fastify router,使用 registerLocalService 启动本地 loopback 服务,再用 registerApi(..., { proxy }) 暴露为 scoped API。

Server route 示例:

const json = (body: unknown, status = 200) => ({
  status,
  headers: {
    'content-type': 'application/json; charset=utf-8'
  },
  body
})

export function activatePlugin(ctx) {
  ctx.registerCommand('snapshot', payload => ({
    ok: true,
    scope: ctx.scope,
    received: payload
  }))

  ctx.registerApi('notes', {
    title: {
      en: 'Notes API',
      'zh-Hans': '笔记 API'
    },
    description: {
      en: 'Creates and lists notes inside the plugin scoped API.',
      'zh-Hans': '在插件作用域 API 内创建和列出笔记。'
    },
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string' }
      },
      additionalProperties: false
    },
    outputSchema: {
      type: 'object',
      additionalProperties: true
    },
    headerSchema: {
      type: 'object',
      properties: {
        'content-type': { const: 'application/json' }
      },
      additionalProperties: true
    },
    handler: async request => {
      if (request.method === 'GET' && request.path === '') {
        return json({ notes: [] })
      }

      if (request.method === 'POST' && request.path === 'create') {
        const payload = JSON.parse(request.body.toString('utf8') || '{}')
        return json({ created: true, payload }, 201)
      }

      return json({ error: 'Not found' }, 404)
    }
  })
}

前端调用时使用 ctx.api.fetch("notes/create", { method: "POST", body: JSON.stringify(payload) }),不要拼绝对 URL。

调试与验证

实现后按影响面验证:

  • GET /api/plugins 能看到 plugin、scope、贡献项和诊断。
  • GET /api/plugins 中能看到 displayNamedisplayNameI18nicon;切换中文/英文后插件列表与详情标题使用对应名称,图标资产请求返回成功。
  • /plugins 插件商店能看到 plugin;/plugins/<scope> 详情能看到 README、entry、contributions、runtime 注册项和 watch 开关。
  • 点击每个入口验证真实效果:左侧入口、菜单项、chat header、+ 菜单、创建/关闭 workbench tab、launcher 搜索。
  • 验证 workbenchAddMenu.tab 创建的是新 tab 实例,不是常驻 tab。
  • 调用 server command 和 scoped API,确认 scope 不串。
  • 改本地路径 plugin 的 client/src/view.tsx 或样式文件后,宿主 Vite dev server 应能触发模块 HMR / React Fast Refresh;改 client/src/index.tsx、普通 TS / 数据 / i18n / model 模块、manifest、server 或 README 后,plugin watch 触发 scope 级 reload,不需要重启 Electron。
  • 如果改了宿主代码,运行相关 vitestpnpm typecheckpnpm exec dprint check

文档同步

如果新增了用户可见 plugin 能力,同步更新:

  • .oo/docs/usage/plugins.md:manifest、扩展点、watch、plugin store、server API。
  • .oo/docs/usage/web.md:Web UI 上的具体入口和交互变化。
  • plugin 自己的英文和中文 README:告诉使用者它注册了哪些入口、有哪些配置项、需要哪些本地服务、如何调试。

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.