mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
15 KiB
15 KiB
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 区域:
interface Surface {
surfaceId: string; // 唯一标识
pluginId: string; // 所属插件
rootComponentId: string; // 根组件 ID
components: Map<string, Component>; // 组件缓冲区
dataModel: Record<string, any>; // 数据模型
styles?: SurfaceStyles; // 样式配置
}
2. Component Catalog(组件目录)
预定义的安全组件集,插件只能使用这些组件:
// 标准组件目录
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 消息
// 组件更新
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 消息
// 用户操作
interface UserAction {
name: string; // 操作名称
surfaceId: string;
sourceComponentId: string;
context: Record<string, any>; // 解析后的上下文数据
timestamp: string;
}
4. 数据绑定
支持字面值和路径绑定:
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
// 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)
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)
// 在插件详情页使用
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>
);
}
安全考虑
- 组件白名单:只允许使用预定义的组件类型
- Schema 验证:所有消息必须通过 JSON Schema 验证
- 沙箱隔离:每个插件的 Surface 相互隔离
- Action 审计:记录所有用户操作,支持权限控制
- 资源限制:限制组件数量、数据大小等
扩展机制
自定义组件注册
允许宿主应用注册额外的组件:
// 注册自定义组件
componentRegistry.register('CredentialCard', CredentialCardComponent, {
schema: {
credential: { type: 'object', required: true },
onRefresh: { type: 'action' }
}
});
主题支持
通过 Surface styles 支持主题定制:
interface SurfaceStyles {
primaryColor?: string;
font?: string;
borderRadius?: number;
// ... 更多样式属性
}
迁移路径
- Phase 1:实现核心渲染器和基础组件
- Phase 2:添加数据绑定和事件处理
- Phase 3:迁移现有插件 UI 到新系统
- Phase 4:支持自定义组件扩展
与 A2UI 的差异
| 特性 | A2UI | Lime Plugin UI |
|---|---|---|
| 传输方式 | SSE/JSONL 流 | Tauri IPC |
| 渲染框架 | Lit/Angular/Flutter | React |
| 组件风格 | Material Design | TailwindCSS/shadcn |
| 数据更新 | 增量流式 | 批量更新 |
| 使用场景 | 跨平台 Agent UI | 桌面应用插件 |
实时更新:Tauri 事件推送
插件可以通过 Tauri 事件系统向前端推送 UI 更新,实现实时数据刷新。
事件发射器
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 事件:
// 自动处理,无需手动监听
const { surfaces, handleAction } = usePluginUI({ pluginId: 'my-plugin' });
事件载荷格式
interface PluginUIEventPayload {
pluginId: string;
message: UIMessage; // SurfaceUpdate | DataModelUpdate | BeginRendering | DeleteSurface
}
示例插件:凭证监控
完整示例见 src-tauri/src/plugin/examples/credential_monitor.rs:
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![])
}
}
}
下一步计划
- 更多组件:Table、Tabs、Modal 等复杂组件
- 表单验证:支持 TextField 的验证规则
- 主题系统:更完善的样式定制能力
- 插件市场:支持从远程加载插件 UI 定义