mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
co-authored by
Claude Sonnet 4.6
parent
e4b93c38d8
commit
c2261961c1
@@ -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 / 恢复 / 重试 / 取消怎么做
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 主链
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 读取边界。
|
||||
|
||||
@@ -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` 目录协议
|
||||
|
||||
@@ -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 定义
|
||||
Reference in New Issue
Block a user