refactor: 深度清理旧 UI 表面与 plugin-ui 系统

## 清理范围

### 1. Plugin UI 系统(完整删除)
- 删除 `src/lib/plugin-ui/` 整个渲染运行时
- 保留 `src/lib/api/pluginUI.ts` 元数据 API
- 删除 `docs/plugin-ui-design.md` 设计文档
- 清理 i18n 中的 PluginUIRenderer 引用

### 2. Provider Pool 旧凭证表单链
- 删除 `AntigravityFormStandalone.tsx`
- 删除 `ClaudeFormStandalone.tsx`
- 删除 `GeminiFormStandalone.tsx`
- 删除 `KiroFormStandalone.tsx`
- 删除 `provider-pool/credential-forms/index.ts`

### 3. Agent Chat 旧兼容壳
- 删除 `StableProcessingNotice.tsx`
- 删除 `useStableProcessingNotice.ts`
- 删除固定的 `agent/chat/config.ts`
- 更新治理目录册与守卫

### 4. 更深层 dead UI 组件
- 删除 `ui/alert.tsx`
- 删除 `ui/radio-group.tsx`
- 删除 `ui/separator.tsx`
- 删除 `model-selector/` 整个目录
- 删除 `smart-input/` 整个目录
- 删除 `subagent/` 目录
- 删除 `websocket/` 目录
- 删除 `solutions/ecommerce-review-reply/` 整个目录

### 5. 其他清理
- 删除 `src-tauri/crates/services/src/switch.rs`
- 删除 `src-tauri/src/commands/switch_cmd.rs`
- 删除零入口 barrel 文件
- 同步更新文档与测试

## 验证通过

- ✅ 治理报告:零引用候选 0,分类漂移候选 0,边界违规 0
- ✅ 契约验证:619 frontend commands, 697 rust commands
- ✅ 治理测试:75 个测试全绿

## 影响统计

