Skip to content

界面 Plugin Runtime 与前端入口

界面 plugin runtime

界面 plugin 仍然使用同一个 plugins 配置和同一个 manifest,不引入新的公开名称。一个 plugin 可以同时提供数据资产、界面贡献、server 命令和 scoped API。

本地目录 plugin 会从这些位置自动发现:

  • 全局 plugin:~/.oneworks/global/plugins/*
  • 项目开发 plugin:.oo/plugins.dev/*

.oo/plugins.dev/* 默认开启 watch,适合调试本地界面 plugin;这个目录是本地开发目录,不应该提交到仓库。普通可提交插件优先作为 package 放在 packages/plugins/<name> 并通过 .oo.config.jsonplugins 显式声明。如果用户项目没有合适的 package/plugin 目录,也可以把轻量本地插件放在 .oo/plugins/<name>,但它只是建议放置位置,不是 runtime 自动发现根,必须在 plugins 里显式声明。

普通目录 plugin 至少包含一个 plugin.json / plugin.yaml / plugin.yml,也可以用 package.json 里的 plugin manifest。常见结构:

text
my-plugin/
  package.json
  plugin.json
  README.md
  client/
    src/index.tsx
    src/view.tsx
    src/i18n.ts
    src/styles.ts
    dist/index.js
    vite.config.ts
  server/
    src/index.ts
    dist/index.js
    tsconfig.json
  rules/
  skills/

默认约定是:本地源码写在 client/src/index.tsx,发布入口是 Vite build 后的 client/dist/index.jsclient/src/index.tsx 仍应保持为薄入口:加载样式和子模块、注册 view / command / launcher provider,并在 dispose() 里清理资源。真实页面组件、i18n 文案、数据模型和样式应拆到 client/src/ 下的普通 ESM 模块,避免把整个插件塞进一个入口文件。

插件入口路径优先走约定的 package.json exports,manifest 不需要重复写一遍。推荐写法:

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"
  }
}

exports["./client"].source 是宿主 Vite dev server 加载的源码入口;exports["./client"].default 是发布 / 提交时的静态 ESM 产物。exports["./server"].source 是 watch / 本地开发时的 server TS 入口;exports["./server"].default 是发布 / 提交时的 server JS 入口。显式 plugin.client.entryplugin.client.devEntryplugin.server.entry 仍可兜底兼容旧插件,但新插件按上面的 exports 约定生成。

静态 /client 入口不会在宿主里再做 TypeScript 或 JSX 转译。也就是说,未编译的 client/src/index.tsx 不能作为静态入口直接加载。新的 TypeScript / TSX 插件按这条链路处理:

  • 本地路径 plugin 在 watch 开启或位于 .oo/plugins.dev/* 时,开发态宿主会把 exports["./client"].source 转成同源 /ui/@fs/... entry,由宿主 Vite dev server 负责 TypeScript / TSX 转译、source map、样式模块 HMR 和 React Fast Refresh;不需要插件自己再启动一个 Vite dev server。
  • 可提交 / 发布态运行 vite build --config client/vite.config.ts,把 TypeScript / TSX 编译到 client/dist/index.js,宿主静态加载这个 JavaScript 产物。
  • plugin.client.devServer 只作为旧插件或外部 loopback dev server 兼容路径保留;新插件不要为了 HMR 配它。

Server 侧也支持本地 TS:watch 开启的本地插件会优先加载 exports["./server"].source,并通过宿主已有的 esbuild register 转译 .ts / .tsx / .mts / .cts;发布 / 提交态使用 exports["./server"].default 的 JS 产物。server 入口没有独立 dev server,文件变化仍走 plugin scope reload。

.oo/plugins.dev/* watch、显式 watch: true 和插件详情页 watch 对 manifest、server、README、静态入口,以及 host Vite client source 下普通 TS / 数据 / i18n / model 模块变更提供 plugin scope 级 reload:文件变化后宿主 dispose 当前插件并重新 import entry。TSX / JSX 组件和样式模块由 Vite HMR / React Fast Refresh 自处理,入口文件变化仍走 plugin scope 级 reload。拆分无构建模块时,入口如果动态加载子模块,推荐把宿主传入 entry 的 pluginVersion 查询参数继续带到子模块 import 上,否则浏览器的 ESM module registry 可能在同一页面生命周期内复用旧子模块:

js
const pluginVersion =
  new URL(import.meta.url).searchParams.get('pluginVersion') ?? Date.now()
const load = path =>
  import(`${path}?pluginVersion=${encodeURIComponent(pluginVersion)}`)

export async function activatePlugin(ctx) {
  const [{ createHomeView }, { css }] = await Promise.all([
    load('./view.js'),
    load('./styles.js')
  ])
  // register views and commands...
}

Manifest 示例:

json
{
  "__oneworksPluginManifest": true,
  "name": "@acme/plugin-workspace-tools",
  "displayName": "Workspace Tools",
  "config": {
    "schema": {
      "type": "object",
      "properties": {
        "greeting": {
          "type": "string",
          "default": "Hello from Workspace Tools",
          "titleI18n": {
            "en": "Greeting",
            "zh-Hans": "问候语"
          },
          "descriptionI18n": {
            "en": "Text shown by plugin commands and views.",
            "zh-Hans": "插件命令和视图展示的文本。"
          },
          "x-oneworks-ui": {
            "icon": "waving_hand",
            "placeholder": "Hello"
          }
        },
        "showLauncherPreview": {
          "type": "boolean",
          "default": true,
          "titleI18n": {
            "en": "Launcher preview",
            "zh-Hans": "启动器预览"
          },
          "x-oneworks-ui": {
            "icon": "manage_search"
          }
        }
      }
    }
  },
  "plugin": {
    "contributions": {
      "navItems": [
        {
          "id": "dashboard",
          "title": "Workspace Tools",
          "icon": "dashboard"
        }
      ],
      "chatHeaderActions": [
        {
          "id": "snapshot",
          "title": "Snapshot",
          "icon": "camera_alt",
          "command": "snapshot"
        }
      ],
      "workbenchTabs": [
        {
          "id": "debug-web",
          "title": "Debug Web",
          "icon": "language",
          "placement": "bottom",
          "clientView": "debug-web"
        }
      ],
      "workbenchAddMenu": [
        {
          "id": "debug-web",
          "title": "Debug Web",
          "icon": "language",
          "tab": "debug-web"
        }
      ],
      "workspaceDrawerTabs": [
        {
          "id": "context",
          "title": "Context",
          "icon": "account_tree",
          "placement": "right",
          "clientView": "context"
        }
      ],
      "launcherSearchProviders": [
        {
          "id": "docs",
          "title": "Docs",
          "command": "search-docs"
        }
      ],
      "routes": [
        {
          "id": "home",
          "title": "Workspace Tools",
          "clientView": "home"
        }
      ]
    }
  }
}

配置字段:

  • config.schema: 插件配置的 JSON Schema。插件详情页的「配置」tab 会把它渲染成交互表单,值保存到当前实例的 plugins[].options
  • config.jsonSchema: config.schema 的等价别名。
  • config.uiSchema: 可选。直接提供配置页内部的 ConfigUiObjectSchema 时,会跳过 JSON Schema 自动推断。

JSON Schema 自动推断当前支持:

  • 根对象的 properties 会按路径展开成表单字段。
  • stringnumberintegerboolean、字符串数组会渲染成对应输入控件。
  • enumoneOf / anyOf 中的字符串 const 会渲染成下拉选项。
  • format: "textarea" / "multiline"x-oneworks-ui.control: "textarea" 会渲染成多行文本。
  • 无法映射的对象、数组或复杂类型会使用 JSON 编辑字段兜底。
  • titleI18ndescriptionI18ni18n.<language>.title/description 会按当前界面语言选择文案;zhzh-CNzh-Hans 会按同一中文语系兜底匹配。
  • x-oneworks-ui.iconplaceholdercontrolsensitive 可以补充图标、占位符、控件类型和敏感字段展示。

界面贡献字段:

  • navItems: 注册左侧导航入口。默认打开 /ui/plugins/<scope>/<id>,也可以显式写 routecommand
  • navMoreMenu: 注册左侧更多菜单项,支持 commandroutehref
  • navFooterBefore: 注册左侧底部「菜单」按钮上方的结构化入口,支持 commandroutehref;宿主负责布局、active 状态和图标尺寸。
  • chatHeaderActions: 注册 chat header 右上角 action 按钮,点击后执行当前 plugin scope 下的 command
  • chatHeaderMoreMenu: 注册 chat header 更多菜单项。
  • routeHeaderActions: 注册通用路由容器 header 右侧按钮,点击后执行当前 plugin scope 下的 command,用 targetRoute / targetRoutes 限定宿主路由。
  • routeWindowBarActions: 注册侧栏折叠时 window bar 按钮,适合和 routeHeaderActions 成对提供紧凑态入口,同样用 targetRoute / targetRoutes 限定宿主路由。
  • routeMoreMenuItems: 扩展当前路由容器的「更多」菜单项,支持 commandroutehref;菜单项里的 route 是点击后的导航目标。
  • routeSidebarContextMenu: 扩展当前路由侧栏 root / group / item 的右键菜单项,支持 commandroutehref;菜单项里的 route 是点击后的导航目标。
  • routes: 注册 plugin 页面,路径是 /ui/plugins/<scope>/<routeId>;未写 routeId 时使用 id
  • workbenchTabs: 声明可创建的 workbench tab 模板。它们不会在启动后常驻显示。
  • workbenchAddMenu: 注册下方面板 + 菜单项。设置 tab 后,点击菜单项会根据对应 workbenchTabs[].id 创建一个新的 tab 实例;新 tab 可以关闭。未设置 tabcommandroutehref 时,会尝试用菜单项自己的 id 找同名 tab。
  • workspaceDrawerTabs: 注册工作区抽屉 tab。当前它会归一到同一套 workbench tab 模型里,placement 可以是 rightbottom
  • launcherSearchProviders: 注册 launcher 搜索源。桌面 launcher 里使用 server-backed command;普通 workspace client 里也可以在前端 activatePlugin 中动态注册本地搜索源。

route 级贡献用 targetRoutetargetRoutes 表达“显示在哪个宿主 route container 上”。它们是宿主显示条件,不是点击行为;菜单 / 导航项里的 route 仍只表示点击后的跳转目标。不要把 route 当成宿主匹配条件,也不要把 targetRoute 当成跳转地址。

targetRoute / targetRoutes 的值支持:

  • route key,例如 pluginsplugin-routeconfig
  • 当前 pathname,例如 /plugins/plugins/demo/ui/plugins/ui/plugins/demo
  • /* 结尾的路径通配,例如 /plugins/*/ui/plugins/*

路径匹配会同时考虑应用内路径和带 client basename 的路径。默认 Web basename 是 /ui,所以 targetRoute: "/plugins/*"targetRoute: "/ui/plugins/*" 都可以匹配插件页面。配置页历史上同时出现过 settings / config 命名,route key 匹配时二者互为 alias:targetRoute: "settings" 可以匹配当前 config route,targetRoute: "config" 也可以匹配旧的 settings route。

route 级 chrome 目前可以扩展四类入口:header action、侧栏折叠态 window bar action、route more menu、route sidebar 入口 / 列表右键菜单。它们会与 route 自己声明的 chrome 合并,不会替换 route-owned action。菜单贡献只提供结构化 item;宿主会用通用 nav-rail-more-menu / overlay 结构渲染嵌套菜单、选中态、快捷键、危险态和自定义 item,不要在业务 route 或 plugin 里自己拼菜单 DOM / 样式。

这些能力的可用性取决于宿主 route 是否接入对应 container 能力:header action、window bar action 和 route more menu 由 useRoutePluginChrome(routeKey) 统一安装;route sidebar 右键菜单只有在该 route 提供共享 sidebar 数据时才会渲染。sidebar 右键菜单可以由宿主按 root / group / item 动态解析,plugin command 会收到当前 route 和被右键目标的 payload,避免插件读取 DOM 或猜 URL。

route action 状态字段由通用 chrome 统一渲染:active 会切换为 activeIcon / activeLabel / activeTitledisabled 会禁用按钮,danger 会在 hover/focus 时使用危险色,shortcut 会进入 tooltip;菜单型贡献额外支持 selected。旧 manifest 里的 routeMoreMenu 会被兼容归一到 routeMoreMenuItems,新插件只写 routeMoreMenuItems

插件扩展点的原则是:通用布局交互沉淀到 route container / layout 组件;route 层负责业务状态和 slot 内容;plugin 只注册贡献、命令和需要渲染的 view / slot 内容。需要新增通用 chrome 能力时,应扩展宿主结构化 API,而不是让每个 plugin 或 route 复制 header、window bar、右键菜单或 overlay 样式。

idscope、命令和贡献注册都在 plugin scope 内隔离。不同 plugin 不能抢占同一个 scope;plugin 也不能注册顶层 /api/* 路由。

前端入口

插件 client 入口会被前端动态加载。新插件优先通过 package.jsonexports["./client"] 暴露入口;旧插件也可以继续用 plugin.client.entry。入口模块导出 activatePlugin(ctx)。推荐把 UI 写成 React view,用宿主暴露的 ctx.reactview.ui,避免在插件里拼 DOM 字符串:

js
function HomeView({ ctx, react, view }) {
  const h = react.createElement
  const { Sender } = view.ui
  const placeholder = view.i18n.resolveText({
    en: 'Type a message',
    'zh-Hans': '输入消息'
  })

  return h(
    'main',
    { className: 'workspace-tools' },
    h('pre', null, JSON.stringify(view.host, null, 2)),
    h(Sender, {
      placeholder,
      onSend(text) {
        console.log('plugin sender submit', text, view.host)
      }
    })
  )
}

export async function activatePlugin(ctx) {
  const style = document.createElement('style')
  style.textContent = '.workspace-tools { padding: 16px; }'
  document.head.appendChild(style)

  const view = ctx.views.register('home', {
    renderNode: view =>
      ctx.react.createElement(HomeView, {
        ctx,
        react: ctx.react,
        view
      })
  })

  const command = ctx.commands.register('snapshot', () => {
    window.dispatchEvent(new CustomEvent('workspace-tools:snapshot'))
    return { ok: true }
  })

  return {
    dispose() {
      view.dispose()
      command.dispose()
      style.remove()
    }
  }
}

可用的前端能力:

  • ctx.scope: 当前 plugin scope。
  • ctx.i18n.getLanguage(): 读取当前界面语言。前端命令、toast、launcher 本地搜索等不在 view render 内执行的逻辑应在运行时读取它,而不是缓存激活时的语言。
  • ctx.i18n.resolveText(value, fallback): 按当前界面语言解析本地化文案。value 可以是字符串,也可以是 { en, "zh-Hans" } 这样的语言映射。
  • ctx.react: 宿主 React 单例的轻量出口,包含 createElementFragmentuseStateuseEffectuseMemouseCallbackuseRef。无构建插件可以直接用它写 React;有构建插件也应把 React 视为宿主单例,不要 bundle 第二份 React。
  • ctx.views.register(viewId, { renderNode }): 注册 React view;manifest 里的 clientView 会引用这里的 viewId。这是新 UI 插件的推荐写法。
  • ctx.views.register(viewId, render): 注册兼容 DOM view。只有无 React 需求的简单插件才使用这个路径。
  • ctx.commands.register(commandId, handler): 注册前端命令。
  • ctx.commands.execute(commandId, payload): 执行当前 scope 的前端命令;如果前端没有注册,会转到 server plugin command。
  • ctx.slots.register(slot, contribution): 动态注册 slot 贡献,slot 名包括 nav.itemsnav.moreMenunav.footer.beforechat.header.actionschat.header.moreMenuroute.header.actionsroute.windowBar.actionsroute.moreMenu.itemsroute.sidebar.contextMenuworkbench.addMenuworkbench.tabslauncher.searchProviders。动态注册的 route 级贡献也必须遵守同一套语义:targetRoute / targetRoutes 决定显示在哪个宿主 route,route 只表示点击后的跳转目标。
  • ctx.extensionPoints.register({ id, title, description, contributionSchema }): 注册当前插件暴露给其他插件的扩展点。扩展点完整 id 是 <scope>/<id>
  • ctx.extensionPoints.onAvailable(target, callback): 监听扩展点可用事件,target 可以是当前 scope 下的 id,也可以是跨插件的 <scope>/<id>。如果目标点已经存在会立即触发;如果晚于当前插件注册,也会在目标点出现时触发。callback(point) 可以返回 disposable 或 Promise,目标扩展点卸载时宿主会自动清理这次贡献。
  • ctx.extensionPoints.has(target): 判断扩展点是否存在,仅适合只读判断;需要注册贡献时优先使用 onAvailable(...),避免激活顺序不同导致贡献丢失。
  • ctx.extensionPoints.contribute(target, contribution): 向已存在的扩展点贡献结构化能力。contribution 必须有稳定 id,通常也应提供 title / titleI18ndescriptionI18niconcommand
  • ctx.pluginApis.register({ id, title, description, inputSchema, outputSchema, handler }): 暴露当前插件的纯前端 API,供其他插件在同一个 client runtime 内调用。handler(input, meta) 可以返回值或 Promise;meta 包含 callerScopetargetScopeapiId
  • ctx.pluginApis.call(target, input, options?): 调用其他插件暴露的纯前端 API。所有调用都返回 Promise;如果目标 API 还没注册,Promise 会先等待 API 挂载,再等待 handler 完成。target 使用 <scope>/<id>,同 scope 调用可只传 idoptions.timeoutMs 可用于需要明确超时的场景。
  • ctx.routes.register({ id, title, viewId }): 动态注册 /plugins/<scope>/<id> 页面。
  • ctx.launcher.registerSearchProvider(provider): 在 workspace client 内注册本地 launcher 搜索源。
  • ctx.notifications.show(message): 在宿主右下角消息队列里发布界面级消息。宿主会自动注入当前 plugin 来源,卡片会展示插件名、发送时间、标题和 markdown 描述;scope、屏蔽 / 关闭和自定义操作会在 hover / focus 后展示,避免普通消息占用过多视觉密度。
  • ctx.notifications.close(id): 关闭一条仍在队列里的消息。
  • ctx.notifications.muteCurrentPlugin(): 屏蔽当前 plugin 后续发布的界面级消息;用户也可以在消息卡片上执行同样的通用操作。
  • ctx.api.fetch(path, init): 只允许访问当前 scope 下的 scoped API,例如 ctx.api.fetch('echo/foo') 会请求 /api/plugins/<scope>/proxy/echo/foo
  • ctx.hot.accept(callback)ctx.hot.reload(): 配合 watch / dev server 调试当前 plugin,不需要重启 Electron。

插件需要提示用户时应使用宿主消息队列,不要自己创建 fixed toast、引入第三方通知组件或复制宿主样式。description 默认按 markdown 渲染,普通消息默认约 6.5 秒后自动清理,带操作的消息默认约 9.5 秒后自动清理;用户 hover 消息时会暂停自动清理。只有需要明确常驻时才传 ttlMs: nullactions[].onClick 可以触发插件命令、跳转或更新插件视图状态:

js
ctx.notifications.show({
  level: 'success',
  title: ctx.i18n.resolveText({
    en: 'Index completed',
    'zh-Hans': '索引完成'
  }),
  description: 'Indexed **24 files**.',
  actions: [
    {
      id: 'open',
      title: 'Open',
      icon: 'open_in_new',
      onClick: async ({ close }) => {
        await ctx.commands.execute('open-result')
        close()
      }
    }
  ]
})

插件间扩展点也可以在 manifest 中声明,适合让点位在所有 client entry 激活前就可发现:

json
{
  "plugin": {
    "contributions": {
      "extensionPoints": [
        {
          "id": "quick-actions",
          "titleI18n": {
            "en": "Quick actions",
            "zh-Hans": "快捷操作"
          },
          "descriptionI18n": {
            "en": "Lets other plugins add actions to this plugin view.",
            "zh-Hans": "允许其他插件向当前插件视图补充操作。"
          },
          "contributionSchema": {
            "type": "object",
            "required": ["id", "title", "command"],
            "additionalProperties": true
          }
        }
      ],
      "extensionContributions": [
        {
          "id": "run-addon",
          "target": "demo/quick-actions",
          "title": "Run add-on action",
          "icon": "add_link",
          "command": "plugin-demo-extension/run-addon"
        }
      ]
    }
  }
}

贡献方不要在激活时只做一次 has(...) 判断;扩展点可能来自另一个还没激活完成的插件。推荐写法是监听目标扩展点出现,并把这一次贡献的 disposable 返回给宿主:

js
ctx.extensionPoints.onAvailable(
  'demo/quick-actions',
  point =>
    ctx.extensionPoints.contribute('demo/quick-actions', {
      id: 'run-addon',
      title: 'Run add-on action',
      icon: 'add_link',
      command: `${ctx.scope}/run-addon`,
      extensionPointTitle: point.title
    })
)

拥有方 view 通过 view.extensions.getContributions('quick-actions') 读取当前扩展点贡献;跨 scope 读取时传 <scope>/<id>。如果扩展点不存在会返回空数组。

纯前端插件 API 适合封装不需要 HTTP 边界的插件内部过程调用。调用方只需要 await Promise;运行时会处理“目标插件还没 ready / API 还没 register”的等待:

js
ctx.pluginApis.register({
  id: 'describe',
  title: 'Describe current plugin state',
  inputSchema: {
    type: 'object',
    additionalProperties: true
  },
  outputSchema: {
    type: 'object',
    required: ['scope'],
    properties: {
      scope: { type: 'string' }
    }
  },
  handler: async (input, meta) => ({
    input,
    callerScope: meta.callerScope,
    scope: ctx.scope
  })
})

const result = await ctx.pluginApis.call('demo/describe', { from: ctx.scope })

React 与热更新边界

  • .oo/plugins.dev/<name>、显式 watch: true 和插件详情页 watch 开关对 manifest、server、README、静态入口,以及 host Vite client source 下普通 TS / 数据 / i18n / model 模块变更提供 plugin 级 reload:文件变化后 server 发送 plugin.changed,前端 dispose 当前 scope、重新拉取 plugin 列表并重新 import entry。
  • ctx.hot.reload() 触发同一套 plugin 级 reload;ctx.hot.accept(callback) 用于 reload 前清理或同步状态,不等同于 Vite import.meta.hot.accept
  • 本地路径 plugin 的 exports["./client"].source 会在开发态走宿主 Vite /@fs entry,TSX / JSX 组件和样式模块的 HMR、React Fast Refresh 由宿主 Vite dev server 提供。这一路径仍应复用宿主 React 单例:React 不要被打进插件 bundle,宿主组件继续通过 view.ui.* 使用。
  • 无构建插件可以直接使用 ctx.react.createElementctx.react.useState 等 hooks 写 React view;这种模式没有模块级 Fast Refresh,但能避免 DOM 字符串和手动挂载清理。

View context、宿主状态与通用组件

React view 的 renderNode(view) 会收到 view context。兼容 DOM view 的 render(container, view) 会收到同一个 view 作为第二个参数。宿主会在独立 plugin route、底部 workbench tab 和右侧 drawer tab 渲染时传入当前界面状态:

js
ctx.views.register('home', {
  renderNode(view) {
    return ctx.react.createElement(HomeView, { ctx, react: ctx.react, view })
  }
})

view.host 当前包含:

  • language: 当前界面语言,例如 zhzh-Hansen
  • themeMode: 用户选择的主题来源,值为 lightdarksystem
  • resolvedThemeMode: 实际生效主题,值为 lightdark
  • isDarkMode: 当前是否深色模式。
  • surface: 当前 view 的宿主位置,值为 routeworkbenchdrawer

语言、主题或宿主位置变化时,React view 会按普通 React 组件更新;DOM view 会重新调用 render,并先清理上一轮返回的 dispose()。插件如果在 DOM view 内注册事件、timer、MutationObserver 或宿主组件,也必须在 dispose() 里清理。

view.i18n 当前包含:

  • language: 当前界面语言,与 view.host.language 一致。
  • resolveText(value, fallback): 按当前界面语言解析插件自带文案。React view 内的按钮、placeholder、菜单项和状态文案应使用这个方法,不要用 view.host.language.startsWith('zh') 手写分支。

view.ui 是宿主暴露给 React view 的声明式组件集合,组件名和命令式 component id 一一对应:

  • view.ui.Icon
  • view.ui.Button
  • view.ui.Input
  • view.ui.Segmented
  • view.ui.Switch
  • view.ui.Sender
  • view.ui.ProjectFileTree
  • view.ui.OverlayDropdown
  • view.ui.OverlayMenu
  • view.ui.OverlaySearchMenu
  • view.ui.OverlaySearchRow
  • view.ui.OverlaySegmented
  • view.ui.OverlaySelectLabel
  • view.ui.OverlayTree

React view 应优先使用 view.ui.*view.components.render(component, container, props) 是 DOM view 的兼容命令式 API,可以把宿主内置组件直接渲染到插件自己的 DOM 挂载点,返回 { dispose() }

view.ui.*view.components.render(...) 使用同一套 props:

  • button: 渲染宿主按钮。常用 props:labelicontype: "default" | "primary" | "text" | "link" | "dashed"size: "small" | "middle" | "large"dangerdisabledtitleshapeonClick()
  • icon: 渲染宿主图标。常用 props:nameariaLabeltitlesize: "small" | "middle" | "large" | numbertone: "default" | "muted" | "primary" | "success" | "warning" | "danger"
  • input: 渲染宿主输入框。常用 props:valueplaceholderariaLabeltype: "text" | "password" | "textarea"rowsallowClearsizedisabledonChange(value)onCommit(value)onCommit 会在 blur 或回车时触发,适合插件把输入提交为配置。
  • overlayDropdown: 渲染真实的宿主 dropdown 触发器和浮层。常用 props:labeliconplacementopendefaultOpencloseOnSelectonOpenChange(open)contentcontent 支持 { type: "menu" | "searchMenu" | "tree", props },对应 props 与下面的 overlayMenu / overlaySearchMenu / overlayTree 相同。需要真实弹出式 overlay 时优先用它,不要把 overlay primitive 直接平铺在页面里。
  • overlayMenu: 渲染宿主 overlay 菜单 primitive。常用 props:itemsselectedKeysdefaultOpenKeysopenKeyssubmenuPlacement: "left" | "right"submenuTrigger: "click" | "hover"surfacewidthonItemClick(item)onOpenKeysChange(keys)items 支持 action、{ type: "divider" }{ type: "section" },action 支持 icondescriptionshortcutselecteddisabledconfirmLabeltone: "danger"children
  • overlaySearchMenu: 渲染带搜索行的宿主 overlay 菜单。常用 props:itemssearchValueplaceholderemptyLabelselectedKeyssearchPlacement: "top" | "bottom"onSearchChange(value)onItemClick(item)
  • overlaySearchRow: 渲染宿主 overlay 搜索行。常用 props:valueplaceholderclearLabelautoFocusonChange(value)onClear()
  • overlaySegmented: 渲染宿主 overlay 分段控件。常用 props:ariaLabelvalueoptions: [{ value, label, icon }]onChange(value)
  • overlaySelectLabel: 渲染宿主 overlay select label。常用 props:iconlabelmeta
  • overlayTree: 渲染宿主 overlay 树。常用 props:nodescollapsedKeysdefaultCollapsedKeysexpandAllsurfaceonNodeToggle(key)onNodeActivate(node)surface: true 会给树套用宿主 overlay panel 外观。nodes 支持 keylabelmetaiconcollapsedIconexpandedIconselecteddisabledconfirmLabeltrailingIconchildren
  • segmented: 渲染宿主分段选择。常用 props:valueoptions: [{ value, label, icon, disabled }]iconOnlyblockariaLabelsizedisabledonChange(value)
  • sender: 渲染宿主 sender 输入组件,默认套用 chat sender surface,并包含模型、权限、adapter / account 和 status bar 编排。样式只能通过结构化选项控制:surface: "chat" | "plain"density: "default" | "compact"。常用 props:placeholderinitialContentdefaultAdapterdefaultModelsubmitLabelautoFocusadapterLockedmodelUnavailableshowHeadershowStatusBarhideReferenceActionshideSelectionControlshideSubmitActionsubmitLoadingstopLoadingonInputChange(value)onSend(text, mode)onSendContent(content, mode)onCancel()
  • switch: 渲染宿主开关。常用 props:checkedcheckedLabeluncheckedLabelsize: "small" | "default"disabledonChange(checked)
  • projectFileTree: 渲染项目目录树。常用 props:sessionIdactivePathselectedPathsselectionMode: "none" | "multiple"selectableTypes: "all" | "files"showContextMenushowLoadingStaterefreshKeyonOpenFile(path)onReferenceNodes(nodes)onSelectionChange(selection)

宿主组件仍然运行在 One Works 前端里,会自动跟随语言、主题、布局、权限和 workspace API。插件不要复制图标、按钮、输入框、分段选择、开关、sender、目录树、菜单、浮层、模型选择器或文件选择器的 DOM/CSS;现有组件不够时应优先扩展 view.ui / view.components 的结构化 API。

宿主组件用法示例

React view 直接从 view.ui 取宿主组件:

js
function HomeView({ react, view }) {
  const h = react.createElement
  const { OverlayDropdown, Sender } = view.ui
  const senderPlaceholder = view.i18n.resolveText({
    en: 'Type a message',
    'zh-Hans': '输入消息'
  })
  const commandsLabel = view.i18n.resolveText({
    en: 'Commands',
    'zh-Hans': '命令'
  })
  const searchPlaceholder = view.i18n.resolveText({
    en: 'Search commands',
    'zh-Hans': '搜索命令'
  })

  return h(
    'main',
    { className: 'workspace-tools' },
    h(Sender, {
      surface: 'chat',
      density: 'default',
      showHeader: true,
      showStatusBar: true,
      placeholder: senderPlaceholder,
      initialContent: '',
      defaultAdapter: 'default',
      defaultModel: 'model-key',
      onSend(text, mode) {
        console.log('sender submit', { text, mode })
      }
    }),
    h(OverlayDropdown, {
      label: commandsLabel,
      icon: 'terminal',
      content: {
        type: 'searchMenu',
        props: {
          placeholder: searchPlaceholder,
          items: [
            {
              key: 'open',
              label: 'Open command',
              icon: 'terminal',
              shortcut: 'O'
            },
            {
              key: 'view',
              label: 'View mode',
              icon: 'visibility',
              children: [
                {
                  key: 'view-list',
                  label: 'List',
                  icon: 'view_list',
                  selected: true
                },
                { key: 'view-tree', label: 'Tree', icon: 'account_tree' }
              ]
            }
          ],
          onItemClick(item) {
            console.log('command selected', item.key)
          }
        }
      }
    })
  )
}

ctx.views.register('home', {
  renderNode(view) {
    return ctx.react.createElement(HomeView, { react: ctx.react, view })
  }
})

如果不是弹出式浮层,而是要在当前插件页面里平铺一个宿主 overlay surface,可以直接渲染 primitive,并让宿主套上 panel 外观:

js
function TreePanel({ react, view }) {
  const h = react.createElement
  const { OverlayTree } = view.ui

  return h(OverlayTree, {
    surface: true,
    nodes: [
      {
        key: 'workspace',
        label: 'Workspace',
        collapsedIcon: 'folder',
        expandedIcon: 'folder_open',
        children: [{ key: 'readme', label: 'README.md', icon: 'article' }]
      }
    ],
    onNodeActivate(node) {
      console.log('tree node', node.key)
    }
  })
}

兼容 DOM view 仍然可以使用 view.components.render(...) 命令式挂载宿主组件。这个路径需要插件自己准备 DOM 挂载点,并在 dispose() 里清理:

js
ctx.views.register('home-dom', (container, view) => {
  container.innerHTML =
    '<main class="workspace-tools"><div data-sender></div></main>'

  const sender = view.components.render(
    'sender',
    container.querySelector('[data-sender]'),
    {
      placeholder: 'Type a message',
      onSend(text) {
        console.log('sender submit', text)
      }
    }
  )

  return {
    dispose() {
      sender.dispose()
    }
  }
})

可用 CSS 变量

插件 CSS 应优先使用宿主 token,而不是硬编码新主题。常用稳定变量:

  • 文本与背景:--ant-color-text--ant-color-text-secondary--ant-color-bg-container--ant-color-bg-layout--bg-color--sub-bg-color--text-color--sub-text-color
  • 边框与填充:--ant-color-border--ant-color-border-secondary--ant-color-fill-quaternary--border-color--sub-border-color
  • 品牌与状态:--primary-color--primary-soft-bg--primary-text-color--success-color--warning-color--danger-color
  • Chrome 尺寸:--app-chrome-icon-size--app-chrome-action-gap--route-container-header-overlay-height--route-container-header-padding-inline--route-container-header-padding-block

推荐写法:

css
.workspace-tools {
  color: var(--ant-color-text, var(--text-color));
  background: transparent;
}

.workspace-tools__output {
  border: 1px solid var(--ant-color-border-secondary, var(--border-color));
  background: var(--ant-color-fill-quaternary, var(--sub-bg-color));
}

One Works 文档站独立构建,面向用户接入与使用。支持邮箱:support@oneworks.cloud