Skip to content

UI 扩展 ​

插件不只是后台能力,还可以把界面注入客户端:侧边栏面板、聊天区顶部按钮、设置页 Tab、工作台数据卡片。UI 用 React + TypeScript 编写,由 scripts/build-ui.mjs 打包为单个 ESM bundle ui.js。


扩展点总览 ​

扩展点注入位置声明字段组件映射
侧边栏面板左侧导航栏contributes.sidebarPanelscomponents
顶部按钮聊天区头部操作区contributes.topActionstopActions
设置页 Tab设置页导航contributes.settingsTabssettingsTabs
工作台卡片工作台首页contributes.widgetCardswidgetCards

所有 UI 扩展都需要:

  1. contributes.ui.entry 指向 ui.js
  2. permissions 中声明 ui.render
  3. ui-src/ 目录与构建产物 ui.js

contributes 声明 ​

json
{
  "permissions": ["ui.render", "network"],
  "contributes": {
    "ui": { "entry": "ui.js" },
    "sidebarPanels": [
      {
        "id": "ai-macro-recorder:panel",
        "icon": "Video",
        "label": "Macros",
        "labelZh": "宏管理",
        "component": "macro-panel",
        "position": 50,
        "wideMode": false
      }
    ],
    "topActions": [
      {
        "id": "ai-macro-recorder:record",
        "component": "record-button",
        "position": 50
      }
    ],
    "settingsTabs": [
      {
        "id": "my-plugin:settings",
        "icon": "Settings",
        "label": "My Plugin",
        "labelZh": "我的插件",
        "component": "settings-tab",
        "position": 50
      }
    ],
    "widgetCards": [
      {
        "id": "ai-macro-recorder:stats",
        "icon": "CirclePlay",
        "label": "Macro Stats",
        "labelZh": "录制统计",
        "description": "Total runs: {{count}}",
        "tier": "core",
        "previewComponent": "MacroCardPreview",
        "modes": ["work"]
      }
    ]
  }
}

sidebarPanels ​

字段必需说明
id✅面板唯一 ID,必须以 <pluginKey>: 为前缀,避免与内置面板冲突
icon✅lucide 图标名
label / labelZh✅英文 / 中文标签
component✅ui.js 中 components 映射的键名
position❌排序权重,默认 50(位于内置项之后)
wideMode❌true 时面板在主内容区全屏展示,而非挤在侧栏

topActions ​

字段必需说明
id✅按钮唯一 ID,以 <pluginKey>: 为前缀
component✅ui.js 中 topActions 映射的键名
position❌排序权重,默认 50

顶部按钮需自行渲染图标与状态,通过 props.host 调用插件工具。

settingsTabs ​

字段必需说明
id / icon / label / labelZh✅同侧边栏面板
component✅ui.js 中 settingsTabs 映射的键名
position❌排序权重,默认 50

widgetCards ​

工作台数据卡片,通过插件 MCP 工具按需拉取数据并自动刷新。完整说明(数据获取协议、刷新策略、卡片库集成)见插件工作台卡片。

字段必需说明
id✅卡片 ID,格式 <pluginKey>:<cardName>
icon / label / labelZh✅图标与标题
description / descriptionZh❌卡片底部描述,支持 {{variable}} 占位符
tier❌core(15s 刷新)/ enhanced(60s 刷新,默认)
previewComponent✅ui.js 中 widgetCards 映射的键名
modes❌关联场景模式:work / life / study;不填则所有模式可见

目录结构 ​

plugins/my-plugin/
├── manifest.json
├── index.js               # MCP 入口(提供数据与动作)
├── ui-src/
│   ├── index.tsx          # UI 入口,default 导出 PluginUiModule
│   ├── MyPanel.tsx        # 侧边栏面板组件
│   └── types.ts           # 类型定义(可自行声明 props 类型)
├── ui.js                  # 构建产物(发布时内联为 inlineUiCode)
└── package.json

UI 入口 ​

ui-src/index.tsx 的 default 导出形状:

tsx
import type { ComponentType } from 'react'
import { MyPanel } from './MyPanel'
import { RecordButton } from './RecordButton'

const uiModule = {
  // contributes.sidebarPanels[].component → 组件
  components: {
    'macro-panel': MyPanel as ComponentType<any>,
  },
  // contributes.topActions[].component → 组件
  topActions: {
    'record-button': RecordButton as ComponentType<any>,
  },
  // contributes.settingsTabs[].component → 组件(可选)
  settingsTabs: {},
  // contributes.widgetCards[].previewComponent → 组件(可选)
  widgetCards: {},
  // configSchema.fields[].action.kind → handler(可选)
  configActions: {},
}

export default uiModule

完整类型定义(PluginUiModule):

键类型对应声明
componentsRecord<string, ComponentType<PluginPanelProps>>sidebarPanels[].component
topActionsRecord<string, ComponentType<PluginPanelProps>>topActions[].component
settingsTabsRecord<string, ComponentType<PluginPanelProps>>settingsTabs[].component
widgetCardsRecord<string, ComponentType<WidgetCardPreviewProps>>widgetCards[].previewComponent
configActionsRecord<string, PluginConfigActionHandler>configSchema.fields[].action.kind

组件 Props ​

PluginPanelProps ​