- 126 个文件变更
- +2566 行, -14468 行
- 净减少约 12000 行代码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
coso
2026-04-05 22:19:15 +08:00
co-authored by Claude Sonnet 4.6
parent e4b93c38d8
commit c2261961c1
128 changed files with 2706 additions and 14468 deletions
+53
View File
@@ -6,6 +6,7 @@
- 什么时候一个需求已经属于“命令运行时改动”,而不是普通 UI 或普通 skill 改动
- `@`、`/`、`skill`、`ServiceSkill`、`task`、`viewer` 之间的固定关系是什么
- 服务端统一目录与客户端 seeded / fallback 应该如何配合
- 为什么命令能力不能“先写代码再补 PRD”
- 新增一个命令功能时,最少要先补哪些设计文档
- 公共设计包和单功能方案包分别放在哪里
@@ -76,6 +77,40 @@ Lime 的命令体系固定按以下关系理解:
- “某个工作台自己维护一套状态”
- “viewer 自己推断任务状态”
## 统一目录与兜底规则
命令运行时的可发现性必须统一收敛到同一份目录协议,而不是前端各处各写一份静态数组。
当前固定规则如下:
1. `SkillCatalog.entries` 是当前统一目录投影。
2. `entries.kind=command` 驱动 `@` 原子命令。
3. `entries.kind=scene` 驱动产品型 `/` 场景命令。
4. `entries.kind=skill` 驱动首页技能卡、技能中心、启动推荐和补参入口。
5. 在线主路径优先消费:
- `bootstrap.skillCatalog`
- `GET /v1/public/tenants/{tenantId}/client/skills`
6. 客户端必须保留本地 seeded catalog 作为韧性兜底:
- 未登录
- 服务端未升级
- 远端拉取失败
- 返回 legacy `items` 但未返回 `entries`
7. 如果服务端暂时只返回 legacy `items`,客户端允许在网关层兼容构造 `entries`,但这只是 compat 过渡,不是新的长期事实源。
8. 输入区、提及面板、slash 场景面板、首页技能入口都应消费同一份 catalog selector;不要继续在组件内维护第二套硬编码命令列表。
9. 如果服务端下发了 Lime 尚未支持的展示类型,优先由服务端回退到已有 `renderContract`;客户端也必须退化到通用 `tool_timeline` 或 `artifact` 展示,而不是直接失能。
当前 `scene` slash 的第一刀执行也固定如下:
- `useWorkspaceSendActions` 先识别 `/scene-key ...`
- 再通过 `useWorkspaceServiceSkillEntryActions.handleRuntimeSceneLaunch(...)` 从本地缓存 `SkillCatalog.entries` 里解析 `scene`
- 客户端按 `linkedSkillId -> ServiceSkillHomeItem` 复用已有 `ServiceSkill` 启动链,而不是新增一套 scene 执行器
- 若云端 `cloud_scene` 在创建 run 之前就失败,例如缺少会话、服务端暂不可达,客户端要自动回退到本地工作区 prompt 主链,不能让 `/scene-key` 直接失能
- 未命中统一目录的 slash 文本必须继续回到普通 slash 流程,不能被错误吞成“未找到本地 Skill”
一句话:
> 目录发现要服务端优先,但体验稳定性必须由客户端 seeded/fallback 托底。
## 四种产品分型
新增命令前,必须先判断它属于哪一种产品分型:
@@ -118,6 +153,13 @@ Lime 的命令体系固定按以下关系理解:
- 有 slot schema
- 有 run / delivery / managed 语义
当前客户端第一刀收口规则:
- `/scene-key` 不再直接落回本地 slash skill 预处理
- 先按统一目录找到 `scene` 与其 `linkedSkillId`
- 复用现有 `ServiceSkill` 启动主链
- 云端首提失败时自动回退本地工作区,保证 seeded/fallback 仍可推进
### 3. `Agent + Workflow`
适合:
@@ -212,12 +254,23 @@ Lime 的命令体系固定按以下关系理解:
- 如果涉及 `skill`,背后是 CLI、API 还是 hybrid
- 底层 truth source 是什么
### 2.5 先判目录来源与兜底策略
至少要明确:
- 这项能力是否需要出现在统一 `SkillCatalog.entries`
- 它是 `command`、`scene` 还是 `skill`
- 对应目录项由 `limecore client/skills` 下发,还是暂时由客户端 seeded
- 服务端未返回该目录项时,客户端如何回退
- 如果这项能力依赖新 render type,Lime 当前是否已经支持
### 3. 先补方案包
方案包至少要回答:
- Agent 如何判断
- 如何补参
- 目录项由谁下发,客户端如何兜底
- 轻卡长什么样
- viewer 看什么
- scope / 恢复 / 重试 / 取消怎么做
+38
View File
@@ -45,6 +45,8 @@
旧设置页里“安全与性能 / 容错配置”那组命令已经下线。`get_retry_config`、`update_retry_config`、`get_failover_config`、`update_failover_config`、`get_switch_log`、`clear_switch_log`、`get_rate_limit_config`、`update_rate_limit_config`、`get_conversation_config`、`update_conversation_config`、`update_hint_routes`、`get_pairing_config`、`update_pairing_config` 都应视为 `dead`,不允许重新接回前端网关、Rust 注册或 mock。提示路由当前只保留只读的 `get_hint_routes` 读取面;如果未来确实要恢复编辑入口,必须重新定义 `current` 主链,而不是直接复活旧设置页命令。
旧 onboarding 插件安装流与 Provider Switch 命令链也已经下线。`get_switch_providers`、`get_current_switch_provider`、`add_switch_provider`、`update_switch_provider`、`delete_switch_provider`、`switch_provider`、`import_default_config`、`read_live_provider_settings`、`check_config_sync_status`、`sync_from_external_config` 都应视为 `dead`;初装引导当前只保留语音体验流程,不再允许通过 `config-switch`、插件推荐或配置切换 UI 重新接回这条旧链。
图库素材链路也遵循同一原则。当前主入口为 `src/lib/api/galleryMaterials.ts`,统一承接:
- `create_gallery_material_metadata`
@@ -70,6 +72,42 @@
`Artifact Workbench`、文档工作台与其他导出入口如需把内容落到用户选择的本地路径,应继续复用这条主链,不要在业务组件里重新扩散 `Blob + a.download` 式浏览器旁路。
命令目录与输入补全链路同样需要单一事实源。当前前端主入口为 `src/lib/api/skillCatalog.ts`,统一承接:
- `bootstrap.skillCatalog`
- `GET /v1/public/tenants/{tenantId}/client/skills`
- 本地 seeded `SkillCatalog`
当前目录协议固定收敛到 `SkillCatalog.entries`:
- `entries.kind=command` 用于 `@` 原子命令
- `entries.kind=scene` 用于产品型 `/` 场景命令
- `entries.kind=skill` 用于首页与技能入口
固定约束:
- `CharacterMention`、`builtinCommands`、场景 slash 补全不得再各自维护一套业务命令静态常量
- 服务端尚未返回 `entries` 时,允许网关层从 legacy `items` 兼容投影出 `entries`
- 客户端必须保留 seeded fallback,不能因为服务端暂时不可用就让 `@配图`、`@转写` 这类主链入口失能
- `src/components/agent/chat/commands/catalog.ts` 只继续承接 Lime 本地 / Codex 原生命令;产品型 `/` 场景不应再长期硬编码在这里
- 若服务端下发的 `renderContract` 超出 Lime 当前支持范围,优先由服务端回退到已支持类型,客户端也必须退化到通用 timeline / artifact 展示
当前 `/scene-key` 的发送主链也已经固定:
- 发送前由 `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 统一拦截 slash 场景
- 再委托 `src/components/agent/chat/workspace/useWorkspaceServiceSkillEntryActions.ts` 的 `handleRuntimeSceneLaunch(...)`
- 运行时只从统一 catalog 解析 `scene -> linkedSkillId -> ServiceSkillHomeItem`
- 对 `cloud_scene`,优先复用现有 `createServiceSkillRun(...)` 云端运行链
- 若云端 run 在创建前就失败,客户端必须自动回退到本地工作区 prompt 主链,不能把 slash scene 直接判死
- 未命中统一 scene 目录的 slash 文本必须继续回到普通 slash / Codex 命令流,不能误报本地 Skill 不存在
如果这轮改动触达了 `client/skills` 协议,不仅要改 Lime 前端 selector,还要同步检查 `limecore` 的:
- OpenAPI source fragments
- `packages/types`
- `packages/api-client`
- `control-plane-svc` skill catalog service 与路由测试
媒体生成任务链路同样需要单一事实源。当前对外公开契约应优先收敛到 `lime media ... generate --json` 这条 CLI 主链,至少覆盖:
- `lime media image generate`
+1 -2
View File
@@ -10,14 +10,13 @@
src/lib/
├── api/ # API 封装
│ ├── apiKeyProvider.ts
│ └── pluginUI.ts
│ └── pluginUI.ts # 插件元数据 API
├── config/ # 配置
│ └── providers.ts
├── types/ # 类型定义
│ └── provider.ts
├── errors/ # 错误处理
│ └── playwrightErrors.ts
├── plugin-ui/ # 插件 UI 系统
├── tauri/ # Tauri 命令封装
├── utils/ # 工具函数
├── flowEventManager.ts # 流量事件管理
@@ -15,6 +15,7 @@
- 用户中心、个人资料、会话同步
- AI 服务商页、云端 Provider、默认来源、模型目录
- `client/bootstrap`、`client/session`、`client/profile`
- `client/skills`、`skillCatalog.entries`、`client/service-skills`
- Gateway、Scene、Service Skill 云配置同步
- 任何“客户端要不要本地维护一份服务端数据”的判断
@@ -24,6 +25,7 @@
- 认证与会话
- 客户端 bootstrap
- `client/skills` 统一命令目录
- 用户资料与账户能力
- Provider Offer / 服务目录 / Scene Catalog
- Gateway 与云端运行时策略
@@ -45,5 +47,8 @@
- 服务端已有接口时,优先补客户端接线
- 云事实源不要在客户端长期维护第二份
- `@` / 产品型 `/` 的统一目录优先看 `client/skills.entries`
- Lime 客户端必须保留 seeded / fallback 韧性兜底,不能只靠服务端在线返回
- 能走运行时配置和 `bootstrap.features` 的,不要写死在前端
- 用户界面不要直接暴露 “OEM” 技术概念
- `scene` 目录项要稳定提供 `sceneKey` 与 `linkedSkillId`。Lime 当前会用它把 `/scene-key` 解析到现有 `ServiceSkill` 启动链;若云端 run 创建前失败,客户端会自动回退到本地工作区 prompt 主链
+1
View File
@@ -30,6 +30,7 @@
- 如果 companion 协议新增了 provider 摘要、桌宠回跳设置、桌宠主动请求同步,或双击 / 三击 / 文本对话触发的桌宠 LLM 交互事件,Playwright 续测只覆盖 Lime 主仓内的“状态事件是否触发”“是否跳到 `设置 -> AI 服务商`”“是否重发脱敏摘要”“是否调用宿主侧 LLM 代理逻辑”和“主窗口是否被唤起”,不在 WebView 层尝试直接操控原生桌宠 UI
- 共享网关控制页已下线,托盘也不再展示网关状态或地址;共享网关 `/v1/routes` 与 selector HTTP 路由也已下线,不再对“启动/停止网关、复制网关地址、路由/curl 示例、selector 路由、托盘运行态文案”做 GUI 续测;server 验证只关注标准 `/v1/messages` 与 `/v1/chat/completions` 主链,如需看运行时状态,走开发者页或实验页的诊断面板
- 旧设置页里的“安全与性能 / 容错配置”已经下线,不再对这些页签、表单或命令写入路径做 GUI 续测;如果还要验证提示路由,只围绕当前输入框 `get_hint_routes` 读取链与提示展示,不再寻找旧设置页入口
- 初装引导里的旧插件选择 / 插件安装 / 配置切换链路已经下线,不再对 `config-switch` 推荐安装、Provider Switch 页面或相关命令做 GUI 续测;当前 onboarding 只围绕现役语音体验流程验证
- 项目排版模板与品牌人设扩展旧链路已下线,不再对相关弹窗、模板列表、默认模板、人设扩展表单做 GUI 续测;项目与工作台回归只围绕当前 `Claw` / `workspace` / 现役 `persona` 主链
- 如果只是模块级代码修改、并不需要真实页面交互,优先跑最小单测或 `verify:local`
+2
View File
@@ -78,6 +78,8 @@
如果本轮是在清退旧设置页的“安全与性能 / 容错配置”命令面,`get_retry_config`、`update_retry_config`、`get_failover_config`、`update_failover_config`、`get_switch_log`、`clear_switch_log`、`get_rate_limit_config`、`update_rate_limit_config`、`get_conversation_config`、`update_conversation_config`、`update_hint_routes`、`get_pairing_config`、`update_pairing_config` 也必须同步从前端网关、Rust 注册和默认 mock 中撤掉;若当前输入框提示仍依赖 `get_hint_routes`,则只保留该只读读取面。最低校验至少包含 `npm run test:contracts` 与 `npm run governance:legacy-report`。
如果本轮是在清退旧 onboarding 插件安装流或 Provider Switch 命令面,`get_switch_providers`、`get_current_switch_provider`、`add_switch_provider`、`update_switch_provider`、`delete_switch_provider`、`switch_provider`、`import_default_config`、`read_live_provider_settings`、`check_config_sync_status`、`sync_from_external_config` 也必须同步从前端常量、Rust 注册、services、默认 mock 与 GUI 入口中撤掉;当前 onboarding 只允许保留语音体验链,不再保留 `config-switch` 推荐安装面。最低校验至少包含 `npm run test:contracts` 与 `npm run governance:legacy-report`。
如果本轮涉及 `companion_*` 桌宠命令族,还要同步检查本地 companion `WebSocket` 入口、前端 `src/lib/api/companion.ts` 网关、Rust 注册、治理目录册以及浏览器模式 mock 返回形态;浏览器模式下这组命令默认也要保持可 mock,不要让桌宠接入把默认页面渲染链路卡死。
如果本轮涉及 team runtime 工具面或主线程用户消息工具,还要同步检查 Rust catalog / inventory、runtime 注册、浏览器 fallback mock 与前端 tool display;`Agent / TeamCreate / TeamDelete / SendMessage / ListPeers` 必须保持同一组 current surface,`SendUserMessage` 也必须继续停留在 current 主线程工具面,`SubAgentTask` 只能继续停留在 compat 读取边界。
+39 -10
View File
@@ -441,24 +441,44 @@ Lime 技能能力必须明确区分三个对象:
客户端现状:
- 本地 seeded skill catalog
- `bootstrap.serviceSkillCatalog`
- `client/service-skills`
- 本地 seeded `SkillCatalog`
- `bootstrap.skillCatalog`
- `client/skills`
- `client/service-skills` compat 投影
- `siteAdapterCatalog`
服务端现状:
- `control-plane-svc` 负责客户端技能目录聚合
- `control-plane-svc` 负责客户端统一技能目录聚合
- Tool Hub 方向负责 tool / adapter 工件真相源
### 长期收敛方向
长期收敛规则固定如下:
1. 统一 skill 目录收敛到 `client/skills`
2. 兼容期保留 `client/service-skills`
3. adapter / tool 工件目录继续独立,不与 skill 目录混用
4. bootstrap 与独立刷新必须消费同一份目录协议
1. 统一技能目录继续收敛到 `client/skills`
2. `bootstrap.skillCatalog` 与独立刷新必须消费同一份目录协议
3. `SkillCatalog.entries` 必须承载三类统一目录项:
- `skill`
- `command`
- `scene`
4. `client/service-skills` 只允许作为 compat 投影继续保留,不再承接新的目录标准定义
5. adapter / tool 工件目录继续独立,不与 skill 目录混用
### 统一目录补充分层
`SkillCatalog.entries` 是分发层的 current 投影,但它不改变 skill 的产品与执行边界:
- `skill` 目录项回答“这是一个什么业务能力”
- `command` 目录项回答“用户可以通过哪个 `@` 原子入口触发它”
- `scene` 目录项回答“用户可以通过哪个 `/` 场景把多个能力编排起来”
固定约束:
- `/` 场景不是新的客户端硬编码系统,而是统一目录中的 `scene`
- `@` 命令不是独立于 skill 的第二套协议,而是统一目录中的 `command`
- `command` / `scene` 可以绑定到 skill、CLI、服务端 API 或 hybrid executor,但绑定规则仍属于运行时层
- Lime 客户端必须保留 seeded catalog 作为 offline / degrade 兜底,不能只依赖服务端在线目录
## 外部 `SKILL.md` 参考边界
@@ -493,13 +513,22 @@ Lime 技能能力必须明确区分三个对象:
## 当前主链
在统一 `client/skills` 正式落地前,当前新增能力的主链固定如下:
当前新增能力的主链固定如下:
- 业务技能目录:继续收敛到 `ServiceSkillCatalog`
- 在线目录:继续收敛到 `client/skills` 与 `bootstrap.skillCatalog`
- 本地兜底:继续收敛到 seeded `SkillCatalog`
- compat 投影:仅在迁移期保留 `client/service-skills`
- 标准摘要层:继续收敛到 `skillBundle`
- 站点工件目录:继续收敛到 `siteAdapterCatalog`
- 业务 skill 引用站点能力:通过 `siteCapabilityBinding.adapterName`
对输入面板和启动入口再补一条固定约束:
- `entries.kind=command` 驱动 `@`
- `entries.kind=scene` 驱动产品型 `/`
- `entries.kind=skill` 驱动首页技能入口与技能中心
- 服务端未返回 `entries` 时,客户端允许由 compat `items` 投影构造,但不得继续在组件层手写平行常量
不要在这个阶段再引入:
- 平级 `skill.json` 目录协议
-462
View File
@@ -1,462 +0,0 @@
# Lime Plugin UI 系统设计
## 概述
借鉴 A2UI 的设计理念,为 Lime 设计一套声明式的插件 UI 系统。核心思想是:
- **安全如数据,表达如代码**:插件只能声明 UI 结构,不能执行任意代码
- **声明式 JSON 格式**:插件通过 JSON 描述 UI 意图,宿主应用负责渲染
- **组件目录(Catalog)机制**:预定义可用组件集,插件只能使用目录中的组件
- **数据绑定分离**:UI 结构与数据模型分离,支持增量更新
## 架构设计
```
┌─────────────────────────────────────────────────────────────────┐
│ Lime Host │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Plugin UI Renderer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Component │ │ Data │ │ Event │ │ │
│ │ │ Registry │ │ Store │ │ Handler │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ JSON Messages │
│ ┌───────────────────────────┼─────────────────────────────┐ │
│ │ Plugin Bridge │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Tauri │ │ Schema │ │ Message │ │ │
│ │ │ IPC │ │ Validator │ │ Router │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│
┌───────────────┴───────────────┐
│ Plugin (Rust) │
│ ┌─────────────────────────┐ │
│ │ UI Declaration API │ │
│ │ - surface_update() │ │
│ │ - data_update() │ │
│ │ - begin_rendering() │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘
```
## 核心概念
### 1. Surface(渲染表面)
每个插件可以拥有一个或多个 Surface,代表独立的 UI 区域:
```typescript
interface Surface {
surfaceId: string; // 唯一标识
pluginId: string; // 所属插件
rootComponentId: string; // 根组件 ID
components: Map<string, Component>; // 组件缓冲区
dataModel: Record<string, any>; // 数据模型
styles?: SurfaceStyles; // 样式配置
}
```
### 2. Component Catalog(组件目录)
预定义的安全组件集,插件只能使用这些组件:
```typescript
// 标准组件目录
const StandardCatalog = {
// 布局组件
Row: { children: 'ComponentRef[]', gap?: 'number', align?: 'Alignment' },
Column: { children: 'ComponentRef[]', gap?: 'number', align?: 'Alignment' },
Card: { child: 'ComponentRef', title?: 'BoundValue<string>' },
Tabs: { items: 'TabItem[]' },
// 展示组件
Text: { text: 'BoundValue<string>', variant?: 'TextVariant' },
Icon: { name: 'IconName', size?: 'number', color?: 'string' },
Badge: { text: 'BoundValue<string>', variant?: 'BadgeVariant' },
Progress: { value: 'BoundValue<number>', max?: 'number' },
// 输入组件
Button: { child: 'ComponentRef', action: 'Action', variant?: 'ButtonVariant' },
TextField: { label: 'BoundValue<string>', value: 'BoundValue<string>' },
Switch: { label: 'BoundValue<string>', checked: 'BoundValue<boolean>' },
Select: { options: 'SelectOption[]', value: 'BoundValue<string>' },
// 数据展示
Table: { columns: 'TableColumn[]', data: 'BoundValue<any[]>' },
List: { children: 'ChildrenDef', direction?: 'Direction' },
KeyValue: { items: 'KeyValueItem[]' },
// 反馈组件
Alert: { message: 'BoundValue<string>', type: 'AlertType' },
Spinner: { size?: 'number' },
Empty: { description?: 'BoundValue<string>' },
};
```
### 3. 消息协议
#### Server → Client 消息
```typescript
// 组件更新
interface SurfaceUpdate {
surfaceId: string;
components: ComponentDef[];
}
// 数据更新
interface DataModelUpdate {
surfaceId: string;
path?: string; // JSONPath,如 '/credentials/0/status'
contents: DataEntry[];
}
// 开始渲染
interface BeginRendering {
surfaceId: string;
root: string; // 根组件 ID
catalogId?: string;
styles?: SurfaceStyles;
}
// 删除 Surface
interface DeleteSurface {
surfaceId: string;
}
```
#### Client → Server 消息
```typescript
// 用户操作
interface UserAction {
name: string; // 操作名称
surfaceId: string;
sourceComponentId: string;
context: Record<string, any>; // 解析后的上下文数据
timestamp: string;
}
```
### 4. 数据绑定
支持字面值和路径绑定:
```typescript
type BoundValue<T> =
| { literal: T } // 字面值
| { path: string } // 数据路径
| { literal: T; path: string }; // 初始化 + 绑定
// 示例
const textComponent = {
id: 'status-text',
component: {
Text: {
text: { path: '/credential/status' }, // 绑定到数据模型
variant: 'body'
}
}
};
```
## 实现方案
### 前端:React Renderer
```
src/lib/plugin-ui/
├── index.ts # 导出入口
├── types.ts # 类型定义
├── PluginUIRenderer.tsx # 主渲染器组件
├── PluginSurface.tsx # Surface 容器
├── ComponentRegistry.ts # 组件注册表
├── DataStore.ts # 数据存储
├── MessageHandler.ts # 消息处理
└── components/ # 标准组件实现
├── layout/
│ ├── Row.tsx
│ ├── Column.tsx
│ ├── Card.tsx
│ └── Tabs.tsx
├── display/
│ ├── Text.tsx
│ ├── Icon.tsx
│ ├── Badge.tsx
│ └── Progress.tsx
├── input/
│ ├── Button.tsx
│ ├── TextField.tsx
│ ├── Switch.tsx
│ └── Select.tsx
└── data/
├── Table.tsx
├── List.tsx
└── KeyValue.tsx
```
### 后端:Rust Plugin API
```rust
// src-tauri/src/plugins/ui_api.rs
/// 插件 UI 声明 API
pub trait PluginUI {
/// 获取插件的 Surface 定义
fn get_surfaces(&self) -> Vec<SurfaceDefinition>;
/// 处理用户操作
fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>>;
}
/// UI 消息类型
pub enum UIMessage {
SurfaceUpdate(SurfaceUpdate),
DataModelUpdate(DataModelUpdate),
BeginRendering(BeginRendering),
DeleteSurface(DeleteSurface),
}
/// Surface 定义
pub struct SurfaceDefinition {
pub surface_id: String,
pub initial_components: Vec<ComponentDef>,
pub initial_data: serde_json::Value,
pub root_id: String,
}
```
## 使用示例
### 插件端(Rust)
```rust
impl PluginUI for CredentialMonitorPlugin {
fn get_surfaces(&self) -> Vec<SurfaceDefinition> {
vec![SurfaceDefinition {
surface_id: "credential-monitor".into(),
root_id: "root".into(),
initial_components: vec![
component!("root", Column {
children: explicit_list!["header", "credential-list"],
gap: 16
}),
component!("header", Row {
children: explicit_list!["title", "refresh-btn"],
align: "spaceBetween"
}),
component!("title", Text {
text: literal!("凭证监控"),
variant: "h3"
}),
component!("refresh-btn", Button {
child: "refresh-icon",
action: action!("refresh")
}),
component!("refresh-icon", Icon { name: "refresh" }),
component!("credential-list", List {
children: template!("credential-item", "/credentials"),
direction: "vertical"
}),
// 模板组件
component!("credential-item", Card {
child: "item-content"
}),
component!("item-content", Row {
children: explicit_list!["item-name", "item-status"]
}),
component!("item-name", Text {
text: path!("name") // 相对路径,从列表项数据解析
}),
component!("item-status", Badge {
text: path!("status"),
variant: path!("statusVariant")
}),
],
initial_data: json!({
"credentials": []
}),
}]
}
fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>> {
match action.name.as_str() {
"refresh" => {
let credentials = self.fetch_credentials()?;
Ok(vec![UIMessage::DataModelUpdate(DataModelUpdate {
surface_id: "credential-monitor".into(),
path: Some("/credentials".into()),
contents: credentials.into_data_entries(),
})])
}
_ => Ok(vec![])
}
}
}
```
### 宿主端(React)
```tsx
// 在插件详情页使用
function PluginDetailPage({ pluginId }: { pluginId: string }) {
return (
<div className="plugin-detail">
<PluginInfo pluginId={pluginId} />
{/* 插件 UI 渲染区域 */}
<PluginUIRenderer
pluginId={pluginId}
onAction={(action) => invoke('plugin_handle_action', { pluginId, action })}
/>
</div>
);
}
```
## 安全考虑
1. **组件白名单**:只允许使用预定义的组件类型
2. **Schema 验证**:所有消息必须通过 JSON Schema 验证
3. **沙箱隔离**:每个插件的 Surface 相互隔离
4. **Action 审计**:记录所有用户操作,支持权限控制
5. **资源限制**:限制组件数量、数据大小等
## 扩展机制
### 自定义组件注册
允许宿主应用注册额外的组件:
```typescript
// 注册自定义组件
componentRegistry.register('CredentialCard', CredentialCardComponent, {
schema: {
credential: { type: 'object', required: true },
onRefresh: { type: 'action' }
}
});
```
### 主题支持
通过 Surface styles 支持主题定制:
```typescript
interface SurfaceStyles {
primaryColor?: string;
font?: string;
borderRadius?: number;
// ... 更多样式属性
}
```
## 迁移路径
1. **Phase 1**:实现核心渲染器和基础组件
2. **Phase 2**:添加数据绑定和事件处理
3. **Phase 3**:迁移现有插件 UI 到新系统
4. **Phase 4**:支持自定义组件扩展
## 与 A2UI 的差异
| 特性 | A2UI | Lime Plugin UI |
|------|------|---------------------|
| 传输方式 | SSE/JSONL 流 | Tauri IPC |
| 渲染框架 | Lit/Angular/Flutter | React |
| 组件风格 | Material Design | TailwindCSS/shadcn |
| 数据更新 | 增量流式 | 批量更新 |
| 使用场景 | 跨平台 Agent UI | 桌面应用插件 |
## 实时更新:Tauri 事件推送
插件可以通过 Tauri 事件系统向前端推送 UI 更新,实现实时数据刷新。
### 事件发射器
```rust
use crate::plugin::{PluginUIEmitter, UIMessage, DataModelUpdate, DataEntry};
// 在 Tauri 命令或服务中使用
fn update_plugin_ui(emitter: &PluginUIEmitter, plugin_id: &str) {
// 发送数据更新
let update = DataModelUpdate {
surface_id: "my-surface".into(),
path: Some("/stats".into()),
contents: vec![
DataEntry::number("count", 42.0),
DataEntry::string("status", "healthy"),
],
};
emitter.emit_data_update(plugin_id, update).unwrap();
}
```
### 前端监听
前端通过 `usePluginUI` Hook 自动监听 `plugin-ui-message` 事件:
```typescript
// 自动处理,无需手动监听
const { surfaces, handleAction } = usePluginUI({ pluginId: 'my-plugin' });
```
### 事件载荷格式
```typescript
interface PluginUIEventPayload {
pluginId: string;
message: UIMessage; // SurfaceUpdate | DataModelUpdate | BeginRendering | DeleteSurface
}
```
## 示例插件:凭证监控
完整示例见 `src-tauri/src/plugin/examples/credential_monitor.rs`:
```rust
use crate::plugin::{PluginUI, SurfaceDefinition, ComponentDef, ChildrenDef, BoundValue};
struct CredentialMonitorPlugin { /* ... */ }
impl PluginUI for CredentialMonitorPlugin {
fn get_surfaces(&self) -> Vec<SurfaceDefinition> {
vec![SurfaceDefinition {
surface_id: "credential-monitor".into(),
root_id: "root".into(),
initial_components: vec![
ComponentDef::column("root", ChildrenDef::explicit(vec!["header", "list"])),
ComponentDef::text_literal("header", "凭证监控"),
ComponentDef::list("list", ChildrenDef::template("item", "/credentials")),
// ... 更多组件
],
initial_data: json!({ "credentials": [] }),
styles: None,
}]
}
async fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>, PluginError> {
match action.name.as_str() {
"refresh" => {
// 返回数据更新消息
Ok(vec![UIMessage::DataModelUpdate(/* ... */)])
}
_ => Ok(vec![])
}
}
}
```
## 下一步计划
1. **更多组件**:Table、Tabs、Modal 等复杂组件
2. **表单验证**:支持 TextField 的验证规则
3. **主题系统**:更完善的样式定制能力
4. **插件市场**:支持从远程加载插件 UI 定义