Ooder A2UI 核心架构深度解析:WEB 拦截层的设计与实现(CLI 与前端融合的自主 UI 桥接层)
副标题:从 CLI 到增强型 UI:A2UI 构建自主 UI 体系与 CLI 的 Bridger 层核心逻辑
作者: Ooder 架构团队
发布日期: 2026 年 4 月 30 日
技术栈: Java + Spring + SPI + 注解驱动
目录
- 开篇:A2UI 的价值定位与设计背景
- 架构演进背景
- 核心架构设计
- 注解层设计
- SPI 扩展机制
- 技能实现层
- 拦截器重构
- 服务层架构
- 项目整合桥接
- RESTful API 设计
- 最佳实践
附录. 关键术语表
0. 开篇:A2UI 的价值定位与设计背景
随着 CLI 命令行技术在研发、运维场景中的深度应用,单纯的命令行交互已无法满足复杂场景下的可视化、易用性需求 —— 这也催生了对「增强型 UI 体系」的核心诉求。A2UI(Augmented & Autonomous UI)的诞生,正是为了填补这一空白:它既是 CLI 命令行效率的延伸,也是 WEB 前端技术的重要发展方向 —— 前端技术正从单纯的页面渲染,走向「命令行级效率 + 可视化交互 + 自主化能力」的融合形态。
Ooder A2UI 的核心目标,是建立一套完全基于自主 UI 技术体系、能够与 CLI 层无缝衔接的 Bridger(桥接)层,打通命令行操作的高效性与 UI 交互的易用性之间的壁垒。本文聚焦于 Ooder A2UI 核心架构中 WEB 拦截层的设计实现,这是整个 A2UI 体系中承接 CLI 桥接能力、驱动 UI 技能扩展的核心环节,也是实现「CLI 指令 - UI 交互 - 自主化处理」闭环的关键载体。
0.1 CLI 命令行的局限性与增强型 UI 的刚需
在软件开发的历史长河中,命令行界面(CLI)曾是开发者与系统交互的唯一方式。虽然 CLI 以其高效、灵活的特点深受技术人员的喜爱,但随着软件系统复杂度的指数级增长,纯文本的交互方式逐渐显露出其局限性:
- 学习曲线陡峭:需要记忆大量命令和参数,新手上手成本高
- 可视化能力弱:难以直观展示复杂的数据结构和关系,调试困难
- 操作效率受限:对于非技术用户,CLI 几乎是不可逾越的鸿沟
- 协作成本高:团队间难以共享操作流程,知识传承困难
- 实时反馈差:无法实时预览操作结果,错误发现滞后
正是在这样的背景下,A2UI(Augmented & Autonomous UI,增强型自主 UI) 应运而生。A2UI 不仅仅是传统 GUI 的升级版,它代表了人机交互的下一个范式:在保持 CLI 高效性的同时,通过智能化的 UI 组件提供更直观、更强大的交互体验。
0.2 A2UI 的核心意义:WEB 前端技术的演进方向
Ooder A2UI 的设计理念代表了 WEB 前端技术的重要演进方向,其核心意义体现在三个层面:
1. 完全自主的 UI 基础设施
传统的 UI 框架往往依赖于第三方组件库,导致定制化困难、版本依赖复杂。Ooder A2UI 从底层开始构建,实现了:
- 零外部依赖:核心组件完全自主研发,不受第三方库制约
- 高度可定制:每个组件都可以根据业务需求深度定制
- 版本控制友好:组件与业务逻辑解耦,升级无负担
- 技术自主可控:掌握核心技术栈,应对技术变革更有底气
2. CLI 与 UI 的无缝桥接
核心创新
这是 Ooder A2UI 最具创新性的设计目标。通过建立一层智能的 Bridger 层,实现:
┌─────────────────────────────────────────────────────────────┐
│ CLI ↔ A2UI Bridger 层 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CLI 命令 ────▶ Bridger 解析 ────▶ UI 组件自动生成 │
│ │
│ UI 交互 ────▶ Bridger 转换 ────▶ CLI 命令执行 │
│ │
│ 双向同步:CLI 操作实时反映到 UI,UI 操作可回溯为 CLI │
│ │
└─────────────────────────────────────────────────────────────┘
图 0-1:CLI ↔ A2UI Bridger 层架构
这种设计使得:
- 开发者可以继续使用熟悉的 CLI 工作流,同时享受 UI 的可视化优势
- 运维人员可以通过 UI 界面操作,系统自动生成对应的 CLI 脚本供审计和复用
- 产品经理和业务人员可以通过直观的 UI 界面参与系统配置,降低沟通成本
- 团队协作更加顺畅,CLI 操作与 UI 操作可以无缝切换和追溯
3. WEB 技术的前沿探索
Ooder A2UI 紧跟现代 WEB 技术的发展趋势,融合了多项前沿技术:
- 组件化架构:借鉴 React、Vue 等现代框架的设计理念,实现高内聚低耦合
- 声明式 UI:通过注解和配置定义 UI,而非命令式编程,提高开发效率
- 响应式设计:一套代码适配多种设备和屏幕尺寸,降低维护成本
- 渐进式增强:从基础功能到高级特性,按需加载,优化性能
- 技能驱动:基于 SPI 机制,将 CLI 指令/UI 操作转化为可扩展技能
0.3 Ooder A2UI 的核心目标:自主 UI 与 CLI 的 Bridger 层构建
Ooder A2UI 的核心目标是建立一套完全自主的 UI 技术体系,并通过 Bridger(桥接)层实现与 CLI 的无缝衔接。这个目标包含三个关键维度:
维度一:自主 UI 技术体系
- 底层自主:从组件定义、渲染引擎到状态管理,完全自主研发
- 技能扩展:通过 Skills 架构,支持 UI 组件的动态扩展和组合
- 注解驱动:使用 Java 注解声明式定义 UI 元素和行为
- SPI 扩展:通过 Java SPI 机制,支持第三方技能的无缝集成
维度二:CLI 桥接能力
- 指令映射:将 CLI 指令映射为 UI 操作,实现可视化执行
- 双向转换:UI 操作可转换为 CLI 脚本,CLI 命令可生成 UI 界面
- 状态同步:CLI 状态与 UI 状态实时同步,保持一致性
- 历史追溯:所有操作可追溯,支持回滚和审计
维度三:WEB 拦截层核心
WEB 拦截层是 Ooder A2UI 架构中最关键的一环,承担着以下核心职责:
- 请求路由与分发:智能识别 CLI 指令和 HTTP 请求,分发到对应的处理器
- 技能驱动:基于 Skills 架构,实现 UI 组件的动态加载和生成
- SPI 扩展:通过 Java SPI 机制,支持第三方技能的无缝集成
- 性能优化:缓存、懒加载、并发控制等多重优化策略
- CLI/UI 双端适配:统一处理 CLI 和 UI 的请求,提供一致的交互体验
0.4 本文核心:WEB 拦截层在 A2UI 架构中的定位与价值
本文聚焦于 Ooder A2UI 架构中最关键的一环 —— WEB 拦截层的设计与实现。作为连接 CLI 指令、HTTP 请求与后端服务的桥梁,WEB 拦截层在整个 A2UI 体系中具有核心地位:
在架构中的定位
┌─────────────────────────────────────────────────────────────┐
│ Ooder A2UI 整体架构 │
├─────────────────────────────────────────────────────────────┤
│ CLI 层 ────▶ Bridger 层 ────▶ WEB 拦截层 ────▶ 服务层 │
│ ↑ │
│ 【本文核心】 │
│ ↓ │
│ 技能驱动层 │
└─────────────────────────────────────────────────────────────┘
图 0-2:WEB 拦截层在 A2UI 架构中的定位
核心价值
- 承接 CLI 桥接能力:将 CLI 指令转换为 HTTP 请求,实现 CLI 与 WEB 的统一处理
- 驱动 UI 技能扩展:基于 Skills 架构,动态加载和生成 UI 组件
- 实现请求智能路由:根据请求类型(CLI/UI/API)智能分发到不同处理器
- 保障系统性能:通过缓存、懒加载等策略,确保高并发场景下的系统稳定性
- 支持技术演进:基于 SPI 机制,支持第三方技能的无缝集成,保持架构的可扩展性
本文将深入解析
通过本文的深度解析,您将全面了解:
- 架构演进:如何从传统的硬编码拦截器演进到 Skills + SPI 架构
- 注解驱动:注解驱动开发如何简化组件定义和 CLI 指令映射
- SPI 扩展:SPI 机制如何实现技能的热插拔和 CLI 适配
- 拦截器重构:如何设计支持 CLI/UI 双端请求的统一拦截器
- 服务层协作:服务层如何协作完成 CLI 指令到 UI 生成的完整流程
- API 设计:如何设计和实现支持 CLI 调用的 RESTful API
- 最佳实践:开发自定义技能、性能优化、调试技巧等实战经验
0.5 适用读者
本文适合以下读者:
- 架构师:了解现代 WEB 应用的架构设计思路,特别是 CLI 与 UI 融合的架构模式
- 后端开发者:学习 Java 注解、SPI、拦截器等高级特性,以及 CLI 适配技巧
- 前端开发者:理解后端 API 设计,更好地进行前后端协作,实现 CLI 驱动的 UI
- 运维工程师:了解如何通过 UI 操作自动生成 CLI 脚本,提高运维效率
- 技术管理者:评估技术方案的可行性和扩展性,把握前端技术演进方向
1. 架构演进背景
1.1 传统拦截机制的痛点(CLI/UI 割裂问题)
在 Ooder 框架的早期版本中,我们采用了基于硬编码的拦截器模式来处理特定后缀的请求(如 .jsx、.cls、.dyn 等)。这种设计存在以下问题,特别是在 CLI 与 UI 的协作方面:
┌─────────────────────────────────────────────────────────┐
│ 传统拦截架构 │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ JSXInterceptor│ │CLSInterceptor│ │DYNInterceptor│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ BaseInterceptor │ │
│ └─────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Module 转换 │ │ 反射执行 │ │ 模板渲染 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 【问题】CLI 指令与 UI 请求被割裂处理,无法统一桥接 │
└─────────────────────────────────────────────────────────┘
核心问题:
- 耦合度高:拦截逻辑与具体的组件类型硬编码绑定,难以扩展
- 扩展困难:新增组件类型需要修改拦截器核心代码,违反开闭原则
- 维护成本高:多个拦截器类职责不清晰,代码重复严重
- 缺乏统一管理:组件注册和发现机制分散,难以统一维护
- CLI/UI 割裂:
关键问题:CLI 指令和 UI 请求被分别处理,无法实现双向桥接
- CLI 命令无法自动生成对应的 UI 界面
- UI 操作无法回溯为 CLI 脚本
- 两套独立的处理逻辑,维护成本翻倍
1.2 Skills + SPI 架构的优势(桥接层能力支撑)
为了解决上述问题,特别是 CLI/UI 割裂的问题,我们引入了 Skills + SPI 的架构设计:
┌─────────────────────────────────────────────────────────┐
│ Skills + SPI 拦截架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SkillDrivenInterceptor (统一入口) │ │
│ │ 【支持 CLI 指令 + UI 请求的统一处理】 │ │
│ └────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ ┌──────▼─────────┐ ┌──────▼─────────┐ │
│ │ A2uiSkillRegistrySPI │ │ResourceResolver│ │
│ │ (技能注册中心) │ │ (资源解析器) │ │
│ │ 【CLI 指令映射】 │ │【CLI/UI 双端】 │ │
│ └──────┬─────────┘ └────────────────┘ │
│ │ │
│ ┌──────▼─────────────────────────────────┐ │
│ │ Skill 实现层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │
│ │ │TreeSkill │ │ FormSkill│ │Chart...│ │ │
│ │ └──────────┘ └──────────┘ └────────┘ │ │
│ │ 【每个技能支持 CLI 指令和 UI 操作】 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 【优势】通过 Bridger 层实现 CLI ↔ UI 的双向桥接 │
└─────────────────────────────────────────────────────────┘
核心优势:
- 解耦:拦截逻辑与具体组件实现完全分离,易于维护
- 可扩展:通过 SPI 机制动态加载新技能,无需修改核心代码
- 统一管理:所有技能通过注册中心集中管理,便于监控和维护
- 注解驱动:使用注解声明式定义技能元数据,简化开发
- CLI/UI 统一:
核心优势:实现 CLI 指令与 UI 操作的统一处理
- CLI 命令自动映射为技能调用,生成对应 UI 界面
- UI 操作可转换为 CLI 脚本,支持审计和复用
- 一套技能逻辑,同时支持 CLI 和 UI 两种交互方式
- Bridger 层负责双向转换,业务层无需关心交互方式
2. 核心架构设计
2.1 整体架构图(六层架构,标注 Bridger 层位置)
┌─────────────────────────────────────────────────────────────────┐
│ Ooder A2UI 整体架构 │
│ (标注 Bridger 层核心位置) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Web 层 (Controller) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ SkillController - RESTful API 入口 │ │ │
│ │ │ 【支持 CLI 调用和 UI 请求】 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────┐ │
│ │ 拦截层 (Interceptor) │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ SkillDrivenInterceptor - 技能驱动的请求分发 │ │ │
│ │ │ 【Bridger 层核心:CLI/UI 双端统一处理】 │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────┐ │
│ │ 服务层 (Service Layer) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │
│ │ │SkillManagement│ │SkillPublish │ │SkillProjectBridge│ │ │
│ │ │ Service │ │ Service │ │ Service │ │ │
│ │ │【CLI 状态管理】│ │【CLI 脚本生成】│ │【CLI/UI 桥接】 │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────┐ │
│ │ SPI 层 (Service Provider Interface) │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ A2uiSkillRegistrySPI - 技能注册与发现的统一接口 │ │ │
│ │ │ 【支持 CLI 指令映射的技能注册】 │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────┐ │
│ │ 技能实现层 (Skill Implementation) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ TreeSkill│ │ FormSkill│ │ChartSkill│ │ ... │ │ │
│ │ │【CLI 指令】│ │【CLI 指令】│ │【CLI 指令】│ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ AbstractA2uiSkill - 通用基类 (模板方法模式) │ │ │
│ │ │ 【提供 CLI 指令解析和 UI 生成的通用方法】 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────┐ │
│ │ 注解层 (Annotation Layer) │ │
│ │ ┌──────────────┐ ┌────────────────────────────────┐ │ │
│ │ │ @A2uiSkill │ │ A2uiSkillBehavior (接口) │ │ │
│ │ │【CLI 指令映射】│ │【CLI 指令处理规范】 │ │ │
│ │ └──────────────┘ └────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ════════════════════════════════════════════════════════════ │
│ 【Bridger 层】贯穿拦截层、服务层、技能实现层,实现 CLI ↔ UI │
│ ════════════════════════════════════════════════════════════ │
└─────────────────────────────────────────────────────────────────┘
2.2 数据流图(补充 CLI 指令到 UI 交互的数据流链路)
┌──────────────────────────────────────────────────────────────────┐
│ 请求处理数据流(CLI + UI 双端支持) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 1. 请求来源(CLI 或 UI) │
│ │ │
│ ├──── CLI 命令 ────┐ │
│ │ │ │
│ └──── UI 请求 ────┤ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Bridger 层:请求识别与转换 │ │
│ │ - CLI 命令 → 转换为 HTTP 请求 │ │
│ │ - UI 请求 → 直接传递 │ │
│ └────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2. SkillDrivenInterceptor.preHandle() │ │
│ │ 【统一处理 CLI 和 UI 请求】 │ │
│ └────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3. SkillManagementService.findSkill() │ │
│ │ - 通过 SPI 查找对应的技能 │ │
│ │ - 支持 CLI 指令名称映射 │ │
│ └────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4. A2uiSkillBehavior.buildGenJson() │ │
│ │ 【生成 UI 组件的 JSON 表示】 │ │
│ │ - CLI 指令参数 → UI 组件配置 │ │
│ │ - UI 操作参数 → UI 组件配置 │ │
│ └────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 5. 响应生成 │ │
│ │ - CLI 调用:返回 JSON + 生成 CLI 脚本 │ │
│ │ - UI 调用:返回 JSON 渲染 UI │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 【双向桥接】 │
│ CLI 命令 ────▶ Bridger 层 ────▶ UI 组件生成 │
│ UI 操作 ────▶ Bridger 层 ────▶ CLI 脚本生成 │
│ │
└──────────────────────────────────────────────────────────────────┘
2.3 技能生命周期管理(新增桥接层技能的生命周期说明)
┌─────────────────────────────────────────────────────────────────┐
│ Skill 生命周期(支持 CLI 桥接) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 定义 │───▶│ 注册 │───▶│ 发现 │───▶│ 调用 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ @A2uiSkill SPI 加载 拦截器解析 buildGenJson() │
│ 注解声明 ServiceLoader 组件类型映射 模板渲染 │
│ 【CLI 指令映射】 【CLI 适配】 【CLI/UI 双端】 【CLI 参数解析】│
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 发布 │───▶│ 桥接 │───▶│ 管理 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 生成 Java 源码 添加到项目 增删改查操作 │
│ 编译打包 技能依赖管理 CLI 脚本管理 │
│ 【CLI 文档生成】 【CLI 命令注册】 │
│ │
│ ════════════════════════════════════════════════════════════ │
│ 【Bridger 层支持】 │
│ - CLI 指令自动映射为技能 ID │
│ - 技能调用记录可追溯,生成 CLI 脚本 │
│ - UI 操作与 CLI 命令双向同步 │
│ ════════════════════════════════════════════════════════════ │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 注解层设计
3.1 @A2uiSkill 注解(新增 CLI 指令映射扩展说明)
注解是整个技能系统的元数据基础,用于声明式地定义技能的核心属性,包括 CLI 指令的映射关系。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface A2uiSkill {
String id(); // 技能唯一标识(CLI 指令名称)
String name() default ""; // 技能显示名称
String description() default ""; // 技能描述
String version() default "1.0.0"; // 版本号
String category() default "data-display"; // 分类
String[] capabilities() default {}; // 能力列表
ModuleViewType moduleViewType() default ModuleViewType.NONE;
ComponentType componentType() default ComponentType.MODULE;
int priority() default 100; // 优先级
// ⭐ CLI 桥接扩展
String[] cliCommands() default {}; // CLI 命令别名列表
String cliHelp() default ""; // CLI 帮助文档
}
设计要点:
- id 是必填项:确保每个技能有唯一标识,同时作为 CLI 指令的默认名称
- CLI 指令映射:通过
cliCommands字段,支持一个技能对应多个 CLI 命令别名 - CLI 帮助文档:通过
cliHelp字段,为 CLI 用户提供详细的命令说明 - 默认值策略:减少配置负担,常用字段提供合理默认值
- 类型安全:使用枚举类型(ModuleViewType、ComponentType)避免字符串错误
- 可扩展性:capabilities 数组支持声明技能的特殊能力
CLI 指令映射示例:
@Component
@A2uiSkill(
id = "treegrid",
name = "树形表格",
description = "用于展示树形结构数据的表格组件",
category = "data-display",
moduleViewType = ModuleViewType.GRIDCONFIG,
componentType = ComponentType.TREEGRID,
cliCommands = {"treegrid", "tg", "tree-grid"}, // CLI 命令别名
cliHelp = "Usage: a2ui treegrid <module_name> [options]\n" +
"Options:\n" +
" --caption <text> Set module caption\n" +
" --fields <list> Set field list (comma-separated)",
priority = 10
)
public class TreeGridSkill extends AbstractA2uiSkill {
// 技能实现...
}
3.2 A2uiSkillBehavior 接口(补充 CLI 指令处理规范)
行为接口定义了技能必须实现的核心方法,包括 CLI 指令的处理规范:
public interface A2uiSkillBehavior {
String getSkillId(); // 获取技能 ID
String getComponentType(); // 获取组件类型
ModuleViewType getModuleViewType(); // 获取模块视图类型
String getCategory(); // 获取分类
String buildGenJson(String moduleName, String caption, // 构建生成 JSON
List<String> fields,
Map<String, Object> options);
Map<String, Object> buildFromNaturalLanguage(String query); // NLP 构建
List<String> getKeywords(); // 关键词列表
JSONObject toCardJson(); // 卡片 JSON
// ⭐ CLI 桥接方法
String[] getCliCommands(); // 获取 CLI 命令别名
String getCliHelp(); // 获取 CLI 帮助文档
Map<String, Object> parseCliArgs(String[] args); // 解析 CLI 参数
String generateCliScript(Map<String, Object> params); // 生成 CLI 脚本
}
方法职责:
buildGenJson(): 核心方法,根据参数生成模块的 JSON 表示,支持 CLI 参数和 UI 参数buildFromNaturalLanguage(): 支持自然语言输入,智能解析意图,可用于 CLI 交互式命令toCardJson(): 生成用于前端展示的卡片元数据parseCliArgs():
新增:解析 CLI 命令行参数,转换为技能调用参数generateCliScript():
新增:根据参数生成可执行的 CLI 脚本
4. SPI 扩展机制
4.1 A2uiSkillRegistry 接口(补充 CLI 适配的 SPI 扩展)
SPI 接口定义了技能注册表的契约,支持 CLI 指令的注册和查询:
public interface A2uiSkillRegistry {
int getPriority(); // 优先级
String getSkillClassName(String skillId); // 获取类名
Class<?> getSkillClass(String skillId); // 获取 Class
Set<String> getSkillIds(); // 所有技能 ID
String getCategory(String skillId); // 获取分类
ModuleViewType getModuleViewType(String skillId); // 获取视图类型
ComponentType getComponentType(String skillId); // 获取组件类型
String findSkillIdByComponentType(String componentType);// 反向查找
// ⭐ CLI 桥接扩展
String findSkillIdByCliCommand(String cliCommand); // 通过 CLI 命令查找技能
Set<String> getAllCliCommands(); // 获取所有 CLI 命令
String getCliHelp(String skillId); // 获取 CLI 帮助文档
}
4.2 A2uiSkillRegistrySPI 实现(CLI 命令缓存)
SPI 加载器使用 Java 原生的 ServiceLoader 机制,并增加 CLI 命令的缓存:
public class A2uiSkillRegistrySPI {
private static volatile A2uiSkillRegistrySPI instance;
private final List<A2uiSkillRegistry> registries = new ArrayList<>();
private final Map<String, String> classNameCache = new ConcurrentHashMap<>();
private final Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
private final Map<String, String> componentTypeToSkillIdCache = new ConcurrentHashMap<>();
// ⭐ CLI 命令缓存
private final Map<String, String> cliCommandToSkillIdCache = new ConcurrentHashMap<>();
private void loadRegistries() {
ServiceLoader<A2uiSkillRegistry> loader = ServiceLoader.load(
A2uiSkillRegistry.class,
Thread.currentThread().getContextClassLoader());
for (A2uiSkillRegistry registry : loader) {
registries.add(registry);
}
// 按优先级排序
registries.sort(Comparator.comparingInt(A2uiSkillRegistry::getPriority));
// ⭐ 初始化 CLI 命令缓存
initializeCliCommandCache();
}
private void initializeCliCommandCache() {
for (A2uiSkillRegistry registry : registries) {
Set<String> cliCommands = registry.getAllCliCommands();
for (String cliCommand : cliCommands) {
String skillId = registry.findSkillIdByCliCommand(cliCommand);
if (skillId != null) {
cliCommandToSkillIdCache.put(cliCommand.toLowerCase(), skillId);
}
}
}
}
// ⭐ 通过 CLI 命令查找技能
public String findSkillIdByCliCommand(String cliCommand) {
return cliCommandToSkillIdCache.get(cliCommand.toLowerCase());
}
}
设计亮点:
- 多重缓存:classNameCache、classCache、componentTypeToSkillIdCache、cliCommandToSkillIdCache 四层缓存
- CLI 命令映射:支持 CLI 命令别名到技能 ID 的快速查找
- 优先级排序:支持多个注册表,按优先级加载
- 线程安全:使用 volatile 和 ConcurrentHashMap 保证并发安全
- 热加载支持:提供 reload() 和 clearCache() 方法
4.3 SPI 配置文件
在 META-INF/services 下创建配置文件:
# 文件路径:src/main/resources/META-INF/services/net.ooder.annotation.spi.A2uiSkillRegistry
net.ooder.a2ui.nlp.skill.A2uiSkillRegistryImpl
5. 技能实现层
5.1 AbstractA2uiSkill 抽象基类(提供 CLI 指令解析通用方法)
抽象基类使用模板方法模式提供通用实现,包括 CLI 指令解析和脚本生成的通用方法:
public abstract class AbstractA2uiSkill implements A2uiSkillBehavior {
protected Configuration freemarkerCfg; // FreeMarker 模板引擎
public AbstractA2uiSkill() {
// 初始化 FreeMarker 配置
freemarkerCfg = new Configuration(Configuration.VERSION_2_3_31);
freemarkerCfg.setClassForTemplateLoading(this.getClass(), "/");
freemarkerCfg.setDefaultEncoding("UTF-8");
}
// 默认实现:通过注解获取元数据
@Override
public String getSkillId() {
A2uiSkill anno = getClass().getAnnotation(A2uiSkill.class);
return anno != null ? anno.id() : getClass().getSimpleName().toLowerCase();
}
// ⭐ CLI 桥接:获取 CLI 命令别名
@Override
public String[] getCliCommands() {
A2uiSkill anno = getClass().getAnnotation(A2uiSkill.class);
return anno != null ? anno.cliCommands() : new String[]{getSkillId()};
}
// ⭐ CLI 桥接:获取 CLI 帮助文档
@Override
public String getCliHelp() {
A2uiSkill anno = getClass().getAnnotation(A2uiSkill.class);
return anno != null ? anno.cliHelp() : "No help available for " + getSkillId();
}
// ⭐ CLI 桥接:解析 CLI 参数(默认实现,子类可重写)
@Override
public Map<String, Object> parseCliArgs(String[] args) {
Map<String, Object> params = new LinkedHashMap<>();
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("--")) {
String key = args[i].substring(2);
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
params.put(key, args[i + 1]);
i++;
} else {
params.put(key, true);
}
}
}
return params;
}
// ⭐ CLI 桥接:生成 CLI 脚本(默认实现)
@Override
public String generateCliScript(Map<String, Object> params) {
StringBuilder script = new StringBuilder();
script.append("a2ui ").append(getSkillId());
if (params.containsKey("moduleName")) {
script.append(" ").append(params.get("moduleName"));
}
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (!"moduleName".equals(entry.getKey())) {
script.append(" --").append(entry.getKey());
if (!(entry.getValue() instanceof Boolean)) {
script.append(" ").append(entry.getValue());
}
}
}
return script.toString();
}
// 模板方法:子类重写以提供具体组件的生成逻辑
protected abstract String getModuleTemplate();
// 通用方法:构建模块 JSON
protected String buildModuleFromTemplate(String className, String propertiesJson,
String childrenJson) {
String template = getModuleTemplate();
Template tpl = new Template("module", template, freemarkerCfg);
// ... 模板渲染逻辑
}
}
5.2 TreeGridSkill 实现示例(展示 CLI 指令支持)
具体技能实现展示如何扩展抽象基类,并支持 CLI 指令:
@Component
@A2uiSkill(
id = "treegrid",
name = "树形表格",
description = "用于展示树形结构数据的表格组件",
category = "data-display",
moduleViewType = ModuleViewType.GRIDCONFIG,
componentType = ComponentType.TREEGRID,
cliCommands = {"treegrid", "tg", "tree-grid"},
cliHelp = "Usage: a2ui treegrid <module_name> [options]\n" +
" --caption <text> Set module caption\n" +
" --fields <list> Set field list (comma-separated)\n" +
" --tree-field <name> Set tree field name",
priority = 10
)
public class TreeGridSkill extends AbstractA2uiSkill {
@Override
public List<String> getKeywords() {
return Arrays.asList("树形表格", "tree", "grid", "层级数据", "树状结构");
}
@Override
public String buildGenJson(String moduleName, String caption,
List<String> fields, Map<String, Object> options) {
// 1. 创建基础属性
JSONObject properties = createBaseProperties(caption, moduleName);
// 2. 添加 TreeGrid 特有配置
properties.put("treeField", options.getOrDefault("treeField", "name"));
properties.put("showTreeLines", options.getOrDefault("showTreeLines", true));
properties.put("expandColumn", options.getOrDefault("expandColumn", 0));
// 3. 构建字段配置
JSONArray columns = new JSONArray();
for (int i = 0; i < fields.size(); i++) {
JSONObject col = new JSONObject();
col.put("field", fields.get(i));
col.put("caption", fields.get(i));
col.put("width", 100);
columns.add(col);
}
properties.put("columns", columns);
// 4. 使用模板生成最终 JSON
return buildModuleFromTemplate(moduleName, properties.toJSONString(), "[]");
}
// ⭐ CLI 桥接:重写 CLI 参数解析
@Override
public Map<String, Object> parseCliArgs(String[] args) {
Map<String, Object> params = super.parseCliArgs(args);
// 解析 fields 参数(逗号分隔)
if (params.containsKey("fields")) {
String fieldsStr = (String) params.get("fields");
params.put("fields", Arrays.asList(fieldsStr.split(",")));
}
return params;
}
}
5.3 内置技能列表
| Skill ID | Component | Category | CLI Commands | Description |
|---|---|---|---|---|
| treegrid | TREEGRID | data-display | treegrid, tg, tree-grid | 树形表格组件 |
| form | FORMLAYOUT | data-input | form, f | 数据录入表单 |
| tree | TREEVIEW | data-display | tree, t | 树形视图组件 |
| gallery | GALLERY | data-display | gallery, gal | 图片画廊组件 |
| chart | ECHARTS | data-visualization | chart, c | ECharts 图表组件 |
| navtree | TREEBAR | navigation | navtree, nt | 导航树组件 |
| navigallery | GALLERY | navigation | navigallery, ngal | 导航画廊组件 |
| navtabs | TABS | navigation | navtabs, nt | 导航标签组件 |
| navmenubar | BAR | navigation | navmenubar, nmb | 导航菜单栏组件 |
| tabscontainer | TABS | layout | tabs, tc | 标签容器组件 |
| layout | LAYOUT | layout | layout, l | 布局组件 |
6. 拦截器重构
6.1 SkillDrivenInterceptor 核心逻辑(强化 CLI/UI 双端请求统一处理)
统一拦截器替代了原有的多个硬编码拦截器,并支持 CLI 和 UI 双端请求的统一处理:
@Component
public class SkillDrivenInterceptor extends BaseInterceptor {
@Autowired
private List<ResourceResolver> resourceResolvers;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String url = request.getRequestURI();
SuffixType suffix = SuffixType.fromUrl(url);
// ⭐ Bridger 层:识别请求来源(CLI 或 UI)
boolean isCliRequest = isCliRequest(request);
// 1. 无后缀请求:数据请求处理
if (suffix == SuffixType.NONE) {
return handleDataRequest(handler, request, response, isCliRequest);
}
// 2. 剥离后缀获取类名
String className = SuffixType.stripSuffix(url);
// 3. 根据后缀类型分发到不同处理器
switch (suffix) {
case JSX: return handleJsx(className, request, response, isCliRequest);
case CLS: return handleCls(className, request, response, isCliRequest);
case DYN: return handleDyn(handler, request, response, isCliRequest);
case VIEW:
case JSA:
case JSAA: return handleView(className, request, response, isCliRequest);
default: return true;
}
}
// ⭐ Bridger 层:判断是否为 CLI 请求
private boolean isCliRequest(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
String cliFlag = request.getHeader("X-A2UI-CLI");
return "true".equals(cliFlag) ||
(userAgent != null && userAgent.startsWith("A2UI-CLI/"));
}
// JSX 处理:技能驱动的模块构建(支持 CLI 和 UI)
private boolean handleJsx(String className, HttpServletRequest request,
HttpServletResponse response, boolean isCliRequest) {
// 1. 尝试静态资源解析
if (tryStaticResource(className, request, response)) {
return false;
}
// 2. 解析组件类型
String componentType = resolveComponentType(className);
// 3. ⭐ Bridger 层:查找对应技能(支持 CLI 命令映射)
A2uiSkillBehavior skill = findSkill(componentType, request, isCliRequest);
if (skill != null) {
try {
// 4. 使用技能构建模块
UIModule uiModule = buildModuleFromSkill(skill, className, request, isCliRequest);
if (uiModule != null) {
// 5. 填充数据并返回 JSON
MethodConfig methodConfig = getCurrMethodConfig(request);
if (methodConfig != null && uiModule.getComponent() != null) {
fillDataFromMethod(methodConfig, uiModule, request);
}
String json = EngineFactory.getAdminESDClient()
.genJSON(uiModule, null, true);
// ⭐ Bridger 层:CLI 请求额外生成 CLI 脚本
if (isCliRequest) {
String cliScript = generateCliScript(skill, request);
response.setHeader("X-A2UI-CLI-Script", cliScript);
}
sendJSON(response, json.toString());
return false;
}
} catch (Exception e) {
log.error("Skill-driven module build failed", e);
}
}
// 6. 降级到传统模块处理
return handleLegacyModule(className, request, response);
}
// ⭐ Bridger 层:技能查找(支持 CLI 命令映射)
private A2uiSkillBehavior findSkill(String componentType,
HttpServletRequest request,
boolean isCliRequest) {
if (componentType == null) return null;
// 1. 从配置工厂查找
A2uiSkillBehavior skill = C2UConfigFactory.getInstance()
.findSkillByComponentType(componentType);
if (skill != null) return skill;
// 2. 从 SPI 查找
A2uiSkillRegistrySPI skillSPI = A2uiSkillRegistrySPI.getInstance();
// ⭐ CLI 请求:尝试通过 CLI 命令查找
if (isCliRequest) {
String cliCommand = request.getHeader("X-A2UI-CLI-Command");
if (cliCommand != null) {
String skillId = skillSPI.findSkillIdByCliCommand(cliCommand);
if (skillId != null) {
return loadSkillInstance(skillSPI, skillId);
}
}
}
// 3. 通过组件类型查找
String skillId = skillSPI.findSkillIdByComponentType(componentType);
if (skillId != null) {
return loadSkillInstance(skillSPI, skillId);
}
return null;
}
// ⭐ Bridger 层:生成 CLI 脚本
private String generateCliScript(A2uiSkillBehavior skill,
HttpServletRequest request) {
Map<String, Object> params = extractParamsFromRequest(request);
return skill.generateCliScript(params);
}
}
6.2 拦截器对比(突出 CLI/UI 双端支持)
┌─────────────────────────────────────────────────────────────────┐
│ 拦截器架构对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 传统架构: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │JSXInter.│ │CLSInter.│ │DYNInter.│ │ ... │ │
│ │(仅 UI) │ │(仅 UI) │ │(仅 UI) │ │ │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └───────────┴───────────┴───────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │BaseInterceptor │ │
│ │(无 CLI 支持) │ │
│ └─────────────────┘ │
│ │
│ 新架构(Bridger 层): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ SkillDrivenInterceptor (统一入口) │ │
│ │ 【支持 CLI + UI 双端请求】 │ │
│ │ ⭐ Bridger 层核心 │ │
│ └────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ ┌──────▼─────────┐ ┌──────▼─────────┐ │
│ │SkillRegistrySPI│ │ResourceResolver│ │
│ │(技能注册中心) │ │(资源解析策略) │ │
│ │【CLI 命令映射】│ │【CLI/UI 双端】 │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ 【核心改进】 │
│ - CLI 指令自动映射为技能调用 │
│ - UI 操作可生成 CLI 脚本 │
│ - 一套技能逻辑,双端复用 │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 服务层架构
7.1 SkillManagementService(CLI 状态管理)
技能管理的核心服务,采用单例模式,支持 CLI 状态管理:
public class SkillManagementService {
private static volatile SkillManagementService instance;
private final Map<String, SkillModuleConfig> skillModuleCache = new ConcurrentHashMap<>();
// ⭐ CLI 桥接:CLI 操作历史记录
private final Map<String, List<CliOperationRecord>> cliOperationHistory = new ConcurrentHashMap<>();
public static SkillManagementService getInstance() {
if (instance == null) {
synchronized (SkillManagementService.class) {
if (instance == null) {
instance = new SkillManagementService();
}
}
}
return instance;
}
// 核心方法:通过技能创建模块(支持 CLI 和 UI)
public UIModule createModuleFromSkill(String skillId, String moduleName,
String projectName, String caption,
List<String> fields,
Map<String, Object> options,
boolean isCliRequest) {
// 1. 查找技能
A2uiSkillBehavior skill = findSkill(skillId);
if (skill == null) {
logger.error("Skill not found: " + skillId);
return null;
}
// 2. 生成模块配置
SkillModuleConfig config = SkillModuleConfig.fromSkill(
skill, moduleName, caption, fields, options);
skillModuleCache.put(moduleName, config);
// 3. 创建物理模块
ProjectCacheManager pcm = getProjectCacheManager(projectName);
INProject project = pcm.getProjectByName(projectName);
UIModule uiModule = pcm.createModule(version, moduleName);
// 4. 保存生成的 JSON 到文件系统
if (uiModule != null && config.getGenJson() != null) {
String physicalPath = version.getPath() + "/" +
moduleName.replace(".", "/") + ".cls";
CtVfsFactory.getCtVfsService().saveFileAsContent(
physicalPath, config.getGenJson(), "UTF-8");
}
// ⭐ CLI 桥接:记录 CLI 操作历史
if (isCliRequest) {
recordCliOperation(skillId, moduleName, options);
}
return uiModule;
}
// ⭐ CLI 桥接:记录 CLI 操作
private void recordCliOperation(String skillId, String moduleName,
Map<String, Object> options) {
CliOperationRecord record = new CliOperationRecord();
record.setSkillId(skillId);
record.setModuleName(moduleName);
record.setOptions(options);
record.setTimestamp(new Date());
cliOperationHistory.computeIfAbsent(skillId, k -> new ArrayList<>())
.add(record);
}
// ⭐ CLI 桥接:获取 CLI 操作历史
public List<CliOperationRecord> getCliOperationHistory(String skillId) {
return cliOperationHistory.getOrDefault(skillId, Collections.emptyList());
}
// ⭐ CLI 桥接:生成 CLI 脚本
public String generateCliScript(String skillId, Map<String, Object> params) {
A2uiSkillBehavior skill = findSkill(skillId);
if (skill != null) {
return skill.generateCliScript(params);
}
return null;
}
// 获取所有技能卡片
public Map<String, Object> getSkillCards() {
Map<String, Object> result = new LinkedHashMap<>();
A2uiSkillRegistrySPI skillSPI = A2uiSkillRegistrySPI.getInstance();
Set<String> skillIds = skillSPI.getSkillIds();
List<JSONObject> cards = new ArrayList<>();
for (String skillId : skillIds) {
A2uiSkillBehavior skill = findSkill(skillId);
if (skill != null) {
JSONObject card = skill.toCardJson();
// ⭐ CLI 桥接:添加 CLI 命令信息
card.put("cliCommands", skill.getCliCommands());
card.put("cliHelp", skill.getCliHelp());
cards.add(card);
}
}
result.put("skills", cards);
result.put("total", cards.size());
return result;
}
}
7.2 SkillPublishService(CLI 脚本生成)
技能发布服务,负责生成可发布的技能包,包括 CLI 脚本和文档:
public class SkillPublishService {
public SkillPublishResult publish(A2uiSkillBehavior skill, String outputDir) {
String skillId = skill.getSkillId();
// 1. 创建技能目录结构
Path skillDir = Paths.get(outputDir, skillId + "-skill");
Files.createDirectories(skillDir);
// 2. 生成 Java 源码
Path javaDir = skillDir.resolve("src/main/java")
.resolve(packageName.replace(".", "/"));
Files.createDirectories(javaDir);
String javaSource = generateSkillJavaSource(skill, packageName);
Path javaFile = javaDir.resolve(skill.getClass().getSimpleName() + ".java");
Files.writeString(javaFile, javaSource);
// 3. 生成 SPI 配置文件
Path metaInfDir = skillDir.resolve("src/main/resources/META-INF/services");
Files.createDirectories(metaInfDir);
String spiContent = packageName + "." +
skill.getClass().getSimpleName() + "\n";
Path spiFile = metaInfDir.resolve("net.ooder.annotation.spi.A2uiSkillRegistry");
Files.writeString(spiFile, spiContent);
// ⭐ CLI 桥接:生成 CLI 脚本模板
Path cliDir = skillDir.resolve("cli");
Files.createDirectories(cliDir);
String cliScript = generateCliScriptTemplate(skill);
Path cliFile = cliDir.resolve(skillId + ".sh");
Files.writeString(cliFile, cliScript);
// ⭐ CLI 桥接:生成 CLI 文档
Path docsDir = skillDir.resolve("docs");
Files.createDirectories(docsDir);
String cliDoc = generateCliDocumentation(skill);
Path docFile = docsDir.resolve("CLI.md");
Files.writeString(docFile, cliDoc);
// 4. 返回发布结果
SkillPublishResult result = SkillPublishResult.success(skillId);
result.setJavaSource(javaSource);
result.setCliScript(cliScript);
result.setPublishedAt(new Date().toInstant().toString());
return result;
}
// ⭐ CLI 桥接:生成 CLI 脚本模板
private String generateCliScriptTemplate(A2uiSkillBehavior skill) {
StringBuilder script = new StringBuilder();
script.append("#!/bin/bash\n\n");
script.append("# A2UI CLI Script for ").append(skill.getSkillId()).append("\n");
script.append("# Generated by Ooder A2UI SkillPublishService\n\n");
script.append("SKILL_ID=\"").append(skill.getSkillId()).append("\"\n");
script.append("MODULE_NAME=\"\"\n");
script.append("CAPTION=\"\"\n");
script.append("FIELDS=\"\"\n\n");
script.append("# Parse arguments\n");
script.append("while [[ $# -gt 0 ]]; do\n");
script.append(" case $1 in\n");
script.append(" --module) MODULE_NAME=\"$2\"; shift 2 ;;\n");
script.append(" --caption) CAPTION=\"$2\"; shift 2 ;;\n");
script.append(" --fields) FIELDS=\"$2\"; shift 2 ;;\n");
script.append(" *) echo \"Unknown option: $1\"; exit 1 ;;\n");
script.append(" esac\n");
script.append("done\n\n");
script.append("# Call A2UI API\n");
script.append("curl -X POST \"http://localhost:8080/api/skill/build\" \\\n");
script.append(" -H \"Content-Type: application/json\" \\\n");
script.append(" -H \"X-A2UI-CLI: true\" \\\n");
script.append(" -H \"X-A2UI-CLI-Command: $SKILL_ID\" \\\n");
script.append(" -d \"{\\\"skillId\\\":\\\"$SKILL_ID\\\",\\\"moduleName\\\":\\\"$MODULE_NAME\\\",\\\"caption\\\":\\\"$CAPTION\\\",\\\"fields\\\":\\\"$FIELDS\\\"}\"\n");
return script.toString();
}
// ⭐ CLI 桥接:生成 CLI 文档
private String generateCliDocumentation(A2uiSkillBehavior skill) {
StringBuilder doc = new StringBuilder();
doc.append("# ").append(skill.getSkillId()).append(" - CLI Usage\n\n");
doc.append("## Description\n");
doc.append(skill.getCliHelp()).append("\n\n");
doc.append("## CLI Commands\n");
for (String cmd : skill.getCliCommands()) {
doc.append("- `a2ui ").append(cmd).append("`\n");
}
doc.append("\n## Examples\n");
doc.append("```bash\n");
doc.append("# Create a tree grid module\n");
doc.append("a2ui treegrid MyModule --caption \"My Tree\" --fields \"id,name,status\"\n");
doc.append("```\n");
return doc.toString();
}
}
7.3 服务层协作关系(CLI/UI 桥接)
┌─────────────────────────────────────────────────────────────────┐
│ 服务层协作图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ │
│ │ SkillController │ │
│ │ (REST API 入口) │ │
│ │ 【支持 CLI 调用】 │ │
│ └──────┬───────────────┘ │
│ │ │
│ │ 调用 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SkillManagementService │ │
│ │ - 技能生命周期管理 │ │
│ │ - 模块创建与删除 │ │
│ │ - 技能卡片生成 │ │
│ │ ⭐ CLI 操作历史记录 │ │
│ │ ⭐ CLI 脚本生成 │ │
│ └──────┬──────────────────────┬──────────────────────────┘ │
│ │ │ │
│ │ 调用 │ 调用 │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │SkillPublishSvc │ │SkillProjectBridge │
│ │- 生成 Java 源码 │ │- 项目技能关联 │ │
│ │- 创建 SPI 配置 │ │- 技能依赖管理 │ │
│ │- 打包发布 │ │- 项目技能概览 │ │
│ │⭐ CLI 脚本生成 │ │⭐ CLI/UI 桥接 │ │
│ │⭐ CLI 文档生成 │ │⭐ CLI 脚本管理 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 【Bridger 层支持】 │
│ - 所有服务层组件支持 CLI 桥接 │
│ - CLI 操作可追溯、可回滚 │
│ - UI 操作自动生成 CLI 脚本 │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 项目整合桥接
8.1 SkillProjectBridge(CLI/UI 桥接服务)
桥接服务负责技能与项目之间的关联管理,并实现 CLI 与 UI 的双向桥接:
public class SkillProjectBridge {
// 添加技能到项目(支持 CLI 和 UI)
public SkillProjectBridgeResult addSkillToProject(
String projectName, String skillId,
String moduleName, String caption,
List<String> fields,
boolean isCliRequest) {
// 1. 查找技能
SkillManagementService skillMgmt = SkillManagementService.getInstance();
A2uiSkillBehavior skill = skillMgmt.findSkill(skillId);
if (skill == null) {
return SkillProjectBridgeResult.fail("Skill not found: " + skillId);
}
// 2. 获取项目管理器
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm == null) {
return SkillProjectBridgeResult.fail("Project not found: " + projectName);
}
// 3. 创建模块
UIModule module = pcm.createModuleFromSkill(projectName, moduleName,
skillId, caption, fields, new HashMap<>(), isCliRequest);
// 4. 记录技能依赖
ProjectConfig config = project.getConfig();
config.addSkillDependency(skillId);
pcm.updateProjectConfig(projectName, config);
// 5. 返回结果
SkillProjectBridgeResult result = SkillProjectBridgeResult.success();
result.setSkillId(skillId);
result.setModuleName(moduleName);
result.setProjectName(projectName);
result.setComponentType(skill.getComponentType());
// ⭐ CLI 桥接:生成 CLI 脚本
if (isCliRequest) {
Map<String, Object> params = new LinkedHashMap<>();
params.put("moduleName", moduleName);
params.put("caption", caption);
params.put("fields", String.join(",", fields));
result.setCliScript(skill.generateCliScript(params));
}
return result;
}
// 列出项目的所有技能
public List<Map<String, String>> listProjectSkills(String projectName) {
List<Map<String, String>> skills = new ArrayList<>();
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm != null) {
// 从项目配置中读取技能依赖
List<String> skillDeps = pcm.getSkillDependencies(projectName);
for (String skillId : skillDeps) {
Map<String, String> info = new LinkedHashMap<>();
info.put("skillId", skillId);
A2uiSkillBehavior skill = SkillManagementService
.getInstance().findSkill(skillId);
if (skill != null) {
info.put("name", skill.toCardJson().getString("name"));
info.put("componentType", skill.getComponentType());
info.put("category", skill.getCategory());
// ⭐ CLI 桥接:添加 CLI 命令信息
info.put("cliCommands", String.join(",", skill.getCliCommands()));
}
skills.add(info);
}
}
return skills;
}
// 从项目移除技能
public boolean removeSkillFromProject(String projectName, String skillId) {
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm == null) return false;
INProject project = pcm.getProjectByName(projectName);
if (project != null && project.getConfig() != null) {
ProjectConfig config = project.getConfig();
// 移除技能依赖
config.removeSkillDependency(skillId);
pcm.updateProjectConfig(projectName, config);
return true;
}
return false;
}
// ⭐ CLI 桥接:获取项目的 CLI 操作历史
public List<CliOperationRecord> getProjectCliHistory(String projectName) {
ProjectCacheManager pcm = getProjectCacheManager(projectName);
if (pcm != null) {
INProject project = pcm.getProjectByName(projectName);
if (project != null) {
return SkillManagementService.getInstance()
.getCliOperationHistory(project.getSkillDependencies());
}
}
return Collections.emptyList();
}
}
8.2 项目与技能的关系(CLI 桥接视角)
┌─────────────────────────────────────────────────────────────────┐
│ 项目 - 技能关系图(CLI 桥接视角) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Project A │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ ProjectConfig │ │ │
│ │ │ skillDependencies: ["treegrid", "form", "chart"] │ │ │
│ │ │ cliOperationHistory: [...] │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Module 1 │ │ Module 2 │ │ Module 3 │ ... │ │
│ │ │ (treegrid)│ │ (form) │ │ (chart) │ │ │
│ │ │ CLI: tg │ │ CLI: f │ │ CLI: c │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Skill Registry (全局注册表) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │treegrid │ │ form │ │ chart │ │ ... │ │ │
│ │ │CLI: tg │ │CLI: f │ │CLI: c │ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 【CLI 桥接关系】 │
│ - 一个项目可以依赖多个技能 (1:N) │
│ - 一个技能可以被多个项目复用 (N:M) │
│ - 技能是全局共享的,项目通过依赖引用 │
│ - ⭐ CLI 命令映射到技能,实现 CLI ↔ UI 双向桥接 │
│ - ⭐ 所有 CLI 操作可追溯,支持审计和回滚 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. RESTful API 设计
9.1 SkillController API 列表(支持 CLI 调用)
@RestController
@RequestMapping("/api/skill")
public class SkillController {
// 1. 获取所有技能列表
@GetMapping("/list")
public ResponseEntity<Map<String, Object>> listSkills() {
// GET /api/skill/list
// Response: { "skills": [...], "total": 11 }
// ⭐ CLI: a2ui list
}
// 2. 通过技能构建模块(支持 CLI 调用)
@PostMapping("/build")
public ResponseEntity<Map<String, Object>> buildFromSkill(
@RequestBody JSONObject request,
@RequestHeader(value = "X-A2UI-CLI", required = false) String cliFlag,
@RequestHeader(value = "X-A2UI-CLI-Command", required = false) String cliCommand) {
// POST /api/skill/build
// Body: { "skillId": "treegrid", "moduleName": "MyModule",
// "caption": "测试模块", "fields": ["id", "name"],
// "options": {...} }
// ⭐ CLI: a2ui treegrid MyModule --caption "测试模块" --fields "id,name"
boolean isCliRequest = "true".equals(cliFlag);
// ... 处理逻辑
}
// 3. 自然语言构建模块
@PostMapping("/nlp-build")
public ResponseEntity<Map<String, Object>> buildFromNaturalLanguage(
@RequestBody JSONObject request) {
// POST /api/skill/nlp-build
// Body: { "query": "创建一个包含 ID 和名称的树形表格" }
// ⭐ CLI: a2ui "创建一个包含 ID 和名称的树形表格"
}
// 4. 添加技能到项目
@PostMapping("/project/add-skill")
public ResponseEntity<Map<String, Object>> addSkillToProject(
@RequestBody JSONObject request,
@RequestHeader(value = "X-A2UI-CLI", required = false) String cliFlag) {
// POST /api/skill/project/add-skill
// ⭐ CLI: a2ui project add-skill ProjectA treegrid --module MyTree
}
// 5. 列出项目的技能
@GetMapping("/project/{projectName}/skills")
public ResponseEntity<Map<String, Object>> listProjectSkills(
@PathVariable String projectName) {
// GET /api/skill/project/ProjectA/skills
// ⭐ CLI: a2ui project list ProjectA
}
// 6. 从项目移除技能
@DeleteMapping("/project/{projectName}/skill/{skillId}")
public ResponseEntity<Map<String, Object>> removeSkillFromProject(
@PathVariable String projectName,
@PathVariable String skillId) {
// DELETE /api/skill/project/ProjectA/skill/treegrid
// ⭐ CLI: a2ui project remove ProjectA treegrid
}
// 7. 发布技能
@PostMapping("/publish/{skillId}")
public ResponseEntity<Map<String, Object>> publishSkill(
@PathVariable String skillId,
@RequestParam(defaultValue = "./published-skills") String outputDir) {
// POST /api/skill/publish/treegrid?outputDir=./published
// ⭐ CLI: a2ui publish treegrid --output ./published
}
// ⭐ CLI 桥接:获取 CLI 操作历史
@GetMapping("/project/{projectName}/cli-history")
public ResponseEntity<List<CliOperationRecord>> getCliHistory(
@PathVariable String projectName) {
// GET /api/skill/project/ProjectA/cli-history
// ⭐ CLI: a2ui project history ProjectA
}
// ⭐ CLI 桥接:生成 CLI 脚本
@PostMapping("/generate-cli-script")
public ResponseEntity<Map<String, String>> generateCliScript(
@RequestBody JSONObject request) {
// POST /api/skill/generate-cli-script
// Body: { "skillId": "treegrid", "params": {...} }
// ⭐ 用于 UI 操作生成 CLI 脚本
}
// 8. 健康检查
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
// GET /api/skill/health
// Response: { "status": "UP", "registeredSkills": 11, ... }
}
}
9.2 API 响应示例(包含 CLI 信息)
获取技能列表:
{
"skills": [
{
"skillId": "treegrid",
"name": "树形表格",
"description": "用于展示树形结构数据的表格组件",
"category": "data-display",
"componentType": "TREEGRID",
"moduleViewType": "GRIDCONFIG",
"version": "1.0.0",
"keywords": ["树形表格", "tree", "grid", "层级数据"],
"cliCommands": ["treegrid", "tg", "tree-grid"],
"cliHelp": "Usage: a2ui treegrid <module_name> [options]..."
}
],
"total": 11
}
添加技能到项目(CLI 调用):
{
"success": true,
"message": "Skill added successfully",
"skillId": "treegrid",
"moduleName": "MyTreeGrid",
"componentType": "TREEGRID",
"projectName": "ProjectA",
"cliScript": "a2ui treegrid MyTreeGrid --caption \"My Tree\" --fields \"id,name,status\""
}
10. 最佳实践
10.1 开发自定义技能(支持 CLI 指令)
步骤 1:创建技能类
@Component
@A2uiSkill(
id = "mycustom",
name = "我的自定义组件",
description = "用于特定业务场景的自定义组件",
category = "custom",
componentType = ComponentType.CUSTOM,
cliCommands = {"mycustom", "mc", "my-custom"}, // ⭐ CLI 命令别名
cliHelp = "Usage: a2ui mycustom <module_name> [options]\n" +
" --caption <text> Set module caption\n" +
" --custom-prop <value> Set custom property",
priority = 50
)
public class MyCustomSkill extends AbstractA2uiSkill {
@Override
public List<String> getKeywords() {
return Arrays.asList("自定义", "custom", "业务组件");
}
@Override
public String buildGenJson(String moduleName, String caption,
List<String> fields,
Map<String, Object> options) {
// 1. 创建基础属性
JSONObject properties = createBaseProperties(caption, moduleName);
// 2. 添加自定义配置
properties.put("customProp1", options.getOrDefault("prop1", "default"));
properties.put("customProp2", options.get("prop2"));
// 3. 使用模板生成
return buildModuleFromTemplate(moduleName,
properties.toJSONString(), "[]");
}
// ⭐ CLI 桥接:重写 CLI 参数解析
@Override
public Map<String, Object> parseCliArgs(String[] args) {
Map<String, Object> params = super.parseCliArgs(args);
// 自定义参数解析逻辑
if (params.containsKey("custom-prop")) {
params.put("customProp", params.get("custom-prop"));
}
return params;
}
}
步骤 2:注册到 SPI
public class MyCustomSkillRegistry implements A2uiSkillRegistry {
private static final Map<String, SkillEntry> SKILL_MAP = new HashMap<>();
static {
register("mycustom", "com.example.MyCustomSkill",
"custom", ModuleViewType.CUSTOMCONFIG, ComponentType.CUSTOM);
}
// 实现接口方法...
}
步骤 3:创建 SPI 配置文件
# src/main/resources/META-INF/services/net.ooder.annotation.spi.A2uiSkillRegistry
com.example.MyCustomSkillRegistry
10.2 技能设计原则(CLI 桥接视角)
- 单一职责:每个技能只负责一种组件类型的生成
- 无状态设计:技能类应该是无状态的,支持并发调用
- 模板复用:使用 FreeMarker 模板提高代码复用性
- 错误处理:在 buildGenJson 中做好参数校验和异常处理
- 关键词优化:提供丰富的关键词以支持 NLP 意图识别
CLI 友好:提供清晰的 CLI 命令别名和帮助文档
参数映射:CLI 参数名与 UI 参数名保持一致或提供映射
脚本生成:实现 generateCliScript 方法,支持 UI 操作回溯
10.3 性能优化建议
┌─────────────────────────────────────────────────────────────────┐
│ 性能优化策略 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 缓存策略 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ - classNameCache: 技能 ID → 类名 │ │
│ │ - classCache: 技能 ID → Class 对象 │ │
│ │ - componentTypeToSkillIdCache: 组件类型 → 技能 ID │ │
│ │ ⭐ cliCommandToSkillIdCache: CLI 命令 → 技能 ID │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 懒加载 │
│ - 技能实例在首次使用时创建 │
│ - 避免启动时加载所有技能 │
│ │
│ 3. 并发控制 │
│ - 使用 ConcurrentHashMap 保证线程安全 │
│ - 单例模式使用双重检查锁定 (DCL) │
│ │
│ 4. 模板预热 │
│ - 应用启动时预加载常用 FreeMarker 模板 │
│ - 避免首次请求时模板编译延迟 │
│ │
│ 5. ⭐ CLI 操作历史限制 │
│ - 限制历史记录数量,避免内存溢出 │
│ - 定期清理过期的历史记录 │
│ │
└─────────────────────────────────────────────────────────────────┘
10.4 调试技巧(CLI 调试)
启用详细日志:
# application.properties
logging.level.net.ooder.engine.core.skill=DEBUG
logging.level.net.ooder.annotation.spi=DEBUG
logging.level.net.ooder.web.interceptor=DEBUG
查看已注册技能(包含 CLI 命令):
curl http://localhost:8080/api/skill/health
测试 CLI 调用:
# 使用 curl 模拟 CLI 调用
curl -X POST http://localhost:8080/api/skill/build \
-H "Content-Type: application/json" \
-H "X-A2UI-CLI: true" \
-H "X-A2UI-CLI-Command: treegrid" \
-d '{
"skillId": "treegrid",
"moduleName": "TestModule",
"caption": "测试模块",
"fields": ["id", "name", "status"]
}'
查看 CLI 操作历史:
curl http://localhost:8080/api/skill/project/ProjectA/cli-history
总结
Ooder A2UI 的 Skills 架构设计通过以下几个关键点实现了框架的现代化重构,并实现了 CLI 与 UI 的无缝桥接:
- 注解驱动:使用
@A2uiSkill注解声明式定义技能元数据,包括 CLI 指令映射 - SPI 扩展:通过 Java SPI 机制实现技能的动态加载和热插拔,支持 CLI 命令注册
- 统一拦截:
SkillDrivenInterceptor替代多个硬编码拦截器,支持 CLI/UI 双端请求 - 服务分层:清晰的服务层职责划分(管理、发布、桥接),包含 CLI 脚本生成
- 模板方法:
AbstractA2uiSkill提供通用实现,包括 CLI 参数解析和脚本生成 - 项目桥接:
SkillProjectBridge实现技能与项目的松耦合关联,支持 CLI 操作追溯
Bridger 层:贯穿整个架构,实现 CLI ↔ UI 的双向桥接
这套架构不仅解决了传统拦截机制的痛点,更为未来的扩展提供了坚实的基础。通过 Bridger 层的设计,实现了:
- CLI 命令自动映射为技能调用,生成对应 UI 界面
- UI 操作可转换为 CLI 脚本,支持审计和复用
- 一套技能逻辑,双端复用,降低维护成本
- 操作历史可追溯,支持回滚和审计
通过 SPI 机制,第三方开发者可以轻松开发自定义技能,丰富 A2UI 的组件生态,同时自动获得 CLI 支持。
附录 关键术语表
术语定义
| 术语 | 英文 | 定义 |
|---|---|---|
| A2UI | Augmented & Autonomous UI | 增强型自主 UI,CLI 的可视化增强层,代表 WEB 前端技术的演进方向 |
| Bridger 层 | Bridger Layer | Ooder A2UI 中连接 CLI 命令行与自主 UI 体系的核心桥接层,实现双向同步 |
| WEB 拦截层 | WEB Interceptor Layer | A2UI 架构中承接 CLI/HTTP 请求,驱动技能扩展的核心拦截逻辑层 |
| 技能驱动 | Skill-Driven | 基于 SPI 机制,将 CLI 指令/UI 操作转化为可扩展技能的核心设计思想 |
| Skills | Skills | 可复用的 UI 组件生成单元,支持 CLI 指令和 UI 操作 |
| SPI | Service Provider Interface | Java 服务提供者接口,用于技能的动态加载和热插拔 |
| CLI 指令映射 | CLI Command Mapping | 将 CLI 命令名称映射到技能 ID 的机制 |
| CLI 脚本生成 | CLI Script Generation | 根据 UI 操作参数生成可执行的 CLI 脚本 |
| CLI 操作历史 | CLI Operation History | 记录所有 CLI 调用的历史,支持审计和回滚 |
| 注解驱动 | Annotation-Driven | 使用 Java 注解声明式定义技能元数据和行为 |
架构层次术语
| 层次 | 职责 | CLI 桥接支持 |
|---|---|---|
| Web 层 | RESTful API 入口 | 支持 CLI 调用 |
| 拦截层 | 请求路由与分发 | Bridger 层核心,CLI/UI 双端统一处理 |
| 服务层 | 业务逻辑处理 | CLI 状态管理、脚本生成 |
| SPI 层 | 技能注册与发现 | CLI 命令注册与查询 |
| 技能实现层 | 具体技能实现 | CLI 参数解析、脚本生成 |
| 注解层 | 元数据定义 | CLI 指令映射声明 |
CLI 命令格式
# 基本格式
a2ui <skill_id|cli_command> [module_name] [options]
# 示例
a2ui treegrid MyModule --caption "My Tree" --fields "id,name,status"
a2ui tg MyModule --caption "My Tree" --fields "id,name,status" # 使用别名
a2ui form MyForm --caption "My Form" --fields "name,email,phone"
a2ui chart MyChart --type "line" --title "Sales Data"
# 项目管理
a2ui project add-skill ProjectA treegrid --module MyTree
a2ui project list ProjectA
a2ui project history ProjectA
# 技能管理
a2ui list
a2ui publish treegrid --output ./published