所有面板/按钮/Tab 组件都只接收一个 prop:

tsx
interface PluginPanelProps {
  host: PluginHostApi
}

PluginHostApi ​

插件组件通过 props.host 访问宿主能力,客户端不会把 window.electronAPI 直接暴露给插件:

成员说明
pluginKey插件标识
serverIdMCP server ID,固定为 plugin:<pluginKey>
callTool(toolName, args)直接调用本插件的 MCP 工具(不经过 AI)
getConfig()读取用户配置(Record<string, string>)
sendChatMessage(text)向当前会话发送一条用户消息并触发 AI 处理(用于把任务交接给 AI)
language当前界面语言,'zh' / 'en'
log受控日志(自动加 [Plugin:<pluginKey>] 前缀)

callTool 返回:

ts
{
  success: boolean
  content?: Array<{ type: string; text?: string; data?: string; mimeType?: string }>
  error?: string
}

组件示例 ​

tsx
import { useEffect, useState } from 'react'

interface Props {
  host: {
    language: 'zh' | 'en'
    log: { info: (...a: unknown[]) => void; error: (...a: unknown[]) => void }
    callTool: (name: string, args: Record<string, unknown>) => Promise<{
      success: boolean
      content?: Array<{ type: string; text?: string }>
      error?: string
    }>
  }
}

export function MacroPanel({ host }: Props) {
  const [count, setCount] = useState<number | null>(null)
  const isZh = host.language === 'zh'

  useEffect(() => {
    host.callTool('list_macros', {})
      .then((res) => {
        if (!res.success) {
          host.log.error('[macro-panel] list_macros failed:', res.error)
          return
        }
        const raw = res.content?.find((c) => c.type === 'text')?.text ?? '[]'
        setCount(JSON.parse(raw).length)
      })
      .catch((e) => host.log.error('[macro-panel] unexpected:', e))
  }, [host])

  return (
    <div className="flex flex-col gap-2 p-3">
      <span className="text-sm">{isZh ? '宏数量' : 'Macros'}</span>
      <span className="text-[20px] font-semibold">{count ?? '--'}</span>
    </div>
  )
}

要点:

  • 面板数据一律通过 host.callTool 走插件自己的 MCP 工具获取,不要在 UI 里直接发请求;
  • 组件由客户端错误边界包裹,但内部异步错误仍要自行 catch 并 host.log.error 记录,便于在客户端日志里定位;
  • 文案按 host.language 双语切换,与客户端界面语言保持一致。

配置表单动作处理 ​

configSchema.fields[].action.kind 对应的处理器在 configActions 中注册,用于实现"测试连接""拉取模型列表"等按钮:

tsx
const uiModule = {
  configActions: {
    // kind 与 manifest 中 action.kind 一致
    async fetchModels(params: Record<string, string>, formValues: Record<string, string>, host: any) {
      const res = await fetch(`${formValues.base_url}/v1/models`, {
        headers: { Authorization: `Bearer ${formValues.api_key}` },
      })
      if (!res.ok) return { success: false, error: `HTTP ${res.status}` }
      const data = await res.json()
      return { success: true, items: data.data.map((m: any) => m.id) }
    },
  },
}
export default uiModule

返回结构:

字段说明
success是否成功
items结果列表(如模型列表),客户端弹出选择弹窗
value直接写入 targetField 的值(跳过选择弹窗)
error失败信息

fetchModels 与 testConnection 两个 kind 客户端已内置实现;只有自定义 kind 才需要在这里注册。


构建 UI bundle ​

修改 ui-src/ 后必须重新构建,否则发布的仍是旧的 ui.js:

bash
# 构建单个插件
node scripts/build-ui.mjs my-plugin

# 构建所有含 ui-src 的插件
node scripts/build-ui.mjs --all

# 或用 npm script
npm run build:ui -- my-plugin
npm run build:ui:all

构建配置(脚本已固定,无需手动调整):

项值
格式ESM
targetes2020
platformbrowser
JSXautomatic runtime
压缩开启(内联到 manifest 后体积敏感)
sourcemap不生成

共享依赖(external) ​

以下依赖不打包进 ui.js,运行时由客户端从 window.__AWEECLAW_SHARED__ 提供:

react
react-dom
react-dom/client
react/jsx-runtime
zustand
lucide-react
@xyflow/react
@xyflow/react/system
framer-motion

直接 import 即可,不要在插件 package.json 里声明这些依赖 —— 声明了也不会被打进产物,还会造成版本歧义。


约束与注意事项 ​

  1. ID 前缀:所有扩展点 id 必须以 <pluginKey>: 开头,否则可能与内置项冲突。
  2. 组件键名一致:contributes 里 component / previewComponent 的值必须与 ui.js 导出映射的键完全一致(大小写敏感),否则该扩展点静默不渲染。
  3. 必须声明 ui.render 权限。
  4. 构建产物要提交:ui.js 需位于插件根目录并在打包范围内(发布脚本会自动内联为 inlineUiCode)。
  5. 类型解析依赖客户端仓库:tsconfig.json 的 paths 指向 ../aweeclaw-client/node_modules,构建 ui-src 时客户端仓库需与本仓库同级。
  6. 不要依赖 window.electronAPI:能力访问统一走 props.host。

下一步 ​

AweeClaw AI 应用构建平台