UI 扩展
插件不只是后台能力,还可以把界面注入客户端:侧边栏面板、聊天区顶部按钮、设置页 Tab、工作台数据卡片。UI 用 React + TypeScript 编写,由 scripts/build-ui.mjs 打包为单个 ESM bundle ui.js。
扩展点总览
| 扩展点 | 注入位置 | 声明字段 | 组件映射 |
|---|---|---|---|
| 侧边栏面板 | 左侧导航栏 | contributes.sidebarPanels | components |
| 顶部按钮 | 聊天区头部操作区 | contributes.topActions | topActions |
| 设置页 Tab | 设置页导航 | contributes.settingsTabs | settingsTabs |
| 工作台卡片 | 工作台首页 | contributes.widgetCards | widgetCards |
所有 UI 扩展都需要:
contributes.ui.entry指向ui.jspermissions中声明ui.renderui-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.jsonUI 入口
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):
| 键 | 类型 | 对应声明 |
|---|---|---|
components | Record<string, ComponentType<PluginPanelProps>> | sidebarPanels[].component |
topActions | Record<string, ComponentType<PluginPanelProps>> | topActions[].component |
settingsTabs | Record<string, ComponentType<PluginPanelProps>> | settingsTabs[].component |
widgetCards | Record<string, ComponentType<WidgetCardPreviewProps>> | widgetCards[].previewComponent |
configActions | Record<string, PluginConfigActionHandler> | configSchema.fields[].action.kind |
组件 Props
PluginPanelProps
所有面板/按钮/Tab 组件都只接收一个 prop:
tsx
interface PluginPanelProps {
host: PluginHostApi
}PluginHostApi
插件组件通过 props.host 访问宿主能力,客户端不会把 window.electronAPI 直接暴露给插件:
| 成员 | 说明 |
|---|---|
pluginKey | 插件标识 |
serverId | MCP 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 |
| target | es2020 |
| platform | browser |
| JSX | automatic 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 里声明这些依赖 —— 声明了也不会被打进产物,还会造成版本歧义。
约束与注意事项
- ID 前缀:所有扩展点
id必须以<pluginKey>:开头,否则可能与内置项冲突。 - 组件键名一致:
contributes里component/previewComponent的值必须与ui.js导出映射的键完全一致(大小写敏感),否则该扩展点静默不渲染。 - 必须声明
ui.render权限。 - 构建产物要提交:
ui.js需位于插件根目录并在打包范围内(发布脚本会自动内联为inlineUiCode)。 - 类型解析依赖客户端仓库:
tsconfig.json的paths指向../aweeclaw-client/node_modules,构建ui-src时客户端仓库需与本仓库同级。 - 不要依赖
window.electronAPI:能力访问统一走props.host。

