chore: import upstream snapshot with attribution
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
# 飞书 (Lark) 接入方案
|
||||
|
||||
> 本文档记录飞书平台接入的完整开发方案,基于现有 Telegram 插件架构进行扩展。
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
### 1.1 基本信息
|
||||
|
||||
- **功能名称**: 飞书机器人接入
|
||||
- **所属模块**: Channel Plugin 层
|
||||
- **涉及进程**: 主进程 (process)
|
||||
- **运行环境**: GUI 模式(AionUi 运行中)
|
||||
- **依赖**: 现有 Channel 架构、PairingService、SessionManager
|
||||
|
||||
### 1.2 功能描述
|
||||
|
||||
1. 复用现有 Channel 插件架构,新增飞书平台支持
|
||||
2. 用户可通过飞书机器人与 AionUi 进行对话
|
||||
3. 支持 Gemini、Claude、Codex 等多 Agent 切换
|
||||
4. 与 Telegram 功能完全对齐
|
||||
|
||||
### 1.3 用户场景
|
||||
|
||||
```
|
||||
触发: 用户在飞书中 @AionBot 或私聊发送消息
|
||||
过程: 飞书机器人接收消息 → 转发给 Aion Agent → LLM 处理
|
||||
结果: 处理完成后通过飞书消息卡片推送结果给用户
|
||||
```
|
||||
|
||||
### 1.4 参考资源
|
||||
|
||||
- **飞书开放平台**: https://open.feishu.cn/
|
||||
- **Node SDK**: https://github.com/larksuite/node-sdk
|
||||
- **现有实现**: `src/channels/plugins/telegram/`
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术选型
|
||||
|
||||
### 2.1 平台对比
|
||||
|
||||
| 项目 | Telegram | 飞书 |
|
||||
| ------------ | -------------------------------- | -------------------------- |
|
||||
| **Bot 库** | grammY | @larksuiteoapi/node-sdk |
|
||||
| **运行模式** | Polling / Webhook | WebSocket 长连接 / Webhook |
|
||||
| **认证方式** | Bot Token | App ID + App Secret |
|
||||
| **交互组件** | Inline Keyboard + Reply Keyboard | 消息卡片 (Message Card) |
|
||||
| **消息格式** | Markdown / HTML | 富文本 / 消息卡片 JSON |
|
||||
| **流式更新** | editMessageText | PATCH /im/v1/messages/:id |
|
||||
|
||||
### 2.2 技术选择
|
||||
|
||||
| 项目 | 选择 | 说明 |
|
||||
| -------- | ----------------------- | -------------------------- |
|
||||
| SDK | @larksuiteoapi/node-sdk | 官方 Node.js SDK |
|
||||
| 运行模式 | WebSocket (优先) | 无需公网地址,适合桌面应用 |
|
||||
| 消息格式 | 消息卡片 | 支持富文本和交互按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置流程
|
||||
|
||||
### 3.1 飞书应用创建
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Step 1: 创建应用 │
|
||||
│ 飞书开放平台 → 创建企业自建应用 → 获取 App ID/Secret │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Step 2: 开启机器人能力 │
|
||||
│ 应用能力 → 机器人 → 开启 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Step 3: 配置权限 │
|
||||
│ 权限管理 → 添加以下权限: │
|
||||
│ • im:message (获取与发送单聊、群组消息) │
|
||||
│ • im:message.group_at_msg (接收群聊@机器人消息) │
|
||||
│ • im:chat (获取群组信息) │
|
||||
│ • contact:user.id:readonly (获取用户 ID) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Step 4: 发布应用 │
|
||||
│ 版本管理与发布 → 创建版本 → 申请发布 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Step 5: 配置 AionUi │
|
||||
│ 设置 → Channels → 飞书 → 粘贴 App ID/Secret → 启动 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 配置项
|
||||
|
||||
| 配置项 | 类型 | 说明 | 必填 |
|
||||
| ----------- | -------------------- | ------------------- | :--: |
|
||||
| App ID | string | 飞书应用 ID | ✅ |
|
||||
| App Secret | string | 飞书应用密钥 | ✅ |
|
||||
| 运行模式 | websocket / webhook | 事件接收模式 | ✅ |
|
||||
| Webhook URL | string | 仅 webhook 模式需要 | ❌ |
|
||||
| 配对模式 | boolean | 是否需要配对码授权 | ✅ |
|
||||
| 速率限制 | number | 每分钟最大消息数 | ❌ |
|
||||
| 默认 Agent | gemini / acp / codex | 默认使用的 Agent | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 4. 配对安全机制
|
||||
|
||||
### 4.1 流程设计(与 Telegram 一致)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ① 用户在飞书中发起 │
|
||||
│ 用户 → @AionBot: 任意消息 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ② Bot 返回配对请求(消息卡片) │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ 👋 欢迎使用 Aion 助手! │ │
|
||||
│ │ │ │
|
||||
│ │ 🔑 配对码: ABC123 │ │
|
||||
│ │ 请在 AionUi 中批准此配对 │ │
|
||||
│ │ │ │
|
||||
│ │ [📖 使用指南] [🔄 刷新状态] │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ③ AionUi 显示待批准请求 │
|
||||
│ 设置页面展示: 用户名、配对码、请求时间、[批准]/[拒绝] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ④ 用户在 AionUi 点击 [批准] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ⑤ Bot 推送配对成功消息 │
|
||||
│ Bot → 用户: "✅ 配对成功!现在可以开始对话了" │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 安全措施
|
||||
|
||||
| 机制 | 说明 |
|
||||
| ------------ | ------------------------------ |
|
||||
| 配对码认证 | 6位随机码,10分钟有效 |
|
||||
| 本地批准 | 必须在 AionUi 中批准,非飞书中 |
|
||||
| 用户白名单 | 仅授权用户可使用 |
|
||||
| 速率限制 | 防止滥用 |
|
||||
| 凭证加密存储 | 使用加密存储 App Secret |
|
||||
|
||||
---
|
||||
|
||||
## 5. 消息转换规则
|
||||
|
||||
### 5.1 入站转换(飞书 → 统一格式)
|
||||
|
||||
| 飞书事件类型 | 统一消息 content.type |
|
||||
| ------------------------------- | --------------------- |
|
||||
| `im.message.receive_v1` (text) | `text` |
|
||||
| `im.message.receive_v1` (image) | `image` |
|
||||
| `im.message.receive_v1` (file) | `file` |
|
||||
| `im.message.receive_v1` (audio) | `audio` |
|
||||
| `card.action.trigger` | `action` |
|
||||
|
||||
### 5.2 出站转换(统一格式 → 飞书)
|
||||
|
||||
| 统一消息 type | 飞书 API | content_type |
|
||||
| ------------- | ------------------------- | ------------ |
|
||||
| `text` | POST /im/v1/messages | text |
|
||||
| `image` | POST /im/v1/messages | image |
|
||||
| `buttons` | POST /im/v1/messages | interactive |
|
||||
| 流式更新 | PATCH /im/v1/messages/:id | - |
|
||||
|
||||
### 5.3 消息卡片结构
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"wide_screen_mode": true
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "Aion 助手"
|
||||
}
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "消息内容..."
|
||||
},
|
||||
{
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": { "tag": "plain_text", "content": "🆕 新对话" },
|
||||
"type": "primary",
|
||||
"value": { "action": "session.new" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 交互设计
|
||||
|
||||
### 6.1 组件映射
|
||||
|
||||
| 场景 | Telegram | 飞书 |
|
||||
| ---------------- | -------------------- | ------------------ |
|
||||
| **常驻快捷操作** | Reply Keyboard | 消息卡片底部按钮组 |
|
||||
| **消息操作按钮** | Inline Keyboard | 消息卡片交互按钮 |
|
||||
| **配对请求** | 文本 + 按钮 | 消息卡片 |
|
||||
| **AI 回复** | Markdown + 按钮 | 富文本/卡片 + 按钮 |
|
||||
| **设置菜单** | 多级 Inline Keyboard | 消息卡片 |
|
||||
|
||||
### 6.2 交互场景
|
||||
|
||||
**场景 1: 配对成功后的主菜单**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 消息卡片 (Message Card) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ✅ 配对成功!现在可以开始对话了 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ [🆕 新对话] [🔄 Agent] [📊 状态] [❓ 帮助] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**场景 2: AI 回复带操作按钮**
|
||||
|
||||
````
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 消息卡片 (Message Card) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 这是一个快速排序的实现: │
|
||||
│ │
|
||||
│ ```python │
|
||||
│ def quicksort(arr): │
|
||||
│ if len(arr) <= 1: │
|
||||
│ return arr │
|
||||
│ ... │
|
||||
│ ``` │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ [📋 复制] [🔄 重新生成] [💬 继续] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
````
|
||||
|
||||
**场景 3: Agent 切换**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 消息卡片 (Message Card) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 🔄 切换 Agent │
|
||||
│ │
|
||||
│ 选择一个 AI Agent: │
|
||||
│ 当前: 🤖 Gemini │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ [✓ 🤖 Gemini] [🧠 Claude] [⚡ Codex] │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 文件结构
|
||||
|
||||
```
|
||||
src/channels/
|
||||
├── plugins/
|
||||
│ ├── telegram/ # 现有 Telegram 插件
|
||||
│ │ ├── TelegramPlugin.ts
|
||||
│ │ ├── TelegramAdapter.ts
|
||||
│ │ ├── TelegramKeyboards.ts
|
||||
│ │ └── index.ts
|
||||
│ │
|
||||
│ └── lark/ # 新增飞书插件
|
||||
│ ├── LarkPlugin.ts # 飞书插件主类
|
||||
│ ├── LarkAdapter.ts # 消息格式转换
|
||||
│ ├── LarkCards.ts # 消息卡片模板
|
||||
│ └── index.ts
|
||||
│
|
||||
├── types.ts # 需要新增 'lark' 到 PluginType
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 接口设计
|
||||
|
||||
### 8.1 LarkPlugin 类
|
||||
|
||||
```typescript
|
||||
class LarkPlugin extends BasePlugin {
|
||||
// 生命周期
|
||||
async initialize(config: LarkPluginConfig): Promise<void>;
|
||||
async start(): Promise<void>;
|
||||
async stop(): Promise<void>;
|
||||
|
||||
// 消息处理
|
||||
async sendMessage(chatId: string, message: IUnifiedOutgoingMessage): Promise<string>;
|
||||
async editMessage(chatId: string, messageId: string, message: IUnifiedOutgoingMessage): Promise<void>;
|
||||
|
||||
// 事件处理
|
||||
private handleMessageEvent(event: LarkMessageEvent): void;
|
||||
private handleCardAction(action: LarkCardAction): void;
|
||||
|
||||
// Token 管理
|
||||
private async refreshAccessToken(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 配置接口
|
||||
|
||||
```typescript
|
||||
interface LarkPluginConfig {
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
mode: 'websocket' | 'webhook';
|
||||
webhookUrl?: string;
|
||||
encryptKey?: string; // 事件加密密钥
|
||||
verificationToken?: string; // 事件验证令牌
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 飞书特有注意事项
|
||||
|
||||
| 项目 | 说明 |
|
||||
| ---------------- | ------------------------------------------ |
|
||||
| **应用类型** | 建议使用企业自建应用,个人开发者有功能限制 |
|
||||
| **权限审核** | 部分权限需要管理员审批 |
|
||||
| **消息卡片限制** | 卡片 JSON 最大 30KB,需要分片处理长消息 |
|
||||
| **Token 刷新** | Access Token 有效期 2 小时,需要自动刷新 |
|
||||
| **事件订阅** | WebSocket 模式无需公网地址,更适合桌面应用 |
|
||||
| **@提及** | 群聊中需要 @机器人 才会收到消息 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发计划
|
||||
|
||||
### Phase 1: 基础连接 (预计 2-3 天)
|
||||
|
||||
- [ ] 创建 LarkPlugin 基类
|
||||
- [ ] 实现 WebSocket 事件接收
|
||||
- [ ] 实现 Access Token 自动刷新
|
||||
- [ ] 基础消息收发功能
|
||||
|
||||
### Phase 2: 安全认证 (预计 1-2 天)
|
||||
|
||||
- [ ] 复用 PairingService
|
||||
- [ ] 配对流程消息卡片
|
||||
- [ ] 设置页面 UI 适配
|
||||
|
||||
### Phase 3: 交互完善 (预计 2-3 天)
|
||||
|
||||
- [ ] 消息卡片模板系统
|
||||
- [ ] 按钮回调处理
|
||||
- [ ] Agent 切换功能
|
||||
- [ ] 流式响应支持
|
||||
|
||||
### Phase 4: 优化 (预计 1-2 天)
|
||||
|
||||
- [ ] 长消息分片处理
|
||||
- [ ] 错误处理完善
|
||||
- [ ] 多语言支持
|
||||
- [ ] 日志与监控
|
||||
|
||||
---
|
||||
|
||||
## 11. 功能对齐清单
|
||||
|
||||
| 功能 | Telegram | 飞书 | 复用组件 |
|
||||
| ------------- | :------: | :--: | --------------------- |
|
||||
| Bot 配置验证 | ✅ | 🔲 | - |
|
||||
| Bot 启动/停止 | ✅ | 🔲 | ChannelManager |
|
||||
| 配对码认证 | ✅ | 🔲 | PairingService |
|
||||
| 本地批准流程 | ✅ | 🔲 | 现有 UI |
|
||||
| 用户白名单 | ✅ | 🔲 | Database |
|
||||
| 按钮交互 | ✅ | 🔲 | SystemActions |
|
||||
| 流式响应 | ✅ | 🔲 | ChannelMessageService |
|
||||
| Agent 切换 | ✅ | 🔲 | SystemActions |
|
||||
| 新建会话 | ✅ | 🔲 | SessionManager |
|
||||
| 速率限制 | ✅ | 🔲 | RateLimiter |
|
||||
|
||||
---
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
### 12.1 功能验收
|
||||
|
||||
- [ ] 飞书应用凭证配置和验证
|
||||
- [ ] Bot 启动/停止控制
|
||||
- [ ] 配对码生成和本地批准流程
|
||||
- [ ] 已授权用户管理
|
||||
- [ ] 消息卡片交互
|
||||
- [ ] 与 Gemini/Claude Agent 对话
|
||||
- [ ] Agent 切换功能
|
||||
- [ ] 新建会话功能
|
||||
- [ ] 流式消息响应
|
||||
|
||||
### 12.2 安全验收
|
||||
|
||||
- [ ] 配对码 10 分钟过期
|
||||
- [ ] 必须在 AionUi 本地批准
|
||||
- [ ] 未授权用户无法使用
|
||||
- [ ] App Secret 加密存储
|
||||
- [ ] 速率限制生效
|
||||
|
||||
### 12.3 兼容性
|
||||
|
||||
- [ ] macOS 正常运行
|
||||
- [ ] Windows 正常运行
|
||||
- [ ] 多语言支持
|
||||
|
||||
---
|
||||
|
||||
## 模板维护
|
||||
|
||||
- **创建日期**: 2026-01-30
|
||||
- **最后更新**: 2026-01-30
|
||||
- **适用版本**: AionUi v0.x+
|
||||
- **维护者**: 项目团队
|
||||
@@ -0,0 +1,366 @@
|
||||
# AionUi 功能开发规范模板
|
||||
|
||||
> 本模板用于规范化向 AI 描述功能开发需求,确保 AI 能够准确理解任务并遵循项目约定。
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
### 1.1 基本信息
|
||||
|
||||
- **功能名称**: [简洁命名]
|
||||
- **所属模块**: [ ] Agent层 [ ] 对话系统 [ ] 预览系统 [ ] 设置系统 [ ] 工作区 [ ] 其他
|
||||
- **涉及进程**: [ ] 主进程(process) [ ] 渲染进程(renderer) [ ] WebServer [ ] Worker
|
||||
|
||||
### 1.2 功能描述
|
||||
|
||||
[用 1-3 句话描述功能的核心目的和价值]
|
||||
|
||||
### 1.3 用户场景
|
||||
|
||||
```
|
||||
触发: [用户如何触发此功能]
|
||||
过程: [系统如何响应]
|
||||
结果: [功能完成后的状态]
|
||||
```
|
||||
|
||||
### 1.4 数据流
|
||||
|
||||
| 方向 | 数据类型 | 说明 |
|
||||
| ---- | -------- | ---- |
|
||||
| 输入 | | |
|
||||
| 输出 | | |
|
||||
|
||||
---
|
||||
|
||||
## 2. 开发规范
|
||||
|
||||
### 2.1 技术栈约束
|
||||
|
||||
- **框架**: Electron 37 + React 19 + TypeScript 5.8
|
||||
- **UI库**: Arco Design (@arco-design/web-react)
|
||||
- **图标**: Icon Park (@icon-park/react)
|
||||
- **CSS**: UnoCSS 原子化样式
|
||||
- **状态管理**: React Context (AuthContext / ConversationContext / ThemeContext / LayoutContext)
|
||||
- **IPC通信**: @office-ai/platform bridge 系统
|
||||
- **国际化**: i18next + react-i18next
|
||||
- **数据库**: better-sqlite3
|
||||
|
||||
### 2.2 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
| ------------ | --------------------- | ----------------------------------------------- |
|
||||
| React 组件 | PascalCase | `MessageList.tsx`, `FilePreview.tsx` |
|
||||
| Hooks | use 前缀 + PascalCase | `useAutoScroll.ts`, `useColorScheme.ts` |
|
||||
| Bridge 文件 | 功能名 + Bridge | `conversationBridge.ts`, `databaseBridge.ts` |
|
||||
| Service 文件 | 功能名 + Service | `WebuiService.ts` |
|
||||
| 接口类型 | I 前缀 | `ICreateConversationParams`, `IResponseMessage` |
|
||||
| 类型别名 | T 前缀或直接命名 | `TChatConversation`, `PresetAgentType` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 工具函数 | camelCase | `formatMessage`, `parseResponse` |
|
||||
|
||||
### 2.3 文件位置规范
|
||||
|
||||
```
|
||||
新增文件应放置于对应目录:
|
||||
|
||||
src/
|
||||
├── agent/ # AI 代理实现
|
||||
│ ├── acp/ # ACP 协议代理
|
||||
│ ├── codex/ # Codex 代理
|
||||
│ └── gemini/ # Gemini 代理
|
||||
│
|
||||
├── common/ # 跨进程共享模块
|
||||
│ ├── adapters/ # API 适配器
|
||||
│ ├── types/ # 共享类型定义
|
||||
│ └── utils/ # 共享工具函数
|
||||
│
|
||||
├── process/ # Electron 主进程
|
||||
│ ├── bridge/ # IPC 桥接定义 (24+ 个)
|
||||
│ ├── database/ # SQLite 数据库操作
|
||||
│ ├── services/ # 业务逻辑服务
|
||||
│ └── task/ # 任务管理
|
||||
│
|
||||
├── renderer/ # React 渲染进程
|
||||
│ ├── components/ # 可复用 UI 组件
|
||||
│ │ └── base/ # 基础组件
|
||||
│ ├── context/ # React Context 状态
|
||||
│ ├── hooks/ # 自定义 Hooks (31+)
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── conversation/ # 对话页面
|
||||
│ │ │ ├── preview/ # 预览面板
|
||||
│ │ │ └── workspace/ # 工作区
|
||||
│ │ ├── settings/ # 设置页面 (12+)
|
||||
│ │ └── login/ # 登录页面
|
||||
│ ├── messages/ # 消息渲染组件
|
||||
│ ├── i18n/locales/ # 国际化文本
|
||||
│ ├── services/ # 前端服务
|
||||
│ └── utils/ # 前端工具函数
|
||||
│
|
||||
├── webserver/ # Web 服务器 (WebUI 模式)
|
||||
│ ├── routes/ # API 路由
|
||||
│ └── middleware/ # 中间件
|
||||
│
|
||||
├── worker/ # Web Worker
|
||||
│
|
||||
└── types/ # 全局类型定义
|
||||
```
|
||||
|
||||
### 2.4 代码风格 (Prettier 配置)
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": true, // 使用分号
|
||||
"singleQuote": true, // 使用单引号
|
||||
"jsxSingleQuote": true, // JSX 使用单引号
|
||||
"trailingComma": "es5", // ES5 兼容的尾随逗号
|
||||
"tabWidth": 2, // 2 空格缩进
|
||||
"useTabs": false, // 不使用 Tab
|
||||
"bracketSpacing": true, // 括号内空格
|
||||
"arrowParens": "always", // 箭头函数始终括号
|
||||
"endOfLine": "lf" // Unix 换行符
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 质量要求
|
||||
|
||||
- [ ] TypeScript 类型完整,避免使用 `any`
|
||||
- [ ] 使用 bridge 系统进行 IPC 通信
|
||||
- [ ] 实现错误边界处理
|
||||
- [ ] 支持国际化 (使用 i18next 的 `t()` 函数)
|
||||
- [ ] 深色/浅色主题兼容
|
||||
- [ ] 响应式布局适配
|
||||
|
||||
### 2.6 禁止事项
|
||||
|
||||
- ❌ 直接使用 `ipcMain` / `ipcRenderer`,必须通过 bridge 系统
|
||||
- ❌ 在渲染进程直接访问 Node.js API
|
||||
- ❌ 硬编码中文/英文文本,需使用 i18n key
|
||||
- ❌ 使用内联样式,应使用 UnoCSS 类名
|
||||
- ❌ 在组件中直接操作 DOM,使用 React ref
|
||||
- ❌ 忽略 TypeScript 错误 (`@ts-ignore`)
|
||||
|
||||
---
|
||||
|
||||
## 3. 实现架构
|
||||
|
||||
### 3.1 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 用户界面 (UI) │
|
||||
│ React 组件 / Hooks / Context │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│ IPC Bridge
|
||||
┌─────────────────────▼───────────────────────────────────┐
|
||||
│ 主进程 (Main) │
|
||||
│ Bridge → Service → Database / External API │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────▼───────────────────────────────────┐
|
||||
│ 数据层 (Data) │
|
||||
│ SQLite / LocalStorage / External Services │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 需要修改/新增的文件
|
||||
|
||||
**主进程 (src/process/)**
|
||||
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
| -------- | ------------------- | ---- |
|
||||
| | [ ] 新增 / [ ] 修改 | |
|
||||
|
||||
**渲染进程 (src/renderer/)**
|
||||
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
| -------- | ------------------- | ---- |
|
||||
| | [ ] 新增 / [ ] 修改 | |
|
||||
|
||||
**共享模块 (src/common/)**
|
||||
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
| -------- | ------------------- | ---- |
|
||||
| | [ ] 新增 / [ ] 修改 | |
|
||||
|
||||
**类型定义 (src/types/)**
|
||||
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
| -------- | ------------------- | ---- |
|
||||
| | [ ] 新增 / [ ] 修改 | |
|
||||
|
||||
### 3.3 IPC 通信设计
|
||||
|
||||
如需新增 IPC 通道,遵循以下模式:
|
||||
|
||||
```typescript
|
||||
// src/process/bridge/[功能]Bridge.ts
|
||||
import { bridge } from '@anthropic/platform';
|
||||
|
||||
export const [功能名] = {
|
||||
// Provider 模式: 请求-响应 (类似 HTTP 请求)
|
||||
[方法名]: bridge.buildProvider<TResponse, TParams>('[通道名]'),
|
||||
|
||||
// Emitter 模式: 事件流 (用于流式数据)
|
||||
[事件名]: bridge.buildEmitter<TData>('[通道名].stream'),
|
||||
};
|
||||
|
||||
// 使用示例:
|
||||
// 渲染进程调用: const result = await [功能名].[方法名].request(params);
|
||||
// 渲染进程监听: [功能名].[事件名].on((data) => { ... });
|
||||
```
|
||||
|
||||
### 3.4 状态管理设计
|
||||
|
||||
- [ ] 使用现有 Context: **\*\*\*\***\_\_\_\_**\*\*\*\***
|
||||
- [ ] 需要新增 Context: **\*\*\*\***\_\_\_\_**\*\*\*\***
|
||||
- [ ] 仅组件内部状态 (useState/useReducer)
|
||||
- [ ] 需要持久化存储
|
||||
|
||||
### 3.5 国际化 Key 设计
|
||||
|
||||
```json
|
||||
// 添加到 src/renderer/i18n/locales/[lang].json
|
||||
// Key 命名规范: [模块].[功能].[描述]
|
||||
|
||||
{
|
||||
"conversation.export.title": "导出对话",
|
||||
"conversation.export.success": "导出成功",
|
||||
"conversation.export.error": "导出失败"
|
||||
}
|
||||
```
|
||||
|
||||
**支持的语言文件:**
|
||||
|
||||
- `zh-CN.json` - 简体中文 (必须)
|
||||
- `en-US.json` - English (必须)
|
||||
- `zh-TW.json` - 繁體中文
|
||||
- `ja-JP.json` - 日本語
|
||||
- `ko-KR.json` - 한국어
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
### 4.1 功能验收
|
||||
|
||||
- [ ] [具体功能点 1]
|
||||
- [ ] [具体功能点 2]
|
||||
- [ ] [具体功能点 3]
|
||||
|
||||
### 4.2 边界情况
|
||||
|
||||
- [ ] [异常场景 1 的处理]
|
||||
- [ ] [异常场景 2 的处理]
|
||||
|
||||
### 4.3 兼容性验收
|
||||
|
||||
- [ ] macOS 正常运行
|
||||
- [ ] Windows 正常运行
|
||||
- [ ] 深色模式显示正确
|
||||
- [ ] 浅色模式显示正确
|
||||
- [ ] 多语言切换正常
|
||||
|
||||
### 4.4 代码质量
|
||||
|
||||
- [ ] `npm run lint` 无错误
|
||||
- [ ] `npm run build` 构建成功
|
||||
- [ ] TypeScript 无类型错误
|
||||
- [ ] 无 console.log 遗留
|
||||
|
||||
---
|
||||
|
||||
## 5. 参考资料
|
||||
|
||||
### 5.1 类似功能参考
|
||||
|
||||
[列出项目中可参考的类似实现]
|
||||
|
||||
| 功能 | 文件路径 | 说明 |
|
||||
| ---- | -------- | ---- |
|
||||
| | | |
|
||||
|
||||
### 5.2 依赖的现有模块
|
||||
|
||||
[列出需要调用的现有接口/组件/Hook]
|
||||
|
||||
| 模块 | 路径 | 用途 |
|
||||
| ---- | ---- | ---- |
|
||||
| | | |
|
||||
|
||||
### 5.3 外部依赖
|
||||
|
||||
[如需引入新依赖,列出并说明理由]
|
||||
|
||||
| 依赖包 | 版本 | 用途 | 必要性说明 |
|
||||
| ------ | ---- | ---- | ---------- |
|
||||
| | | | |
|
||||
|
||||
### 5.4 特殊注意事项
|
||||
|
||||
[列出实现过程中需要特别注意的事项]
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
以下是一个完整的功能需求示例:
|
||||
|
||||
```markdown
|
||||
## 1. 功能概述
|
||||
|
||||
### 1.1 基本信息
|
||||
|
||||
- **功能名称**: 对话导出 PDF
|
||||
- **所属模块**: [x] 对话系统
|
||||
- **涉及进程**: [x] 主进程(process) [x] 渲染进程(renderer)
|
||||
|
||||
### 1.2 功能描述
|
||||
|
||||
允许用户将当前对话导出为 PDF 文件,保留消息格式、代码高亮和图片。
|
||||
|
||||
### 1.3 用户场景
|
||||
|
||||
触发: 用户点击对话页面右上角的"导出"按钮,选择"导出为 PDF"
|
||||
过程: 系统收集对话内容,渲染为 HTML,转换为 PDF
|
||||
结果: 弹出保存对话框,用户选择保存位置后生成 PDF 文件
|
||||
|
||||
### 3.2 需要修改/新增的文件
|
||||
|
||||
**主进程 (src/process/)**
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
|----------|------|------|
|
||||
| src/process/bridge/exportBridge.ts | [x] 新增 | PDF 导出 IPC 通道定义 |
|
||||
| src/process/services/ExportService.ts | [x] 新增 | PDF 生成逻辑 |
|
||||
|
||||
**渲染进程 (src/renderer/)**
|
||||
| 文件路径 | 操作 | 说明 |
|
||||
|----------|------|------|
|
||||
| src/renderer/pages/conversation/components/ChatHeader.tsx | [x] 修改 | 添加导出下拉菜单 |
|
||||
| src/renderer/hooks/useExportPdf.ts | [x] 新增 | 导出功能 Hook |
|
||||
|
||||
### 4.1 功能验收
|
||||
|
||||
- [ ] 点击导出按钮显示导出选项菜单
|
||||
- [ ] 选择 PDF 后弹出保存对话框
|
||||
- [ ] 生成的 PDF 包含完整对话内容
|
||||
- [ ] 代码块保留语法高亮样式
|
||||
- [ ] 图片正确嵌入 PDF
|
||||
|
||||
### 5.1 类似功能参考
|
||||
|
||||
| 功能 | 文件路径 | 说明 |
|
||||
| ------------- | ----------------------------------------------------- | --------------- |
|
||||
| Markdown 导出 | src/renderer/hooks/useExportMarkdown.ts | 可参考导出流程 |
|
||||
| PDF 预览 | src/renderer/pages/conversation/preview/PdfViewer.tsx | 可参考 PDF 处理 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模板维护
|
||||
|
||||
- **创建日期**: 2025-01-27
|
||||
- **适用版本**: AionUi v0.x+
|
||||
- **维护者**: [项目团队]
|
||||
|
||||
如需更新模板,请同步修改本文件并通知团队成员。
|
||||
@@ -0,0 +1,174 @@
|
||||
# Package OfficeCLI Skill as AionUi Assistant
|
||||
|
||||
Convert an OfficeCLI skill into a fully wired AionUi assistant preset, or update an existing one.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/package-assistant <skill-name> [assistant-id] [avatar]
|
||||
```
|
||||
|
||||
- `<skill-name>` — Name of the skill directory under `/Users/veryliu/Documents/GitHub/officecli/skills/` (e.g. `officecli-docx`, `officecli-pptx`, `officecli-xlsx`, `officecli-data-dashboard`)
|
||||
- `[assistant-id]` — Optional. ID for the new assistant (defaults to `<skill-name>-creator`)
|
||||
- `[avatar]` — Optional. Emoji avatar (defaults to auto-selected based on skill type)
|
||||
|
||||
Arguments: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Before You Start — Detect Mode
|
||||
|
||||
Before doing anything, check whether this skill has already been packaged:
|
||||
|
||||
1. Check if `aionui/src/process/resources/skills/<skill-name>/` exists
|
||||
2. Check if the assistant-id already appears in `assistantPresets.ts`
|
||||
|
||||
- **Both exist** → **Update mode**: Only re-copy skill files (Step 1). Skip Steps 2–4.
|
||||
- **Neither exists** → **New mode**: Run all steps (1–5).
|
||||
- **Skill exists but assistant doesn't** (or vice versa) → Run whichever steps are missing.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Copy Skill Files
|
||||
|
||||
1. Read all files from `officecli/skills/<skill-name>/` (SKILL.md, creating.md, editing.md, reference/, etc.)
|
||||
2. Create target directory at `aionui/src/process/resources/skills/<skill-name>/` — **directory name must be identical** to the officecli source directory name (both repos use the same `officecli-xxx` naming convention)
|
||||
3. Copy all files, but apply these transformations to SKILL.md:
|
||||
- **Remove version comments**: Delete any `# officecli: vX.X.X` line from inside the frontmatter. AionUi's frontmatter parser (`/^---\s*\n([\s\S]*?)\n---/`) requires clean YAML — version tracking comments break parsing and skills won't appear in the Skills Center.
|
||||
- **Verify `name` field matches directory name**: The `name` field in SKILL.md frontmatter **MUST match the skill directory name exactly**. Both officecli and aionui use the same directory name (e.g. `officecli-docx`), so the `name` field should already be correct. If not, fix it. Mismatches cause: `params/name must be equal to one of the allowed values`.
|
||||
- **Keep frontmatter fields**: `name` and `description` must stay intact
|
||||
- **Keep BEFORE YOU START section**: The officecli install/update check section must be preserved — it's critical for users who don't have officecli installed
|
||||
4. Copy creating.md, editing.md, and any other files (reference/ directories, etc.) as-is
|
||||
|
||||
## Step 2 — Research the Skill (New Mode Only)
|
||||
|
||||
Before writing descriptions and prompts, **study the skill thoroughly**:
|
||||
|
||||
1. **Read the full skill content**: SKILL.md, creating.md, editing.md — understand what it uniquely does
|
||||
2. **Identify differentiators**: What makes this skill different from other similar assistants already in aionui? The `descriptionI18n` must clearly communicate this so users can tell assistants apart at a glance
|
||||
3. **Mine iteration test results**: Check `/Users/veryliu/Documents/GitHub/officecli/iterations/` for test reports related to this skill:
|
||||
- Look for subdirectories matching the skill name (e.g. `data-dashboard/`, `xlsx/`, etc.) or related keywords
|
||||
- Find documents/prompts that scored highest or produced the best results
|
||||
- High-scoring test documents = prompts that are proven to produce good output → use these as `promptsI18n`
|
||||
- If no exact match, check parent directories or README files for pointers
|
||||
4. **Understand the output**: What does the skill produce? What does a "great" result look like?
|
||||
|
||||
## Step 3 — Create Assistant Rule Files (New Mode Only)
|
||||
|
||||
Create two rule files in `aionui/src/process/resources/assistant/<assistant-id>/`:
|
||||
|
||||
**`<assistant-id>.md`** (English):
|
||||
|
||||
```markdown
|
||||
# <Assistant Display Name>
|
||||
|
||||
You are **<Display Name>** — an AI assistant that <one-line capability summary>.
|
||||
|
||||
## When the user greets you or asks what you can do
|
||||
|
||||
Introduce yourself briefly:
|
||||
|
||||
> <2-3 sentence self-introduction. Mention key capabilities, invite collaboration, acknowledge limitations honestly.>
|
||||
|
||||
Then wait for the user's request.
|
||||
|
||||
## When the user wants to <primary action>
|
||||
|
||||
Follow the `officecli-<skill-name>` skill exactly. It contains the complete workflow. Do not deviate from or simplify the skill's instructions.
|
||||
|
||||
Before work starts, proactively remind the user once:
|
||||
|
||||
> After the file appears in the workspace, you can preview it directly in AionUi. However, please do not click "Open with system app" while I'm still working, as this may lock the file and cause the operation to fail.
|
||||
|
||||
After work completes, explicitly tell the user:
|
||||
|
||||
> Your <output type> is ready. Please open it now to review.
|
||||
```
|
||||
|
||||
**`<assistant-id>.zh-CN.md`** (Chinese):
|
||||
Same structure translated to natural Chinese. Keep the tone friendly and professional, not overly formal.
|
||||
|
||||
## Step 4 — Add Preset Entry (New Mode Only)
|
||||
|
||||
Add a new entry to `ASSISTANT_PRESETS` array in `aionui/src/common/config/presets/assistantPresets.ts`.
|
||||
|
||||
**Placement**: New officecli-based assistants go at the **top** of the array (after morph-ppt).
|
||||
|
||||
**Entry structure**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: '<assistant-id>',
|
||||
avatar: '<emoji>',
|
||||
presetAgentType: 'gemini',
|
||||
resourceDir: 'src/process/resources/assistant/<assistant-id>',
|
||||
ruleFiles: {
|
||||
'en-US': '<assistant-id>.md',
|
||||
'zh-CN': '<assistant-id>.zh-CN.md',
|
||||
},
|
||||
defaultEnabledSkills: ['<skill-name>'],
|
||||
nameI18n: {
|
||||
'en-US': '<English Name>',
|
||||
'zh-CN': '<Chinese Name>',
|
||||
},
|
||||
descriptionI18n: {
|
||||
'en-US': '<English description — MUST clearly differentiate from other assistants>',
|
||||
'zh-CN': '<Chinese description — same differentiation requirement>',
|
||||
},
|
||||
promptsI18n: {
|
||||
'en-US': [
|
||||
'<3 example prompts — prefer high-scoring prompts from iteration tests>',
|
||||
],
|
||||
'zh-CN': [
|
||||
'<3 Chinese example prompts — translated from the same high-scoring sources>',
|
||||
],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Description writing rules**:
|
||||
|
||||
- Users see multiple assistants side by side and decide which to use based on descriptions
|
||||
- Each description must answer: "Why would I use THIS assistant instead of another?"
|
||||
- Mention the specific output type, use case, and unique strength
|
||||
- Avoid generic phrases like "professional documents" — be specific about what kind
|
||||
|
||||
**Prompt selection rules**:
|
||||
|
||||
- First priority: High-scoring prompts from `/Users/veryliu/Documents/GitHub/officecli/iterations/`
|
||||
- These are battle-tested prompts that are proven to produce good results with the skill
|
||||
- If no iteration data exists, write prompts that showcase the skill's unique strengths
|
||||
- Prompts should be diverse (different use cases) and practical (things real users would ask)
|
||||
- **CRITICAL: Prompts must be self-contained** — the user should be able to click the prompt and get a complete result without needing to provide any data, files, or attachments. Bad: "I have a CSV file, build a dashboard from it". Good: "Create a SaaS MRR dashboard with 12 months of sample data showing growth trends and churn breakdown". The assistant should generate sample data or pick a topic on its own.
|
||||
|
||||
## Step 5 — Verify
|
||||
|
||||
1. Confirm SKILL.md frontmatter starts with `---` on line 1 (no content before it)
|
||||
2. Confirm frontmatter contains `name:` and `description:` fields
|
||||
3. Confirm no `# officecli:` version comment remains in the frontmatter
|
||||
4. Confirm assistant rule files exist in both en-US and zh-CN (new mode only)
|
||||
5. Confirm preset entry is added to assistantPresets.ts with correct paths (new mode only)
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The `_builtin/office-cli` skill already handles officecli discovery, but each skill's "BEFORE YOU START" section provides skill-specific install guidance — keep both.
|
||||
- Avatar selection guide: 📝 for docx/word, 📊 for pptx/slides, 📈 for xlsx/excel, 📉 for dashboards/data, ✨ for morph/animation
|
||||
- All assistants use `presetAgentType: 'gemini'` as default
|
||||
- Both officecli and aionui use the same `officecli-xxx` directory naming convention. The skill directory name, SKILL.md `name` field, and `defaultEnabledSkills` entry must all be identical (e.g. `officecli-docx`). Exception: `morph-ppt` does not use the prefix.
|
||||
|
||||
## Existing Assistants Reference
|
||||
|
||||
When packaging a new assistant, review existing officecli-based assistants to avoid description overlap:
|
||||
|
||||
| ID | Skill | Focus |
|
||||
| ----------------- | ------------------------ | ----------------------------------------------------- |
|
||||
| morph-ppt | morph-ppt | Morph-animated presentations with visual styles |
|
||||
| word-creator | officecli-docx | Word documents — reports, proposals, letters |
|
||||
| academic-paper | officecli-academic-paper | Formal academic papers — TOC, equations, bibliography |
|
||||
| ppt-creator | officecli-pptx | General PPT creation, editing, analysis |
|
||||
| excel-creator | officecli-xlsx | Excel — financial models, trackers, formulas |
|
||||
| dashboard-creator | officecli-data-dashboard | CSV → Excel dashboards with KPI, charts, auto-scaling |
|
||||
|
||||
Update this table when adding new assistants.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: architecture
|
||||
description: |
|
||||
Project architecture and file structure conventions for all process types.
|
||||
Use when: (1) Creating new files or modules, (2) Deciding where code should go,
|
||||
(3) Converting single-file components to directories, (4) Reviewing code for structure compliance,
|
||||
(5) Adding new bridges, services, agents, or workers.
|
||||
---
|
||||
|
||||
# Architecture Skill
|
||||
|
||||
Determine correct file placement and structure for an Electron multi-process project.
|
||||
|
||||
## Detailed References
|
||||
|
||||
- **Renderer layer** (components, hooks, utils, pages, CSS): [references/renderer.md](references/renderer.md)
|
||||
- **Main process & shared layer** (bridges, services, worker, preload): [references/process.md](references/process.md)
|
||||
- **Project root & monorepo layout** (directory structure, migration status): [references/project-layout.md](references/project-layout.md)
|
||||
|
||||
---
|
||||
|
||||
## Decision Tree — Where Does New Code Go?
|
||||
|
||||
```
|
||||
Is it UI (React components, hooks, pages)?
|
||||
└── YES → packages/desktop/src/renderer/ → see references/renderer.md
|
||||
|
||||
Is it an IPC handler responding to renderer calls?
|
||||
└── YES → packages/desktop/src/process/bridge/ → see references/process.md
|
||||
|
||||
Is it business logic running in the main process?
|
||||
└── YES → packages/desktop/src/process/services/ → see references/process.md
|
||||
|
||||
Is it an AI platform connection (API client, message protocol)?
|
||||
└── YES → packages/desktop/src/process/agent/<platform>/
|
||||
|
||||
Is it a background task that runs in a worker thread?
|
||||
└── YES → packages/desktop/src/process/worker/
|
||||
|
||||
Is it used by BOTH main and renderer processes?
|
||||
└── YES → packages/desktop/src/common/
|
||||
|
||||
Is it an HTTP/WebSocket endpoint?
|
||||
└── YES → packages/desktop/src/process/webserver/
|
||||
|
||||
Is it a plugin/extension resolver or loader?
|
||||
└── YES → packages/desktop/src/process/extensions/
|
||||
|
||||
Is it a messaging channel (Lark, DingTalk, Telegram)?
|
||||
└── YES → packages/desktop/src/process/channels/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Process Boundary Rules
|
||||
|
||||
**Hard rules — violating them causes runtime crashes.**
|
||||
|
||||
| Process | Can use | Cannot use |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
|
||||
| **Main** (`packages/desktop/src/process/`) | Node.js, Electron main APIs, `fs`, `path`, `child_process` | DOM APIs (`document`, `window`, React) |
|
||||
| **Renderer** (`packages/desktop/src/renderer/`) | DOM APIs, React, browser APIs | Node.js APIs (`fs`, `path`), Electron main APIs |
|
||||
| **Worker** (`packages/desktop/src/process/worker/`) | Node.js APIs | DOM APIs, Electron APIs |
|
||||
| **Preload** (`packages/desktop/src/preload/`) | `contextBridge`, `ipcRenderer` | DOM manipulation, Node.js `fs` |
|
||||
|
||||
Cross-process communication:
|
||||
|
||||
- Main ↔ Renderer: IPC via `packages/desktop/src/preload/` + `packages/desktop/src/process/bridge/*.ts`
|
||||
- Main ↔ Worker: fork protocol via `packages/desktop/src/process/worker/WorkerProtocol.ts`
|
||||
|
||||
```typescript
|
||||
// NEVER in renderer
|
||||
import { something } from '@process/services/foo'; // crashes at runtime
|
||||
|
||||
// Use IPC instead
|
||||
const result = await window.api.someMethod(); // goes through preload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Directories
|
||||
|
||||
| Scope | Convention | Reason |
|
||||
| ---------------------------------- | ---------- | ------------------------------------------------------- |
|
||||
| **Renderer** component/module dirs | PascalCase | React convention — dir name = component name |
|
||||
| **Everything else** | lowercase | Node.js convention |
|
||||
| **Categorical dirs** (everywhere) | lowercase | `components/`, `hooks/`, `utils/`, `services/` |
|
||||
| **Platform dirs** (everywhere) | lowercase | `acp/`, `codex/`, `gemini/` — cross-process consistency |
|
||||
|
||||
> Quick test: "Inside `packages/desktop/src/renderer/` AND represents a specific component/feature (not a category)?" → PascalCase. Otherwise → lowercase.
|
||||
|
||||
### Files
|
||||
|
||||
| Content | Convention | Examples |
|
||||
| ------------------------- | ------------------------------- | ------------------------------------- |
|
||||
| React components, classes | PascalCase | `SettingsModal.tsx`, `CronService.ts` |
|
||||
| Hooks | camelCase with `use` prefix | `useTheme.ts`, `useCronJobs.ts` |
|
||||
| Utilities, helpers | camelCase | `formatDate.ts`, `cronUtils.ts` |
|
||||
| Entry points | `index.ts` / `index.tsx` | Required for directory-based modules |
|
||||
| Config, types, constants | camelCase | `types.ts`, `constants.ts` |
|
||||
| Styles | kebab-case or `Name.module.css` | `chat-layout.css` |
|
||||
|
||||
---
|
||||
|
||||
## Structural Rules
|
||||
|
||||
1. **Directory size limit**: Max **10** direct children. Split into subdirectories by responsibility when approaching.
|
||||
2. **No single-file directories**: Merge into parent or related directory.
|
||||
3. **Single file vs directory**: If a component needs a private sub-component or hook, convert to a directory with `index.tsx`.
|
||||
4. **Page-private first**: Start code in `pages/<PageName>/`. Promote to shared only when a second consumer appears.
|
||||
|
||||
## Test File Mapping
|
||||
|
||||
Tests mirror source files in `tests/` subdirectories:
|
||||
|
||||
| Source | Test |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------- |
|
||||
| `packages/desktop/src/process/services/CronService.ts` | `tests/unit/cronService.test.ts` |
|
||||
| `packages/desktop/src/renderer/hooks/ui/useAutoScroll.ts` | `tests/unit/useAutoScroll.dom.test.ts` |
|
||||
| `packages/desktop/src/process/extensions/ExtensionLoader.ts` | `tests/unit/extensions/extensionLoader.test.ts` |
|
||||
|
||||
When `tests/unit/` exceeds 10 direct children, group into subdirectories matching source structure.
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
- [ ] Code is in the correct process directory (no cross-process imports)
|
||||
- [ ] Renderer code does not use Node.js APIs
|
||||
- [ ] Main process code does not use DOM APIs
|
||||
- [ ] New IPC channels are bridged through `preload.ts`
|
||||
- [ ] Renderer component/module dirs use PascalCase; categorical dirs use lowercase
|
||||
- [ ] Platform dirs use lowercase everywhere
|
||||
- [ ] Directory-based modules have `index.tsx` / `index.ts` entry point
|
||||
- [ ] Page-private code is under `pages/<PageName>/`, not in shared dirs
|
||||
- [ ] No single-file directories
|
||||
- [ ] No directory exceeds 10 direct children
|
||||
- [ ] New source files are auto-included in coverage — verify they are not accidentally excluded in `vitest.config.ts` → `coverage.exclude`
|
||||
- [ ] New services separate pure logic from IO
|
||||
@@ -0,0 +1,118 @@
|
||||
# Main Process & Shared Layer
|
||||
|
||||
## `packages/desktop/src/process/` Structure
|
||||
|
||||
```
|
||||
packages/desktop/src/process/
|
||||
├── bridge/ # IPC handlers — one file per domain
|
||||
│ ├── index.ts # Registers all bridges
|
||||
│ └── *Bridge.ts # Individual bridge files
|
||||
├── services/ # Business logic services
|
||||
│ ├── cron/ # Complex service → subdirectory
|
||||
│ └── mcp-services/
|
||||
├── database/ # SQLite layer — schema, migrations, repositories
|
||||
├── task/ # Agent/task management — managers, factories
|
||||
├── utils/ # Main-process-only utilities
|
||||
└── i18n/ # Main-process i18n
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Type | Pattern | Examples |
|
||||
| ----------------- | ------------------------------- | --------------------------------- |
|
||||
| Bridge | `<domain>Bridge.ts` (camelCase) | `cronBridge.ts`, `webuiBridge.ts` |
|
||||
| Service | `<Name>Service.ts` (PascalCase) | `CronService.ts`, `McpService.ts` |
|
||||
| Service interface | `I<Name>Service.ts` | `IConversationService.ts` |
|
||||
| Repository | `<Name>Repository.ts` | `SqliteConversationRepository.ts` |
|
||||
| Agent Manager | `<Platform>AgentManager.ts` | `AcpAgentManager.ts` |
|
||||
|
||||
All directories use lowercase (Node.js convention):
|
||||
|
||||
```
|
||||
packages/desktop/src/process/
|
||||
├── bridge/ # lowercase
|
||||
├── services/ # lowercase
|
||||
│ ├── cron/ # lowercase
|
||||
│ └── mcp-services/ # lowercase (kebab-case for multi-word)
|
||||
├── database/ # lowercase
|
||||
└── task/ # lowercase
|
||||
```
|
||||
|
||||
## Adding a New IPC Bridge
|
||||
|
||||
1. Create `packages/desktop/src/process/bridge/<domain>Bridge.ts`
|
||||
2. Register in `packages/desktop/src/process/bridge/index.ts`
|
||||
3. Expose channel in `packages/desktop/src/preload/`
|
||||
4. Add renderer-side types if needed
|
||||
|
||||
## Adding a New Service
|
||||
|
||||
- Simple → single file in `packages/desktop/src/process/services/`
|
||||
- Complex (multiple files) → subdirectory: `packages/desktop/src/process/services/<name>/`
|
||||
|
||||
## Service Testability Rules
|
||||
|
||||
### Pure Logic vs IO Separation
|
||||
|
||||
- **Pure logic** (transformation, validation, formatting) → standalone functions, no `fs`/`db`/`net`
|
||||
- **IO operations** (file read, DB query, HTTP call) → thin wrappers in service class or repository
|
||||
- Service methods should receive IO results as parameters
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
```typescript
|
||||
// ❌ Hard to test
|
||||
import { db } from '@process/database';
|
||||
function getConversation(id: string) {
|
||||
return db.query('SELECT * FROM conversations WHERE id = ?', id);
|
||||
}
|
||||
|
||||
// ✅ Easy to test
|
||||
function getConversation(repo: IConversationRepository, id: string) {
|
||||
return repo.findById(id);
|
||||
}
|
||||
```
|
||||
|
||||
For existing code using direct imports, `vi.mock()` is acceptable. For new code, prefer parameter injection.
|
||||
|
||||
---
|
||||
|
||||
## Shared Layer
|
||||
|
||||
### Preload (`packages/desktop/src/preload/`)
|
||||
|
||||
IPC bridge between main and renderer. Uses `contextBridge` to expose safe APIs.
|
||||
|
||||
- All main ↔ renderer communication goes through this file
|
||||
- Only `contextBridge` and `ipcRenderer` APIs allowed
|
||||
- No DOM manipulation, no Node.js `fs`
|
||||
|
||||
### Common (`packages/desktop/src/common/`)
|
||||
|
||||
Code imported by **both** main and renderer processes.
|
||||
|
||||
- **Belongs**: shared types, API adapters, protocol converters, storage keys
|
||||
- **Does NOT belong**: React components → `renderer/`, Node.js-specific → `process/`
|
||||
|
||||
### Agent (`packages/desktop/src/process/agent/`)
|
||||
|
||||
One directory per AI platform (lowercase): `acp/`, `codex/`, `gemini/`, `nanobot/`, `openclaw/`. Each has `index.ts` entry. Runs in main or worker process.
|
||||
|
||||
### Worker (`packages/desktop/src/process/worker/`)
|
||||
|
||||
```
|
||||
packages/desktop/src/process/worker/
|
||||
├── fork/ # Fork management
|
||||
├── <platform>.ts # One file per agent platform (lowercase)
|
||||
├── WorkerProtocol.ts # Protocol definition (PascalCase — it's a class)
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Other Modules
|
||||
|
||||
| Module | Location | Purpose |
|
||||
| ---------- | ------------------------------------------ | -------------------------------------------------- |
|
||||
| Channels | `packages/desktop/src/process/channels/` | Multi-channel messaging (Lark, DingTalk, Telegram) |
|
||||
| Extensions | `packages/desktop/src/process/extensions/` | Plugin loading, resolvers, sandbox |
|
||||
| WebServer | `packages/desktop/src/process/webserver/` | Express + WebSocket for WebUI |
|
||||
| Adapter | `packages/desktop/src/common/adapter/` | Platform adapters (browser vs main environment) |
|
||||
@@ -0,0 +1,79 @@
|
||||
# Project Layout
|
||||
|
||||
## Root Directory
|
||||
|
||||
### Rules
|
||||
|
||||
- **Workspace root stays minimal**: root keeps shared config, scripts, tests, docs, assets, and package manager files.
|
||||
- **Desktop app source lives under `packages/desktop/`**: do not add new app runtime code back to the root.
|
||||
- **README translations** → `docs/readme/`, not root. Only main `readme.md` stays at root.
|
||||
- **Guide documents** (`*_GUIDE.md`, `CODE_STYLE.md`) → `docs/`
|
||||
- **Build artifacts** (`out/`, `node_modules/`) are gitignored
|
||||
|
||||
### Current Root Structure (M1)
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── packages/
|
||||
│ └── desktop/ # Electron desktop workspace
|
||||
├── tests/ # Shared test suites
|
||||
├── docs/ # All documentation
|
||||
├── scripts/ # Build and tooling scripts
|
||||
├── resources/ # Static resources (icons, images, installers)
|
||||
├── public/ # Shared Vite public assets
|
||||
├── patches/ # npm/bun patches
|
||||
├── homebrew/ # Homebrew formula
|
||||
├── package.json # Workspace root config
|
||||
├── tsconfig.json # Shared TS config
|
||||
├── vitest.config.ts # Shared test config
|
||||
├── AGENTS.md # Agent conventions
|
||||
├── CLAUDE.md # Claude-specific config
|
||||
└── ... # Other root-level tooling config
|
||||
```
|
||||
|
||||
> **Migration rule**: New desktop runtime modules go under `packages/desktop/`, not the repository root.
|
||||
|
||||
---
|
||||
|
||||
## `packages/desktop/` Layout
|
||||
|
||||
### Workspace Structure
|
||||
|
||||
```
|
||||
packages/desktop/
|
||||
├── src/
|
||||
│ ├── renderer/ # Renderer layer — React UI, no Node.js APIs
|
||||
│ ├── process/ # Main process layer — Node.js / Electron business logic
|
||||
│ ├── common/ # Shared cross-process code
|
||||
│ ├── preload/ # IPC bridge entrypoints
|
||||
│ ├── index.ts # Main process entry
|
||||
│ └── types.d.ts # Ambient declarations
|
||||
├── electron.vite.config.ts
|
||||
├── electron-builder.yml
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### `packages/desktop/src/` Structure
|
||||
|
||||
```
|
||||
packages/desktop/src/
|
||||
├── renderer/ # React UI, browser-only code
|
||||
├── process/ # Electron main-process and worker code
|
||||
│ ├── bridge/ # IPC handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── agent/ # AI platform connections
|
||||
│ ├── channels/ # Multi-channel messaging
|
||||
│ ├── extensions/ # Plugin system
|
||||
│ ├── webserver/ # WebUI server
|
||||
│ └── worker/ # Background workers
|
||||
├── common/ # Shared types, adapters, utilities
|
||||
├── preload/ # contextBridge / ipcRenderer exposure
|
||||
├── index.ts # Main process entry point
|
||||
└── types.d.ts # Ambient declarations
|
||||
```
|
||||
|
||||
### Placement Rules
|
||||
|
||||
- New Electron runtime code belongs in `packages/desktop/src/**`.
|
||||
- Root-level scripts and config may reference `packages/desktop/**`, but should not duplicate app source.
|
||||
- Tests remain under `tests/**` and should reference desktop source through aliases or `packages/desktop/...` paths.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Renderer Layer (`packages/desktop/src/renderer/`)
|
||||
|
||||
## Root Directory — Standard Layout
|
||||
|
||||
At most 3 entry files + 7 directories = 10 items:
|
||||
|
||||
```
|
||||
packages/desktop/src/renderer/
|
||||
├── index.html # Vite HTML entry
|
||||
├── main.tsx # React mount + app bootstrap
|
||||
├── types.d.ts # Ambient type declarations
|
||||
├── pages/ # Page-level modules (business code goes here)
|
||||
├── components/ # Shared UI components (used across multiple pages)
|
||||
├── hooks/ # Shared React hooks (supports business domain subdirs)
|
||||
├── context/ # Global React contexts
|
||||
├── services/ # Client-side services + i18n
|
||||
├── utils/ # Utility functions + types + constants
|
||||
├── styles/ # Global styles + theme configuration
|
||||
└── assets/ # Static assets — Vite resolves to hashed URLs
|
||||
```
|
||||
|
||||
**Does NOT belong at renderer root:**
|
||||
|
||||
- CSS files → `styles/`
|
||||
- Component files (`.tsx`) → `components/` or `pages/`
|
||||
- Single-file directories → merge into a related directory
|
||||
|
||||
## UI Library & Icon Standards
|
||||
|
||||
- **Components**: `@arco-design/web-react` — use Arco components first
|
||||
- **Icons**: `@icon-park/react` — all icons from this library
|
||||
- **No raw HTML** for interactive elements (`<button>`, `<input>`, `<select>`, etc.) — use Arco equivalents
|
||||
- **Layout tags** (`<div>`, `<span>`, `<section>`, etc.) may be used freely
|
||||
|
||||
## CSS Conventions
|
||||
|
||||
- **Prefer UnoCSS** utility classes (`flex items-center gap-8px`)
|
||||
- **Complex/reusable styles**: CSS Modules (`ComponentName.module.css`). No plain `.css` for components
|
||||
- **Semantic color tokens only**: Use `uno.config.ts` tokens (`text-t-primary`, `bg-base`, `border-b-base`) or CSS variables. No hardcoded colors. Exception: `CssThemeSettings/presets/`
|
||||
- **No inline styles** except dynamically computed values
|
||||
- **Arco overrides**: In component's CSS Module via `:global(.arco-xxx)`. No global override files
|
||||
- **Global styles**: Only in `packages/desktop/src/renderer/styles/`
|
||||
|
||||
## `components/` — Layered Structure
|
||||
|
||||
Two layers:
|
||||
|
||||
**Fixed layer:**
|
||||
|
||||
- `base/` — Generic UI primitives (Modal, Select, ScrollArea). No business logic, no app-specific context
|
||||
|
||||
**Business layer:**
|
||||
|
||||
- Subdirectories by business domain (lowercase). Create when ≥ 2 shared components belong to the same domain
|
||||
- Single component may stay at `components/` root until a second same-domain component appears
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Root ≤ 10 direct children
|
||||
- `base/` must not depend on business logic
|
||||
- Single-page components → `pages/<PageName>/components/`
|
||||
|
||||
```
|
||||
packages/desktop/src/renderer/components/
|
||||
├── base/ # UI primitives
|
||||
├── chat/ # Conversation/message domain
|
||||
├── agent/ # Agent selection/configuration
|
||||
├── settings/ # Settings domain
|
||||
├── layout/ # Window frame and layout
|
||||
├── media/ # File preview, image viewer
|
||||
└── ... # New domains as needed
|
||||
```
|
||||
|
||||
## `hooks/` — Grouping by Business Domain
|
||||
|
||||
Group into subdirectories when exceeding 10 children. Generic hooks stay at root.
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── agent/ # Agent/model — useModelProviderList, useAgentReadinessCheck
|
||||
├── chat/ # Chat/message — useAutoTitle, useSendBoxDraft, useSlashCommands
|
||||
├── file/ # File/workspace — useDragUpload, useOpenFileSelector
|
||||
├── mcp/ # MCP related
|
||||
├── ui/ # Generic UI — useAutoScroll, useDebounce, useResizableSplit
|
||||
├── system/ # System-level — useDeepLink, useTheme, usePwaMode
|
||||
└── index.ts # Public re-exports (optional)
|
||||
```
|
||||
|
||||
## `utils/` — Grouping by Business Domain
|
||||
|
||||
Same principle as hooks. Group when exceeding 10 children.
|
||||
|
||||
```
|
||||
utils/
|
||||
├── file/ # File handling — base64, fileType, download
|
||||
├── workspace/ # Workspace — workspace, workspaceEvents, workspaceFs
|
||||
├── chat/ # Chat/message — chatMinimapEvents, diffUtils, latexDelimiters
|
||||
├── model/ # Model/agent — agentLogo, modelCapabilities, modelContextLimits
|
||||
├── theme/ # Theme/style — customCssProcessor, themeCssSync
|
||||
├── ui/ # Generic UI — clipboard, focus, siderTooltip, HOC
|
||||
├── common.ts # Misc utilities
|
||||
├── emitter.ts
|
||||
└── platform.ts
|
||||
```
|
||||
|
||||
## Page Module Structure
|
||||
|
||||
```
|
||||
PageName/ # PascalCase
|
||||
├── index.tsx # Entry point (required)
|
||||
├── components/ # Page-private components (lowercase categorical dir)
|
||||
│ ├── FeatureA.tsx # Simple sub-component
|
||||
│ └── FeatureB/ # Complex sub-component (PascalCase)
|
||||
│ └── index.tsx
|
||||
├── hooks/ # Page-private hooks
|
||||
├── contexts/ # Page-private React contexts
|
||||
├── utils/ # Page-private utilities
|
||||
├── types.ts
|
||||
└── constants.ts
|
||||
```
|
||||
|
||||
Only create sub-directories you need. Use these exact names.
|
||||
|
||||
## Page-Level Directory Naming
|
||||
|
||||
| Type | Convention | Examples |
|
||||
| ------------------------------- | ---------- | --------------------------------------------------------------------------- |
|
||||
| **Categorical** (standard role) | lowercase | `components/`, `hooks/`, `context/`, `utils/` |
|
||||
| **Feature module** (business) | PascalCase | `GroupedHistory/`, `Workspace/`, `Preview/` |
|
||||
| **Platform directory** | lowercase | `acp/`, `codex/`, `gemini/` (mirrors `packages/desktop/src/process/agent/`) |
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
packages/desktop/src/renderer/
|
||||
├── components/ # categorical → lowercase
|
||||
│ ├── SettingsModal/ # component → PascalCase
|
||||
│ └── EmojiPicker/ # component → PascalCase
|
||||
├── pages/ # categorical → lowercase
|
||||
│ ├── settings/ # top-level page → lowercase (route segment)
|
||||
│ │ ├── CssThemeSettings/ # feature module → PascalCase
|
||||
│ │ └── McpManagement/ # feature module → PascalCase
|
||||
│ └── conversation/ # top-level page → lowercase
|
||||
│ ├── GroupedHistory/ # feature module → PascalCase
|
||||
│ ├── Workspace/ # feature module → PascalCase
|
||||
│ ├── acp/ # platform dir → lowercase
|
||||
│ └── components/ # categorical → lowercase
|
||||
└── hooks/ # categorical → lowercase
|
||||
```
|
||||
|
||||
## Shared vs Page-Private Code
|
||||
|
||||
| Scope | Location |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Used by **one** page | `pages/<PageName>/components/`, `hooks/`, etc. |
|
||||
| Used by **multiple** pages | `packages/desktop/src/renderer/components/`, `packages/desktop/src/renderer/hooks/` |
|
||||
|
||||
**Promotion rule**: Start page-private. Move to shared only when a second consumer appears.
|
||||
|
||||
## Component Entry Points
|
||||
|
||||
- Directory-based components **must** have `index.tsx` as the public entry point
|
||||
- Do not import internal files from outside the directory
|
||||
@@ -0,0 +1,274 @@
|
||||
---
|
||||
name: bump-version
|
||||
description: Use when bumping the AionUi version: query AionCore release, verify artifacts, update package.json, generate CHANGELOG, branch, commit, push, create PR, auto-merge, tag release.
|
||||
---
|
||||
|
||||
# Bump Version
|
||||
|
||||
Automate the AionUi release preparation: query AionCore release → verify artifacts → update versions → generate CHANGELOG → branch → PR → tag.
|
||||
|
||||
**Usage:** `/bump-version [version] [flags]`
|
||||
|
||||
- `/bump-version` — auto patch + latest AionCore
|
||||
- `/bump-version 2.2.0` — explicit AionUi version + latest AionCore
|
||||
- `/bump-version 2.2.0 --core v0.1.12` — explicit both versions
|
||||
- `/bump-version --skip-core` — pure frontend release (don't touch aioncoreVersion)
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Pre-flight Checks
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
```
|
||||
|
||||
- **Not on `main`** → Stop: "Please switch to main before running bump-version."
|
||||
- **Dirty working tree** → Stop: "There are uncommitted changes. Please commit or stash them first."
|
||||
|
||||
### Step 2: Pull Latest
|
||||
|
||||
```bash
|
||||
git pull --rebase origin main
|
||||
```
|
||||
|
||||
Fails → Stop: "Failed to pull latest code. Please resolve conflicts or network issues first."
|
||||
|
||||
### Step 3: Determine AionUi Target Version
|
||||
|
||||
Read `package.json` → extract `version` field.
|
||||
|
||||
- **Argument provided** → use as-is
|
||||
- **No argument** → parse `major.minor.patch`, increment `patch` by 1
|
||||
|
||||
Display: "Bumping AionUi: {current} → {target}"
|
||||
|
||||
### Step 4: Query AionCore Latest Release
|
||||
|
||||
**Skip entirely if `--skip-core` is set.**
|
||||
|
||||
```bash
|
||||
gh release view --repo iOfficeAI/AionCore --json tagName,body
|
||||
```
|
||||
|
||||
- If `--core <version>` provided → use that tag instead of latest
|
||||
- Display the AionCore version and ask user to confirm before continuing
|
||||
- Also read current `aioncoreVersion` from `package.json` — if it already matches the queried version, warn the user and ask whether to proceed or use `--skip-core`
|
||||
|
||||
### Step 5: Verify AionCore Artifacts
|
||||
|
||||
**Skip if `--skip-core`.**
|
||||
|
||||
```bash
|
||||
gh release view <tag> --repo iOfficeAI/AionCore --json assets --jq '.assets[].name'
|
||||
```
|
||||
|
||||
Verify all 7 expected assets exist:
|
||||
|
||||
- `aioncore-<tag>-x86_64-unknown-linux-gnu.tar.gz`
|
||||
- `aioncore-<tag>-aarch64-unknown-linux-gnu.tar.gz`
|
||||
- `aioncore-<tag>-x86_64-apple-darwin.tar.gz`
|
||||
- `aioncore-<tag>-aarch64-apple-darwin.tar.gz`
|
||||
- `aioncore-<tag>-x86_64-pc-windows-msvc.zip`
|
||||
- `aioncore-<tag>-aarch64-pc-windows-msvc.zip`
|
||||
- `aioncore-checksums.txt`
|
||||
|
||||
Missing → Stop: "AionCore {tag} is missing artifacts: {list}. Wait for CI to complete or check for build failures."
|
||||
|
||||
### Step 6: Update package.json
|
||||
|
||||
Use Edit tool to replace:
|
||||
|
||||
- `"version": "{current}"` → `"version": "{target}"`
|
||||
- `"aioncoreVersion": "{old}"` → `"aioncoreVersion": "{new core tag}"` (skip if `--skip-core`)
|
||||
|
||||
### Step 7: Generate CHANGELOG Entry
|
||||
|
||||
#### 7a: Determine Previous Tag
|
||||
|
||||
```bash
|
||||
git describe --tags --abbrev=0
|
||||
```
|
||||
|
||||
This gives the most recent tag (e.g. `v2.1.2`).
|
||||
|
||||
#### 7b: Collect Frontend Changes
|
||||
|
||||
```bash
|
||||
git log v{previous}..HEAD --oneline --no-merges --format="%s"
|
||||
```
|
||||
|
||||
- Filter to conventional commit types: `feat`, `fix`, `refactor`, `perf`, `style`
|
||||
- Exclude commits matching `chore: bump version`
|
||||
- Group by type (Features, Bug Fixes, Refactoring, Performance, Styling)
|
||||
- Format each as: `- **scope:** description (#PR)`
|
||||
|
||||
#### 7c: Collect AionCore Changes
|
||||
|
||||
From step 4's release body (already in conventional-changelog format from release-please). Parse into same grouped format.
|
||||
|
||||
**Skip if `--skip-core`.**
|
||||
|
||||
#### 7d: Compose and Write CHANGELOG.md
|
||||
|
||||
If `CHANGELOG.md` exists at repo root → read its current content.
|
||||
If not → start with empty string.
|
||||
|
||||
Prepend the new entry in this format:
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [{target}](https://github.com/iOfficeAI/AionUi/compare/v{previous}...v{target}) ({date YYYY-MM-DD})
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **upload:** abort in-flight uploads when switching conversations (#3019)
|
||||
|
||||
#### Features
|
||||
|
||||
- **thinking:** add streaming indicator (#3015)
|
||||
|
||||
### Core ([{core tag}](https://github.com/iOfficeAI/AionCore/releases/tag/{core tag}))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** load user MCP servers and emit empty-finish diagnostic (#327)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- If `--skip-core`: omit the entire "### Core" section
|
||||
- If no frontend commits since last tag: show `_No frontend changes in this release._` under "### Desktop"
|
||||
- Date format: `YYYY-MM-DD`
|
||||
- Always keep the top-level `# Changelog` header exactly once
|
||||
|
||||
### Step 8: Quality Checks
|
||||
|
||||
```bash
|
||||
bun run lint
|
||||
bun run format
|
||||
bunx tsc --noEmit
|
||||
```
|
||||
|
||||
- **lint fails** → Stop: "Lint errors found. Please fix them before bumping."
|
||||
- **format** → Auto-fixes silently.
|
||||
- **tsc fails** → Stop: "TypeScript errors found. Please fix them before bumping."
|
||||
|
||||
### Step 9: Run Tests
|
||||
|
||||
```bash
|
||||
bunx vitest run
|
||||
```
|
||||
|
||||
Fails → Stop: "Tests failed. Please fix before bumping."
|
||||
|
||||
### Step 10: Branch, Commit, Push
|
||||
|
||||
```bash
|
||||
git checkout -b chore/bump-version-{target}
|
||||
git add package.json CHANGELOG.md
|
||||
git commit -m "chore: bump version to {target} and aioncore to {core tag}"
|
||||
just push -u origin chore/bump-version-{target}
|
||||
```
|
||||
|
||||
If `--skip-core`:
|
||||
|
||||
```bash
|
||||
git commit -m "chore: bump version to {target}"
|
||||
```
|
||||
|
||||
### Step 11: Create PR + Enable Auto-Merge
|
||||
|
||||
```bash
|
||||
gh pr create --base main \
|
||||
--title "chore: bump version to {target}" \
|
||||
--body "<the CHANGELOG entry generated in Step 7>"
|
||||
```
|
||||
|
||||
Capture the PR number from the output. Then enable auto-merge (squash):
|
||||
|
||||
```bash
|
||||
gh pr merge {PR_NUMBER} --auto --squash
|
||||
```
|
||||
|
||||
Display: "PR created: {URL}. Auto-merge enabled — will merge automatically once CI passes."
|
||||
|
||||
### Step 12: Poll for Merge
|
||||
|
||||
Check PR merge status every 5 minutes:
|
||||
|
||||
```bash
|
||||
gh pr view {PR_NUMBER} --json state,mergedAt,mergeStateStatus
|
||||
```
|
||||
|
||||
**Decision logic:**
|
||||
|
||||
| `state` | Action |
|
||||
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `MERGED` | Proceed to Step 13 |
|
||||
| `CLOSED` (not merged) | Stop: "PR was closed without merging. Please check and confirm how to proceed." |
|
||||
| `OPEN` with `mergeStateStatus: BLOCKED` or CI failure persisting > 3 checks | Stop: "PR merge is blocked (CI failure or review required). Please investigate: {URL}" |
|
||||
| `OPEN` otherwise | Wait 5 minutes, check again |
|
||||
|
||||
**Maximum wait:** 30 minutes (6 checks). If not merged after 30 minutes:
|
||||
|
||||
> "PR has not merged after 30 minutes. Please check status: {URL}. Reply 'continue' when merged, or 'abort' to stop."
|
||||
|
||||
**Wait for user confirmation only in this timeout case.**
|
||||
|
||||
### Step 13: Cleanup + Tag
|
||||
|
||||
After merge is confirmed (either via polling or user confirmation):
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull --rebase origin main
|
||||
git branch -d chore/bump-version-{target}
|
||||
```
|
||||
|
||||
Check if remote branch still exists:
|
||||
|
||||
```bash
|
||||
git ls-remote --heads origin chore/bump-version-{target}
|
||||
```
|
||||
|
||||
- Has output → `git push origin --delete chore/bump-version-{target}`
|
||||
- No output → skip
|
||||
|
||||
Create and push tag:
|
||||
|
||||
```bash
|
||||
git tag v{target}
|
||||
git push origin v{target}
|
||||
```
|
||||
|
||||
Wait a few seconds for GitHub to pick up the tag push, then fetch the triggered workflow run:
|
||||
|
||||
```bash
|
||||
gh run list --workflow=release.yml --branch v{target} --limit 1 --json databaseId,url
|
||||
```
|
||||
|
||||
Display: "Tag v{target} created and pushed. Release build triggered! Action: {run URL}"
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
1. Must be on clean main
|
||||
2. git pull --rebase
|
||||
3. Determine AionUi target version (patch+1 or explicit)
|
||||
4. Query AionCore latest release (or --core / --skip-core)
|
||||
5. Verify AionCore artifacts (7 files)
|
||||
6. Edit package.json (version + aioncoreVersion)
|
||||
7. Generate CHANGELOG entry (frontend commits + AionCore release body)
|
||||
8. lint + format + tsc
|
||||
9. vitest run
|
||||
10. branch → commit → push
|
||||
11. gh pr create → enable auto-merge (squash)
|
||||
12. poll merge status (every 5min, max 30min) → stop on failure
|
||||
13. cleanup → git tag → git push tag
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
name: i18n
|
||||
description: |
|
||||
Internationalization (i18n) workflow and standards for managing translations.
|
||||
Use when: (1) Adding new user-facing text, (2) Creating new components with user-facing text,
|
||||
(3) Reviewing code for i18n compliance, (4) Adding a new translation module.
|
||||
---
|
||||
|
||||
# i18n Skill
|
||||
|
||||
Standards and workflow for internationalization. All user-visible text must use i18n.
|
||||
|
||||
**Announce at start:** "I'm using i18n skill to ensure proper internationalization."
|
||||
|
||||
## IMPORTANT: Read Config First
|
||||
|
||||
Before doing any i18n work, **always read `src/common/config/i18n-config.json`** to get the current list of supported languages and modules. Never assume a fixed number — languages and modules may have been added or removed since this skill was written.
|
||||
|
||||
```bash
|
||||
cat src/common/config/i18n-config.json
|
||||
```
|
||||
|
||||
This file is the **single source of truth**. All scripts, runtime code, and this workflow depend on it.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/common/config/i18n-config.json # Single source of truth: languages, modules
|
||||
src/renderer/i18n/
|
||||
├── index.ts # i18next configuration
|
||||
├── i18n-keys.d.ts # AUTO-GENERATED — do not edit manually
|
||||
└── locales/
|
||||
├── <lang>/ # One directory per language in i18n-config.json
|
||||
│ ├── index.ts # Barrel import for all modules
|
||||
│ ├── common.json # One JSON per module in i18n-config.json
|
||||
│ ├── conversation.json
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Key Facts
|
||||
|
||||
- **Reference language**: defined by `referenceLanguage` in `i18n-config.json` (currently `en-US`)
|
||||
- **Supported languages**: defined by `supportedLanguages` array — read the file to get the current list
|
||||
- **Modules**: defined by `modules` array — read the file to get the current list
|
||||
|
||||
## Key Structure
|
||||
|
||||
Keys use **namespaced dot notation** in code: `t('module.key')` or `t('module.nested.key')`.
|
||||
|
||||
Inside each module JSON file, keys can be **flat or nested**:
|
||||
|
||||
```json
|
||||
// common.json — flat keys
|
||||
{
|
||||
"send": "Send",
|
||||
"cancel": "Cancel",
|
||||
"copySuccess": "Copied"
|
||||
}
|
||||
|
||||
// cron.json — nested keys
|
||||
{
|
||||
"scheduledTasks": "Scheduled Tasks",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"paused": "Paused"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In code:
|
||||
|
||||
```typescript
|
||||
t('common.send'); // flat key in common.json
|
||||
t('cron.status.active'); // nested key in cron.json
|
||||
```
|
||||
|
||||
### Key Naming Rules
|
||||
|
||||
- Use **camelCase** for key names: `copySuccess`, `scheduledTasks`
|
||||
- Group related keys with nesting: `status.active`, `actions.pause`
|
||||
- Reusable text goes in `common.json`: save, cancel, delete, confirm, etc.
|
||||
- Feature-specific text goes in the corresponding module
|
||||
|
||||
### Common Suffixes
|
||||
|
||||
| Suffix | Usage |
|
||||
| ------------------- | -------------------- |
|
||||
| `title` | Section/page titles |
|
||||
| `placeholder` | Input placeholders |
|
||||
| `label` | Form labels |
|
||||
| `success` / `error` | Status messages |
|
||||
| `confirm` | Confirmation dialogs |
|
||||
| `empty` | Empty state messages |
|
||||
| `tooltip` | Tooltip text |
|
||||
|
||||
## Adding New Text — Workflow
|
||||
|
||||
### Step 1: Read `src/common/config/i18n-config.json`
|
||||
|
||||
Get the current language list and module list. Do not skip this step.
|
||||
|
||||
### Step 2: Check Existing Keys
|
||||
|
||||
Before adding a new key, search for similar existing keys:
|
||||
|
||||
```bash
|
||||
grep -r "keyword" src/renderer/i18n/locales/en-US/
|
||||
```
|
||||
|
||||
Reuse `common.*` keys when possible.
|
||||
|
||||
### Step 3: Choose the Right Module
|
||||
|
||||
Match the module to the feature area. If no module fits, consider whether a new module is needed (see "Adding a New Module" below).
|
||||
|
||||
### Step 4: Add to ALL Locale Directories
|
||||
|
||||
**CRITICAL:** Every new key must be added to **every** locale in `supportedLanguages`. Use this checklist for each key:
|
||||
|
||||
- [ ] `en-US/<module>.json` — reference language (added in Step 3)
|
||||
- [ ] `zh-CN/<module>.json` — added
|
||||
- [ ] `zh-TW/<module>.json` — added
|
||||
- [ ] Any other language listed in `src/common/config/i18n-config.json` → `supportedLanguages` — added
|
||||
|
||||
A key missing from even one locale will cause `node scripts/check-i18n.js` to fail in CI.
|
||||
|
||||
### Step 5: Use in Component
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation();
|
||||
return <button>{t('common.save')}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Regenerate Types and Validate
|
||||
|
||||
Run these two commands **in order** — both must pass before committing:
|
||||
|
||||
```bash
|
||||
bun run i18n:types # Step A: regenerate i18n-keys.d.ts from reference locale
|
||||
node scripts/check-i18n.js # Step B: validate structure, keys, and type sync
|
||||
```
|
||||
|
||||
- `i18n:types` must be run **before** `check-i18n.js` — the check validates the generated file
|
||||
- If `check-i18n.js` exits with errors (❌), fix them before proceeding
|
||||
- If `check-i18n.js` exits with warnings only (⚠️), review but may proceed
|
||||
- **Never commit with a stale `i18n-keys.d.ts`**
|
||||
|
||||
## Adding a New Module
|
||||
|
||||
1. Add module name to `src/common/config/i18n-config.json` → `modules` array
|
||||
2. Create `<module>.json` in **every** locale directory (read `supportedLanguages` to know which)
|
||||
3. Add import + export in each locale's `index.ts`
|
||||
4. Run `bun run i18n:types` to regenerate type definitions
|
||||
5. Run `node scripts/check-i18n.js` to validate
|
||||
|
||||
## Hardcoded String Detection
|
||||
|
||||
### Prohibited Patterns
|
||||
|
||||
Never use hardcoded Chinese/English text in JSX:
|
||||
|
||||
```tsx
|
||||
// Bad
|
||||
<span>重命名</span>
|
||||
<span>Delete</span>
|
||||
{name || '新对话'}
|
||||
|
||||
// Good
|
||||
<span>{t('common.rename')}</span>
|
||||
<span>{t('common.delete')}</span>
|
||||
{name || t('conversation.newConversation')}
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
- Code comments (any language OK)
|
||||
- `console.log()` / debug output
|
||||
- Internal string constants not shown to users
|
||||
|
||||
## Interpolation
|
||||
|
||||
### Variables
|
||||
|
||||
```json
|
||||
{
|
||||
"taskCount": "{{count}} task(s)",
|
||||
"greeting": "Hello, {{name}}!"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
t('cron.taskCount', { count: 5 });
|
||||
```
|
||||
|
||||
### HTML in Translations
|
||||
|
||||
Use Trans component for complex markup:
|
||||
|
||||
```tsx
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
<Trans i18nKey='cron.countdown'>
|
||||
Task <strong>{{ taskName }}</strong> in <span>{{ countdown }}</span>
|
||||
</Trans>;
|
||||
```
|
||||
|
||||
## zh-TW Maintenance
|
||||
|
||||
Most terms can be auto-converted from zh-CN, but some need manual review:
|
||||
|
||||
| zh-CN | zh-TW | Notes |
|
||||
| ----- | ----- | -------------- |
|
||||
| 视频 | 影片 | Different term |
|
||||
| 软件 | 軟體 | Different term |
|
||||
| 信息 | 訊息 | Different term |
|
||||
| 默认 | 預設 | Different term |
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
Before submitting code with new text:
|
||||
|
||||
- [ ] Read `src/common/config/i18n-config.json` to get current languages and modules
|
||||
- [ ] All user-visible text uses `t()` function
|
||||
- [ ] New keys added to **every** locale directory in `supportedLanguages`
|
||||
- [ ] No hardcoded Chinese/English in JSX
|
||||
- [ ] zh-TW reviewed for term differences
|
||||
- [ ] `bun run i18n:types` ran first (regenerates `i18n-keys.d.ts`)
|
||||
- [ ] `node scripts/check-i18n.js` passed after types regenerated (no errors)
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Correct |
|
||||
| ---------------------------------------------- | ---------------------------------------------------- |
|
||||
| Assuming a fixed number of languages | Always read `i18n-config.json` first |
|
||||
| Adding key to only some locales | Add to **every** locale in `supportedLanguages` |
|
||||
| Editing `i18n-keys.d.ts` manually | Run `bun run i18n:types` to generate |
|
||||
| Using `t("New Chat")` | Define key: `t("conversation.newChat")` |
|
||||
| Not updating `i18n-config.json` for new module | Update config first, then create files |
|
||||
| Adding module JSON but not updating `index.ts` | Must add import + export in each locale's `index.ts` |
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: testing
|
||||
description: |
|
||||
Testing workflow and quality standards for writing and running tests.
|
||||
Use when: (1) Writing new tests, (2) Adding a new feature that needs tests,
|
||||
(3) Modifying logic that has existing tests, (4) Before claiming a task is complete.
|
||||
---
|
||||
|
||||
# Testing Skill
|
||||
|
||||
Standards and workflow for writing and running tests. Every feature must be tested.
|
||||
|
||||
**Announce at start:** "I'm using testing skill to ensure proper test coverage."
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
- Writing new tests for a feature or bug fix
|
||||
- Adding a new feature (must include tests)
|
||||
- Modifying logic that has existing tests (must update them)
|
||||
- Before claiming work is complete
|
||||
- Before committing code
|
||||
|
||||
## Framework
|
||||
|
||||
**Vitest 4** — configured in `vitest.config.ts`.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Individual functions, utilities, components
|
||||
├── integration/ # IPC, database, service interactions
|
||||
├── regression/ # Regression test cases
|
||||
└── e2e/ # End-to-end tests (Playwright, playwright.config.ts)
|
||||
```
|
||||
|
||||
## Two Test Environments
|
||||
|
||||
| Environment | When | File naming |
|
||||
| ---------------- | --------------------------------- | --------------- |
|
||||
| `node` (default) | Main process, utilities, services | `*.test.ts` |
|
||||
| `jsdom` | DOM/browser-dependent code | `*.dom.test.ts` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Identify What to Test
|
||||
|
||||
Before writing tests, list the **riskiest scenarios** first:
|
||||
|
||||
- What happens when the dependency returns `undefined` / throws?
|
||||
- What happens at boundaries (empty list, max retries, past timestamp)?
|
||||
- What is most likely to break in production?
|
||||
|
||||
### Step 2: Write Tests
|
||||
|
||||
Follow these quality rules:
|
||||
|
||||
**1. Describe behavior, not code structure**
|
||||
|
||||
```typescript
|
||||
// Wrong — describes implementation
|
||||
it('should call repo.getConversation', ...)
|
||||
|
||||
// Correct — describes behavior
|
||||
it('should return cached task without hitting repo on second call', ...)
|
||||
it('should reject with error when conversation does not exist', ...)
|
||||
```
|
||||
|
||||
**2. Every describe block must cover at least one failure path**
|
||||
|
||||
Happy-path-only tests leave the most dangerous code untested.
|
||||
|
||||
**3. One behavior per test**
|
||||
|
||||
Keep each `it()` focused. More than 3 `expect()` calls in one test is a signal it is testing too much at once.
|
||||
|
||||
**4. Self-check**
|
||||
|
||||
After writing a test, mentally delete the core logic it targets. If the test would still pass, rewrite it — it is not guarding anything.
|
||||
|
||||
**5. Start from risk, not from coverage gaps**
|
||||
|
||||
List scenarios most likely to produce bugs. Write those first. Coverage is the outcome, not the starting point.
|
||||
|
||||
### Step 3: Run Tests
|
||||
|
||||
```bash
|
||||
bun run test # Run all tests (REQUIRED before every commit)
|
||||
bun run test:coverage # Check coverage (before opening a PR)
|
||||
```
|
||||
|
||||
### Step 4: Verify Coverage
|
||||
|
||||
**Coverage target**: ≥ 80% for all source files matched by `vitest.config.ts` → `coverage.include` (currently `src/**/*.{ts,tsx}` plus a few scripts).
|
||||
|
||||
New source files are automatically included in coverage — no manual config changes needed. If a new file is accidentally excluded by a rule in `coverage.exclude`, remove it from the exclude list.
|
||||
|
||||
### Step 5: Update Existing Tests
|
||||
|
||||
When modifying logic, check if existing tests need updating:
|
||||
|
||||
```bash
|
||||
bun run test -- --reporter=verbose # See which tests pass/fail with names
|
||||
```
|
||||
|
||||
If a test fails because the behavior changed intentionally, update the test. If it fails unexpectedly, investigate.
|
||||
|
||||
## Edge Case Checklist
|
||||
|
||||
When testing a module, verify:
|
||||
|
||||
- [ ] `null` / `undefined` inputs handled
|
||||
- [ ] Empty arrays/objects handled
|
||||
- [ ] Error thrown by dependencies handled
|
||||
- [ ] Boundary values (0, -1, max, empty string)
|
||||
- [ ] Async operations (timeout, rejection, cancellation)
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
Before submitting code:
|
||||
|
||||
- [ ] New features have corresponding test cases
|
||||
- [ ] Modified logic has updated tests
|
||||
- [ ] `bun run test` passes
|
||||
- [ ] Tests describe **behavior**, not implementation
|
||||
- [ ] At least one failure path per describe block
|
||||
- [ ] New source files are not accidentally excluded by `coverage.exclude`
|
||||
- [ ] `bun run test:coverage` meets ≥ 80% target
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Correct |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| Testing implementation details | Test observable behavior |
|
||||
| Only testing happy path | Must include at least one failure path |
|
||||
| 5+ expects in one `it()` | Split into separate tests |
|
||||
| Skipping tests for "simple" code | Simple code breaks too — test the risky parts |
|
||||
| Writing tests after saying "done" | Tests are part of "done", not an afterthought |
|
||||
@@ -0,0 +1,33 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 10%
|
||||
informational: true
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
|
||||
ignore:
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
- '**/*.html'
|
||||
- '**/*.css'
|
||||
- '**/*.svg'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.gif'
|
||||
- '**/*.woff*'
|
||||
- '**/*.ttf'
|
||||
- '**/*.lock'
|
||||
- 'docs/'
|
||||
- 'examples/'
|
||||
- 'mobile/'
|
||||
- 'scripts/'
|
||||
- 'tests/'
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
out
|
||||
dist-server
|
||||
.git
|
||||
.github
|
||||
data
|
||||
*.log
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://developers.google.com/gemini-code-assist/docs/config-schema",
|
||||
"codeReview": {
|
||||
"enabled": true,
|
||||
"autoReview": true,
|
||||
"maxFilesToReview": 50,
|
||||
"excludePatterns": [
|
||||
"**/*.min.js",
|
||||
"**/*.min.css",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/node_modules/**",
|
||||
"**/*.lock",
|
||||
"**/package-lock.json",
|
||||
"**/bun.lockb"
|
||||
]
|
||||
},
|
||||
"pullRequestSummary": {
|
||||
"enabled": true,
|
||||
"includeStats": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# AionUI Code Review Style Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the coding standards and best practices for the AionUI project. The AI code reviewer should use these guidelines when reviewing pull requests.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Runtime**: Bun
|
||||
- **Framework**: Electron + React
|
||||
- **Language**: TypeScript (strict mode)
|
||||
- **Styling**: Tailwind CSS + Arco Design
|
||||
- **State Management**: React hooks + SWR
|
||||
- **i18n**: react-i18next (support: en-US, zh-CN, zh-TW, ja-JP, ko-KR)
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Use strict TypeScript configuration
|
||||
- Avoid `any` type - use `unknown` or proper generics
|
||||
- Prefer interfaces over type aliases for object shapes
|
||||
- Use explicit return types for exported functions
|
||||
- Use optional chaining (`?.`) and nullish coalescing (`??`)
|
||||
|
||||
### React
|
||||
|
||||
- Use functional components with hooks
|
||||
- Prefer `useMemo` and `useCallback` for expensive computations
|
||||
- Avoid inline functions in JSX when possible
|
||||
- Use proper dependency arrays in hooks
|
||||
- Follow React naming conventions (PascalCase for components)
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Always handle Promise rejections
|
||||
- Use try-catch for async/await
|
||||
- Provide meaningful error messages
|
||||
- Log errors appropriately using console.error
|
||||
|
||||
### Security
|
||||
|
||||
- Never commit secrets or API keys
|
||||
- Validate all user inputs
|
||||
- Sanitize data before rendering (XSS prevention)
|
||||
- Use secure IPC communication patterns in Electron
|
||||
|
||||
### Performance
|
||||
|
||||
- Lazy load components when appropriate
|
||||
- Avoid unnecessary re-renders
|
||||
- Use proper memoization
|
||||
- Consider bundle size when adding dependencies
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── common/ # Shared utilities and types
|
||||
├── process/ # Main process code (Electron)
|
||||
├── renderer/ # Renderer process code (React)
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── pages/ # Page components
|
||||
│ └── i18n/ # Internationalization
|
||||
└── agent/ # AI agent related code
|
||||
```
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `docs:` - Documentation
|
||||
- `style:` - Code style (formatting, etc.)
|
||||
- `refactor:` - Code refactoring
|
||||
- `perf:` - Performance improvement
|
||||
- `test:` - Tests
|
||||
- `chore:` - Maintenance tasks
|
||||
|
||||
## Review Priorities
|
||||
|
||||
When reviewing code, prioritize in this order:
|
||||
|
||||
1. **Security** - Vulnerabilities, secrets exposure, injection attacks
|
||||
2. **Correctness** - Logic errors, edge cases, data validation
|
||||
3. **Performance** - Memory leaks, unnecessary computations
|
||||
4. **Maintainability** - Code readability, proper abstractions
|
||||
5. **Style** - Naming conventions, formatting (lowest priority)
|
||||
|
||||
## Language
|
||||
|
||||
- Code comments should be in English or bilingual (English + Chinese)
|
||||
- Use clear and descriptive variable/function names
|
||||
- Avoid abbreviations unless widely understood
|
||||
@@ -0,0 +1,25 @@
|
||||
# Enforce Unix-style line endings
|
||||
* text=auto eol=lf
|
||||
|
||||
# Deny checkout of files with mixed line endings
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.json text eol=lf
|
||||
*.css text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.html text eol=lf
|
||||
*.patch text eol=lf
|
||||
|
||||
# Binary files
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
@@ -0,0 +1,188 @@
|
||||
# CI/CD 设置指南
|
||||
|
||||
## 概述
|
||||
|
||||
这个项目配置了完整的 GitHub Actions CI/CD 流水线,支持自动构建、测试和发布到多个平台。
|
||||
|
||||
## 工作流说明
|
||||
|
||||
### 1. `build-and-release.yml` - 主构建和发布流
|
||||
|
||||
- **触发时机**: 仅推送到 `main` 分支
|
||||
- **功能**:
|
||||
- 代码质量检查 (ESLint, Prettier, TypeScript)
|
||||
- 多平台构建 (macOS Intel/Apple Silicon, Windows, Linux)
|
||||
- 自动创建版本标签
|
||||
- 创建 Draft Release (需要手动审批和发布)
|
||||
- **流程**:
|
||||
1. 代码质量检查
|
||||
2. 三平台并行构建
|
||||
3. 自动创建基于 package.json 版本的标签
|
||||
4. 等待环境审批
|
||||
5. 创建 Draft Release (需要手动编辑和发布)
|
||||
|
||||
## 必需的 GitHub Secrets 配置
|
||||
|
||||
在 GitHub 仓库的 Settings → Secrets and variables → Actions 中配置以下 Secrets:
|
||||
|
||||
### macOS 应用签名 (可选,用于发布到 Mac App Store)
|
||||
|
||||
```
|
||||
APPLE_ID=你的苹果开发者账号邮箱
|
||||
APPLE_ID_PASSWORD=应用专用密码
|
||||
TEAM_ID=苹果开发者团队ID
|
||||
IDENTITY=签名证书名称
|
||||
```
|
||||
|
||||
### GitHub Token
|
||||
|
||||
```
|
||||
GH_TOKEN=你的Personal Access Token (github_pat_开头)
|
||||
```
|
||||
|
||||
**注意**: 需要手动配置,因为需要 `contents: write` 权限来创建 releases。
|
||||
|
||||
### Environment Secrets
|
||||
|
||||
在 Settings → Environments → release 中也需要配置:
|
||||
|
||||
```
|
||||
GH_TOKEN=相同的Personal Access Token
|
||||
```
|
||||
|
||||
## 如何获取 Apple 签名配置
|
||||
|
||||
### 1. Apple ID App-Specific Password
|
||||
|
||||
1. 访问 [appleid.apple.com](https://appleid.apple.com)
|
||||
2. 登录你的 Apple ID
|
||||
3. 在"Sign-In and Security"部分点击"App-Specific Passwords"
|
||||
4. 生成新的应用专用密码
|
||||
5. 复制生成的密码作为 `APPLE_ID_PASSWORD`
|
||||
|
||||
### 2. Team ID
|
||||
|
||||
1. 访问 [Apple Developer Portal](https://developer.apple.com/account/)
|
||||
2. 在"Membership Details"中找到 Team ID
|
||||
3. 复制 Team ID 作为 `TEAM_ID`
|
||||
|
||||
### 3. 签名证书 Identity
|
||||
|
||||
1. 打开 Xcode 或 Keychain Access
|
||||
2. 查看已安装的开发者证书
|
||||
3. 证书名称类似:"Developer ID Application: Your Name (TEAM_ID)"
|
||||
4. 复制完整证书名称作为 `IDENTITY`
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 推荐发布流程 (使用 release.sh)
|
||||
|
||||
1. 确保代码质量符合要求
|
||||
2. 使用发布脚本升级版本:
|
||||
|
||||
```bash
|
||||
# 修复版本
|
||||
./scripts/release.sh patch
|
||||
|
||||
# 功能版本
|
||||
./scripts/release.sh minor
|
||||
|
||||
# 重大版本
|
||||
./scripts/release.sh major
|
||||
|
||||
# 预发布版本
|
||||
./scripts/release.sh prerelease
|
||||
```
|
||||
|
||||
3. 脚本会自动:
|
||||
- 运行代码质量检查
|
||||
- 升级版本号
|
||||
- 创建 git tag
|
||||
- 推送到 main 分支
|
||||
4. GitHub Actions 自动触发构建
|
||||
5. 在 Deployments 页面审批发布
|
||||
6. 编辑 Draft Release 内容
|
||||
7. 手动发布给用户
|
||||
|
||||
### 直接推送发布
|
||||
|
||||
1. 手动修改 `package.json` 中的版本号
|
||||
2. 提交并推送到 `main` 分支
|
||||
3. GitHub Actions 将自动构建并创建 Draft Release
|
||||
|
||||
### 版本管理规范
|
||||
|
||||
- `patch`: 修复bug (1.0.0 → 1.0.1)
|
||||
- `minor`: 新功能 (1.0.0 → 1.1.0)
|
||||
- `major`: 重大更新 (1.0.0 → 2.0.0)
|
||||
- `prerelease`: 预发布版本 (1.0.0 → 1.0.1-beta.0)
|
||||
|
||||
## 构建产物
|
||||
|
||||
成功构建后,将生成以下文件:
|
||||
|
||||
### macOS
|
||||
|
||||
- `.dmg` 文件 (Intel 和 Apple Silicon 版本)
|
||||
- 应用程序包
|
||||
|
||||
### Windows
|
||||
|
||||
- `.exe` NSIS 安装程序 (x64/arm64)
|
||||
- `.zip` 便携版应用 (x64/arm64)
|
||||
|
||||
### Linux
|
||||
|
||||
- `.deb` 安装包 (x64/arm64/armv7l)
|
||||
- `.AppImage` 便携式应用 (x64/arm64/armv7l)
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **Release创建失败 (403错误)**
|
||||
- 检查 GH_TOKEN 是否正确配置
|
||||
- 确认 token 格式为 `github_pat_` 开头
|
||||
- 验证 repository 和 environment 中都有 GH_TOKEN
|
||||
|
||||
2. **macOS 签名失败**
|
||||
- 检查 Apple ID 和密码是否正确
|
||||
- 确认 Team ID 和证书名称准确
|
||||
- 验证苹果开发者账号状态
|
||||
|
||||
3. **构建超时 (Windows)**
|
||||
- Windows 构建通常最慢 (可能40分钟+)
|
||||
- 考虑禁用 MSI target 加速构建
|
||||
|
||||
4. **重复tag错误**
|
||||
- CI/CD 会检查并跳过已存在的 tag
|
||||
- 如果手动创建了 tag,CI/CD 不会重复创建
|
||||
|
||||
### 调试方法
|
||||
|
||||
1. 查看 GitHub Actions 日志
|
||||
2. 本地运行相同的构建命令测试
|
||||
3. 检查 package.json 中的构建脚本
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. 定期更新 GitHub Actions 版本
|
||||
2. 使用最小权限原则配置 Secrets
|
||||
3. 定期审查和清理未使用的 Secrets
|
||||
4. 监控构建日志,避免敏感信息泄露
|
||||
|
||||
## 进阶配置
|
||||
|
||||
### 自动更新检查
|
||||
|
||||
可以集成应用内自动更新功能,配合 GitHub Releases API 实现自动更新提醒。
|
||||
|
||||
### 多环境部署
|
||||
|
||||
可以扩展工作流支持开发、测试、生产环境的分别部署。
|
||||
|
||||
### 性能优化
|
||||
|
||||
- 使用构建缓存加速构建
|
||||
- 并行构建不同平台
|
||||
- 优化依赖安装速度
|
||||
@@ -0,0 +1,72 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Report a bug or unexpected behavior in AionUi
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you using?
|
||||
options:
|
||||
- macOS (Apple Silicon)
|
||||
- macOS (Intel)
|
||||
- Windows
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: AionUi Version
|
||||
description: What version of AionUi are you using?
|
||||
placeholder: e.g., 1.2.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug_description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: Describe what happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What should have happened?
|
||||
placeholder: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual_behavior
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened?
|
||||
placeholder: What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
placeholder: Screenshots, error logs, etc.
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 Community Discussions
|
||||
url: https://github.com/iOfficeAI/AionUi/discussions
|
||||
about: Ask questions, share ideas, and discuss AionUi with the community
|
||||
- name: 📖 Documentation
|
||||
url: https://github.com/iOfficeAI/AionUi/wiki
|
||||
about: Check our documentation for setup guides, configuration, and troubleshooting
|
||||
- name: 🔒 Security Issues
|
||||
url: mailto:security@aionui.com
|
||||
about: Report security vulnerabilities privately (DO NOT create public issues)
|
||||
@@ -0,0 +1,53 @@
|
||||
name: ✨ Feature Request
|
||||
description: Suggest an idea for AionUi
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature_description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: A clear and concise description of the feature you'd like to see.
|
||||
placeholder: Describe the feature you'd like to see...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem_statement
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: What problem does this feature solve?
|
||||
placeholder: What problem or limitation would this feature address?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed_solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe the solution you'd like to see implemented.
|
||||
placeholder: How would you like this feature to work?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: feature_category
|
||||
attributes:
|
||||
label: Feature Category
|
||||
description: Which category does this feature belong to?
|
||||
options:
|
||||
- UI/UX Improvement
|
||||
- AI Model Integration
|
||||
- File Management
|
||||
- Multi-Agent Support
|
||||
- Performance
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the feature request here.
|
||||
placeholder: Use cases, examples, mockups, etc.
|
||||
@@ -0,0 +1,47 @@
|
||||
name: ❓ Question
|
||||
description: Ask a question about AionUi
|
||||
title: '[Question]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What would you like to know?
|
||||
placeholder: Ask your question here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: question_category
|
||||
attributes:
|
||||
label: Question Category
|
||||
description: What type of question is this?
|
||||
options:
|
||||
- Installation & Setup
|
||||
- Configuration
|
||||
- Usage & Features
|
||||
- Troubleshooting
|
||||
- Development
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: Provide context about your question. What are you trying to achieve?
|
||||
placeholder: Describe what you're trying to do or understand...
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you using?
|
||||
options:
|
||||
- macOS (Apple Silicon)
|
||||
- macOS (Intel)
|
||||
- Windows
|
||||
- Linux
|
||||
- Not applicable
|
||||
@@ -0,0 +1,146 @@
|
||||
name: 'Call OpenAI'
|
||||
description: 'Call OpenAI Chat Completions API with retry logic. Reads system_prompt.txt and user_prompt.txt from RUNNER_TEMP.'
|
||||
|
||||
inputs:
|
||||
openai_api_key:
|
||||
description: 'OpenAI API key'
|
||||
required: true
|
||||
output_file:
|
||||
description: 'Output filename in RUNNER_TEMP for the GPT response'
|
||||
required: false
|
||||
default: 'gpt_response.txt'
|
||||
diff_truncated:
|
||||
description: 'Whether the diff was truncated (for appending note)'
|
||||
required: false
|
||||
default: 'false'
|
||||
contents_truncated:
|
||||
description: 'Whether file contents were truncated (for appending note)'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: 'call'
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
OPENAI_API_KEY: '${{ inputs.openai_api_key }}'
|
||||
OUTPUT_FILE: '${{ inputs.output_file }}'
|
||||
DIFF_TRUNCATED: '${{ inputs.diff_truncated }}'
|
||||
CONTENTS_TRUNCATED: '${{ inputs.contents_truncated }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
core.setFailed('OPENAI_API_KEY is not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
// Read prompts from temp files
|
||||
let systemPrompt, userPrompt;
|
||||
try {
|
||||
systemPrompt = fs.readFileSync(`${tmpDir}/system_prompt.txt`, 'utf-8');
|
||||
userPrompt = fs.readFileSync(`${tmpDir}/user_prompt.txt`, 'utf-8');
|
||||
} catch (err) {
|
||||
core.setFailed(`Failed to read prompt files: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call OpenAI Chat Completions API with retry logic
|
||||
async function callOpenAI(retries = 2) {
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-5.2',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: 0.1,
|
||||
max_completion_tokens: 16384,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
if (attempt < retries) {
|
||||
const delay = Math.pow(2, attempt + 1) * 1000;
|
||||
core.warning(`OpenAI API returned ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${retries})`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = data?.choices?.[0]?.message?.content;
|
||||
if (typeof content !== 'string' || content.trim().length === 0) {
|
||||
throw new Error(
|
||||
`OpenAI response missing choices[0].message.content. Payload keys: ${Object.keys(data || {}).join(', ')}`
|
||||
);
|
||||
}
|
||||
return content;
|
||||
} catch (error) {
|
||||
const isRetryable = attempt < retries && (
|
||||
error.cause?.code === 'ECONNRESET' ||
|
||||
error.cause?.code === 'ETIMEDOUT' ||
|
||||
error.cause?.code === 'ENOTFOUND' ||
|
||||
error.message.includes('fetch') ||
|
||||
error.message.includes('network') ||
|
||||
error.message.includes('socket')
|
||||
);
|
||||
if (isRetryable) {
|
||||
const delay = Math.pow(2, attempt + 1) * 1000;
|
||||
core.warning(`Network error, retrying in ${delay}ms: ${error.message}`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gptContent = await callOpenAI();
|
||||
if (!gptContent) {
|
||||
core.setFailed('GPT returned empty response');
|
||||
return;
|
||||
}
|
||||
|
||||
// Append truncation warning if inputs were truncated
|
||||
const diffTruncated = process.env.DIFF_TRUNCATED === 'true';
|
||||
const contentsTruncated = process.env.CONTENTS_TRUNCATED === 'true';
|
||||
let truncationNote = '';
|
||||
if (diffTruncated || contentsTruncated) {
|
||||
const parts = [];
|
||||
if (diffTruncated) parts.push('diff (>100K chars)');
|
||||
if (contentsTruncated) parts.push('file contents (>80K chars)');
|
||||
truncationNote = [
|
||||
'',
|
||||
'> **Note**: The following inputs were truncated due to size limits: ' + parts.join(', ') + '.',
|
||||
'> Please review the omitted portions manually.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const withNote = gptContent + truncationNote;
|
||||
|
||||
// Truncate if exceeds GitHub's 65536 char limit
|
||||
const MAX_LENGTH = 65000;
|
||||
const finalContent = withNote.length > MAX_LENGTH
|
||||
? withNote.substring(0, MAX_LENGTH) + '\n\n---\n*[Output truncated due to length]*'
|
||||
: withNote;
|
||||
|
||||
const outputFile = process.env.OUTPUT_FILE || 'gpt_response.txt';
|
||||
fs.writeFileSync(`${tmpDir}/${outputFile}`, finalContent);
|
||||
core.info(`GPT response written to ${outputFile} (${finalContent.length} chars)`);
|
||||
@@ -0,0 +1,28 @@
|
||||
name: '🔍 Resolve PR Context'
|
||||
description: 'Resolve the correct ref and base branch for pull_request and workflow_dispatch triggers. Must be called after actions/checkout.'
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number (only used when triggered via workflow_dispatch)'
|
||||
required: false
|
||||
default: ''
|
||||
github_token:
|
||||
description: 'GitHub token for gh API calls'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Resolve PR context
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github_token }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_INFO=$(gh api repos/${{ github.repository }}/pulls/${{ inputs.pr_number }})
|
||||
HEAD_SHA=$(echo "$PR_INFO" | jq -r '.head.sha')
|
||||
BASE_REF=$(echo "$PR_INFO" | jq -r '.base.ref')
|
||||
git fetch origin "refs/pull/${{ inputs.pr_number }}/head"
|
||||
git checkout "$HEAD_SHA"
|
||||
echo "PR_BASE_REF=$BASE_REF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "PR_BASE_REF=${{ github.base_ref }}" >> $GITHUB_ENV
|
||||
fi
|
||||
@@ -0,0 +1,117 @@
|
||||
name: 'Gather PR Diff'
|
||||
description: 'Fetch PR diff, changed file list, and statistics via GitHub API'
|
||||
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number (required for workflow_dispatch triggers)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
skip:
|
||||
description: 'Whether to skip processing (empty diff)'
|
||||
value: '${{ steps.gather.outputs.skip }}'
|
||||
pr_number:
|
||||
description: 'Resolved PR number'
|
||||
value: '${{ steps.gather.outputs.pr_number }}'
|
||||
additions:
|
||||
description: 'Total lines added'
|
||||
value: '${{ steps.gather.outputs.additions }}'
|
||||
deletions:
|
||||
description: 'Total lines deleted'
|
||||
value: '${{ steps.gather.outputs.deletions }}'
|
||||
total_lines:
|
||||
description: 'Total lines changed (additions + deletions)'
|
||||
value: '${{ steps.gather.outputs.total_lines }}'
|
||||
file_count:
|
||||
description: 'Number of changed files'
|
||||
value: '${{ steps.gather.outputs.file_count }}'
|
||||
diff_truncated:
|
||||
description: 'Whether the diff was truncated'
|
||||
value: '${{ steps.gather.outputs.diff_truncated }}'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: 'gather'
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
// Support both PR event trigger and manual workflow_dispatch
|
||||
const prNumber = context.payload.pull_request?.number
|
||||
|| Number(process.env.INPUT_PR_NUMBER)
|
||||
|| null;
|
||||
|
||||
if (!prNumber) {
|
||||
core.setFailed('No PR number found. Provide pr_number input for manual trigger.');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
|
||||
// Fetch diff (returns raw text when using diff media type)
|
||||
const { data: diff } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
mediaType: { format: 'diff' },
|
||||
});
|
||||
|
||||
if (!diff || String(diff).trim().length === 0) {
|
||||
core.warning('PR has an empty diff, skipping');
|
||||
core.setOutput('skip', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all changed files (paginated)
|
||||
const allFiles = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
allFiles.push(...files);
|
||||
if (files.length < 100) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
const additions = allFiles.reduce((s, f) => s + f.additions, 0);
|
||||
const deletions = allFiles.reduce((s, f) => s + f.deletions, 0);
|
||||
const totalLines = additions + deletions;
|
||||
|
||||
// Truncate diff to 100K characters
|
||||
const MAX_DIFF = 100000;
|
||||
const diffStr = String(diff);
|
||||
const diffTruncated = diffStr.length > MAX_DIFF;
|
||||
const truncatedDiff = diffTruncated
|
||||
? diffStr.substring(0, MAX_DIFF) + '\n\n... [diff truncated at 100K chars]'
|
||||
: diffStr;
|
||||
|
||||
// Build compact file list
|
||||
const fileList = allFiles.map(f => ({
|
||||
filename: f.filename,
|
||||
status: f.status,
|
||||
additions: f.additions,
|
||||
deletions: f.deletions,
|
||||
}));
|
||||
|
||||
// Write large data to temp files (avoid output size limits)
|
||||
fs.writeFileSync(`${tmpDir}/pr_diff.txt`, truncatedDiff);
|
||||
fs.writeFileSync(`${tmpDir}/file_list.json`, JSON.stringify(fileList));
|
||||
|
||||
// Only small values as step outputs
|
||||
core.setOutput('skip', 'false');
|
||||
core.setOutput('additions', String(additions));
|
||||
core.setOutput('deletions', String(deletions));
|
||||
core.setOutput('total_lines', String(totalLines));
|
||||
core.setOutput('file_count', String(allFiles.length));
|
||||
core.setOutput('diff_truncated', String(diffTruncated));
|
||||
@@ -0,0 +1,78 @@
|
||||
name: 'Read File Contents'
|
||||
description: 'Read changed file contents for cross-file analysis. Requires checkout and gather-pr-diff to have run first.'
|
||||
|
||||
outputs:
|
||||
contents_truncated:
|
||||
description: 'Whether file contents were truncated'
|
||||
value: '${{ steps.read.outputs.contents_truncated }}'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: 'read'
|
||||
uses: 'actions/github-script@v7'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
const fileList = JSON.parse(fs.readFileSync(`${tmpDir}/file_list.json`, 'utf-8'));
|
||||
|
||||
const SKIP_PATTERNS = [
|
||||
/package-lock\.json$/,
|
||||
/yarn\.lock$/,
|
||||
/pnpm-lock\.yaml$/,
|
||||
/\.lock$/,
|
||||
/\.png$/i, /\.jpg$/i, /\.jpeg$/i, /\.gif$/i, /\.svg$/i, /\.ico$/i, /\.webp$/i,
|
||||
/\.woff2?$/i, /\.ttf$/i, /\.eot$/i, /\.otf$/i,
|
||||
/\.mp3$/i, /\.mp4$/i, /\.wav$/i, /\.ogg$/i, /\.webm$/i,
|
||||
/\.zip$/i, /\.tar$/i, /\.gz$/i, /\.br$/i,
|
||||
/\.pdf$/i, /\.wasm$/i,
|
||||
/\.map$/,
|
||||
/dist\//,
|
||||
/\.min\./,
|
||||
/node_modules\//,
|
||||
];
|
||||
|
||||
// Priority scoring: lower = more important, read first
|
||||
function filePriority(filename) {
|
||||
if (/^src\/(process|agent|webserver)\//.test(filename)) return 0;
|
||||
if (/^src\/channels\//.test(filename)) return 1;
|
||||
if (/^src\/common\//.test(filename)) return 2;
|
||||
if (/^src\/worker\//.test(filename)) return 3;
|
||||
if (/^src\/renderer\//.test(filename)) return 4;
|
||||
if (/\.(ts|tsx|js|jsx)$/.test(filename)) return 5;
|
||||
return 6;
|
||||
}
|
||||
|
||||
const MAX_CONTENT = 80000;
|
||||
let totalSize = 0;
|
||||
const contents = [];
|
||||
|
||||
const sortedFiles = fileList
|
||||
.filter(f => f.status !== 'removed')
|
||||
.filter(f => !SKIP_PATTERNS.some(p => p.test(f.filename)))
|
||||
.sort((a, b) => filePriority(a.filename) - filePriority(b.filename));
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
if (totalSize >= MAX_CONTENT) {
|
||||
contents.push(`\n--- [remaining files omitted, ${MAX_CONTENT / 1000}K content limit reached] ---`);
|
||||
break;
|
||||
}
|
||||
const filePath = path.join(process.cwd(), file.filename);
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const remaining = MAX_CONTENT - totalSize;
|
||||
const truncated = content.length > remaining
|
||||
? content.substring(0, remaining) + '\n... [file truncated]'
|
||||
: content;
|
||||
contents.push(`=== FILE: ${file.filename} ===\n${truncated}\n`);
|
||||
totalSize += truncated.length;
|
||||
} catch {
|
||||
// File might not exist in checkout (e.g. binary or renamed from)
|
||||
}
|
||||
}
|
||||
|
||||
const contentsTruncated = totalSize >= MAX_CONTENT;
|
||||
fs.writeFileSync(`${tmpDir}/file_contents.txt`, contents.join('\n'));
|
||||
core.setOutput('contents_truncated', String(contentsTruncated));
|
||||
@@ -0,0 +1,61 @@
|
||||
# Pull Request
|
||||
|
||||
> Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting. PRs that ignore the rules below may be closed and asked to resubmit.
|
||||
|
||||
## Description
|
||||
|
||||
<!-- Provide a clear and concise description of what this PR does and why. -->
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Link related issues. "Closes #123" / "Fixes #123" will auto-close the issue on merge. -->
|
||||
|
||||
- Closes #
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] `fix` — Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] `feat` — New feature (non-breaking change which adds functionality)
|
||||
- [ ] `perf` — Performance improvement
|
||||
- [ ] `refactor` — Code restructuring (no behavior change)
|
||||
- [ ] Breaking change (fix or feature that would break existing functionality)
|
||||
- [ ] `docs` — Documentation update
|
||||
|
||||
## Atomic PR Checklist (Rule 1)
|
||||
|
||||
- [ ] This PR contains **exactly one** feature or bug fix that cannot be further decomposed
|
||||
- [ ] The PR title follows Conventional Commit format: `<type>(<scope>): <subject>` (English)
|
||||
|
||||
## Local Checks (Rule 3)
|
||||
|
||||
<!-- Run these before pushing — CI will reject the PR if they fail. -->
|
||||
|
||||
- [ ] `bun run format` — formatting passes
|
||||
- [ ] `bun run lint` — no lint errors (skip if no `.ts`/`.tsx` changed)
|
||||
- [ ] `bunx tsc --noEmit` — no type errors (skip if no `.ts`/`.tsx` changed)
|
||||
- [ ] `bunx vitest run` — tests pass
|
||||
- [ ] i18n validated (`bun run i18n:types` + `node scripts/check-i18n.js`) — only if `src/renderer/`, `locales/`, or `src/common/config/i18n/` changed; N/A otherwise
|
||||
- [ ] New/changed user-facing text uses i18n keys (no hardcoded strings)
|
||||
|
||||
## Runtime Verification
|
||||
|
||||
<!-- Which platforms did you actually run and verify on? -->
|
||||
|
||||
- [ ] Verified on macOS
|
||||
- [ ] Verified on Windows
|
||||
- [ ] Verified on Linux
|
||||
- [ ] I have performed a self-review of my own code
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- If applicable, add screenshots or recordings to help explain your changes. -->
|
||||
|
||||
## Additional Context
|
||||
|
||||
<!-- Add any other context about the pull request here. -->
|
||||
|
||||
---
|
||||
|
||||
<!-- Commits and PR titles must NOT contain AI signatures (Co-Authored-By, "Generated with", etc.). -->
|
||||
|
||||
**Thank you for contributing to AionUi! 🎉**
|
||||
@@ -0,0 +1,166 @@
|
||||
# GPT Workflows
|
||||
|
||||
本项目使用 GPT 驱动的 GitHub Actions 工作流辅助 PR 审查。
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
.github/
|
||||
├── actions/ # 公共 Composite Actions
|
||||
│ ├── gather-pr-diff/action.yml # 收集 PR diff 和变更文件列表
|
||||
│ ├── read-file-contents/action.yml # 按优先级读取变更文件内容
|
||||
│ └── call-openai/action.yml # 调用 OpenAI API(含重试逻辑)
|
||||
└── workflows/
|
||||
├── gpt-review.yml # 代码质量审查
|
||||
├── gpt-pr-assessment.yml # PR 价值评估
|
||||
└── pr-checks.yml # PR 检查入口(触发 gpt-review + gpt-pr-assessment)
|
||||
```
|
||||
|
||||
## 两个 GPT 工作流对比
|
||||
|
||||
| | GPT Review | GPT PR Assessment |
|
||||
| ------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **目的** | 代码质量审查(bug、安全、性能) | 维护者价值评估(合并优先级、风险) |
|
||||
| **角色** | 代码审查专家 | 项目维护者 / 技术负责人 |
|
||||
| **触发** | PR 创建时自动触发(via pr-checks.yml)+ 手动触发(workflow_dispatch) | 外部贡献者 PR 自动触发(via pr-checks.yml)+ 手动触发(workflow_dispatch) |
|
||||
| **额外数据** | 无 | 关联 Issue 内容 |
|
||||
| **输出方式** | PR Review(createReview) | Issue Comment(可更新,不重复创建) |
|
||||
| **输出模板** | 按严重性分级的问题列表 | 7 维度结构化评估报告 |
|
||||
|
||||
## 公共 Actions
|
||||
|
||||
三个 Composite Action 封装了两个工作流的公共逻辑,避免重复代码:
|
||||
|
||||
### `gather-pr-diff`
|
||||
|
||||
收集 PR 的 diff 和变更文件列表。
|
||||
|
||||
- **输入**: `pr_number`(手动触发时需要)
|
||||
- **输出**: `skip`, `pr_number`, `additions`, `deletions`, `total_lines`, `file_count`, `diff_truncated`
|
||||
- **临时文件**: `pr_diff.txt`, `file_list.json`(写入 `RUNNER_TEMP`)
|
||||
|
||||
### `read-file-contents`
|
||||
|
||||
按优先级顺序读取变更文件的完整内容,供 GPT 做跨文件分析。
|
||||
|
||||
- **输出**: `contents_truncated`
|
||||
- **临时文件**: `file_contents.txt`(写入 `RUNNER_TEMP`)
|
||||
- **前置条件**: 需要先运行 `checkout` 和 `gather-pr-diff`
|
||||
- **限制**: 跳过锁文件/二进制文件,总内容上限 80K 字符
|
||||
|
||||
文件读取优先级(由高到低):
|
||||
|
||||
1. `packages/desktop/src/process/`, `packages/desktop/src/process/agent/`, `packages/desktop/src/process/webserver/` — 核心后端
|
||||
2. `packages/desktop/src/process/channels/` — Agent 通信
|
||||
3. `packages/desktop/src/common/` — 公共模块
|
||||
4. `packages/desktop/src/process/worker/` — Worker 进程
|
||||
5. `packages/desktop/src/renderer/` — 前端 UI
|
||||
6. 其他 `.ts/.tsx/.js/.jsx` 文件
|
||||
7. 其余文件
|
||||
|
||||
### `call-openai`
|
||||
|
||||
调用 OpenAI Chat Completions API,包含自动重试、截断提示和长度限制处理。
|
||||
|
||||
- **输入**: `openai_api_key`, `output_file`, `diff_truncated`, `contents_truncated`
|
||||
- **临时文件读取**: `system_prompt.txt`, `user_prompt.txt`(从 `RUNNER_TEMP` 读取)
|
||||
- **临时文件写入**: `{output_file}`(写入 `RUNNER_TEMP`)
|
||||
- **模型**: `gpt-5.2`
|
||||
- **重试策略**: 最多 2 次重试,指数退避(429/5xx 状态码和网络错误)
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ gather-pr-diff │
|
||||
│ (GitHub API) │
|
||||
└────────┬────────────┘
|
||||
│
|
||||
pr_diff.txt, file_list.json
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
▼ │ ▼
|
||||
┌────────────────┐ │ ┌───────────────────┐
|
||||
│ read-file- │ │ │ fetch PR metadata │
|
||||
│ contents │ │ │ + linked issues │
|
||||
│ (本地文件系统) │ │ │ (assessment only) │
|
||||
└───────┬────────┘ │ └────────┬──────────┘
|
||||
│ │ │
|
||||
file_contents.txt │ pr_meta.json,
|
||||
│ │ linked_issues.json
|
||||
└──────┬───────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────────┐ │
|
||||
│ Construct GPT │◄───────────┘
|
||||
│ Prompts │
|
||||
│ (各工作流自定义) │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
system_prompt.txt, user_prompt.txt
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ call-openai │
|
||||
│ (OpenAI API) │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
{output_file}
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Post Comment │
|
||||
│ (各工作流自定义) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## 语言检测
|
||||
|
||||
两个工作流都会自动检测 PR 标题和描述的语言,并使用相同语言输出回复:
|
||||
|
||||
- PR 内容为中文 → 中文评论
|
||||
- PR 内容为英文 → 英文评论
|
||||
- 混合或无法判断 → 默认英文
|
||||
|
||||
## 使用方式
|
||||
|
||||
### GPT Review(自动 + 手动触发)
|
||||
|
||||
**自动触发**:通过 `pr-checks.yml` 在 PR 首次提交时自动触发,无需手动操作。
|
||||
|
||||
**手动触发**:
|
||||
|
||||
1. 进入 GitHub 仓库 → Actions 页面
|
||||
2. 左侧选择 **GPT Review**
|
||||
3. 点击 **Run workflow**
|
||||
4. 输入 PR number
|
||||
5. 等待执行完成,审查结果将以 PR Review 形式出现
|
||||
|
||||
### GPT PR Assessment(自动 + 手动触发)
|
||||
|
||||
**自动触发**:当非项目成员(即 `author_association` 既不是 `OWNER` 也不是 `MEMBER`)首次提交 PR 时,`pr-checks.yml` 会在代码质量检查通过后自动触发评估。
|
||||
|
||||
**手动触发**:
|
||||
|
||||
1. 进入 GitHub 仓库 → Actions 页面
|
||||
2. 左侧选择 **GPT PR Assessment**
|
||||
3. 点击 **Run workflow**
|
||||
4. 输入 PR number
|
||||
5. 等待执行完成,评估报告将作为评论出现在 PR 中
|
||||
|
||||
重复对同一 PR 触发时,评论会被**更新**而非重复创建。
|
||||
|
||||
## Secrets 配置
|
||||
|
||||
| Secret | 用途 |
|
||||
| ---------------- | ----------------------------------- |
|
||||
| `OPENAI_API_KEY` | OpenAI API 访问密钥,两个工作流共用 |
|
||||
|
||||
## 修改指南
|
||||
|
||||
- **更换模型**: 修改 `.github/actions/call-openai/action.yml` 中的 `model` 字段
|
||||
- **调整重试逻辑**: 修改 `.github/actions/call-openai/action.yml` 中的 `callOpenAI` 函数
|
||||
- **修改文件优先级**: 修改 `.github/actions/read-file-contents/action.yml` 中的 `filePriority` 函数
|
||||
- **修改 Review prompt**: 修改 `gpt-review.yml` 中的 "Construct GPT prompts" step
|
||||
- **修改 Assessment prompt**: 修改 `gpt-pr-assessment.yml` 中的 "Construct GPT prompts" step
|
||||
@@ -0,0 +1,687 @@
|
||||
name: Build Pipeline (Reusable)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
matrix:
|
||||
description: 'Build matrix as JSON string (passed via fromJSON in caller)'
|
||||
type: string
|
||||
required: true
|
||||
ref:
|
||||
description: 'Git ref to checkout (leave empty to use the triggering commit)'
|
||||
type: string
|
||||
default: ''
|
||||
skip_code_quality:
|
||||
description: 'Skip code quality checks (faster builds for debugging)'
|
||||
type: boolean
|
||||
default: false
|
||||
append_commit_hash:
|
||||
description: 'Append short commit hash to artifact names'
|
||||
type: boolean
|
||||
default: false
|
||||
upload_installers_only:
|
||||
description: 'Only upload primary installers (exe/msi/dmg/deb), skip zip/yml/blockmap'
|
||||
type: boolean
|
||||
default: false
|
||||
aioncore_run_id:
|
||||
description: 'Optional AionCore Manual Build workflow run id to bundle instead of the pinned release'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
AIONUI_HUB_TAG: 'dist-ebc6a2b'
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !inputs.skip_code_quality }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: bun run postinstall || true
|
||||
|
||||
- name: Run ESLint
|
||||
run: bun run lint
|
||||
|
||||
- name: Check Prettier formatting
|
||||
run: bun run format:check
|
||||
|
||||
- name: TypeScript type check
|
||||
run: bunx tsc --noEmit
|
||||
|
||||
- name: Run unit tests
|
||||
run: bunx vitest run
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: code-quality
|
||||
if: ${{ always() && (inputs.skip_code_quality || needs.code-quality.result == 'success') }}
|
||||
|
||||
outputs:
|
||||
commit-short: ${{ steps.commit.outputs.short }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(inputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT=$(git rev-parse --short HEAD)
|
||||
echo "short=$SHORT" >> $GITHUB_OUTPUT
|
||||
echo "Commit: $SHORT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# Restore Electron/Electron-Builder caches before any build-related install
|
||||
- name: Cache Electron artifacts
|
||||
id: electron-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/.cache/electron
|
||||
${{ runner.temp }}/.cache/electron-builder
|
||||
~/.cache/electron
|
||||
~/.cache/electron-builder
|
||||
${{ env.LOCALAPPDATA }}\electron-builder\Cache
|
||||
key: electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('package.json', 'bun.lock') }}
|
||||
restore-keys: |
|
||||
electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-
|
||||
electron-cache-${{ matrix.platform }}-
|
||||
|
||||
- name: Cache status
|
||||
run: echo "electron-cache-hit=${{ steps.electron-cache.outputs.cache-hit }}"
|
||||
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 30
|
||||
command: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: bun run postinstall || true
|
||||
|
||||
- name: Setup Windows native dependencies (for Windows only)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
run: |
|
||||
bun install --global node-gyp
|
||||
|
||||
- name: Install MSVC ARM64 toolchain (Windows ARM64 only)
|
||||
if: matrix.platform == 'windows-arm64'
|
||||
continue-on-error: true
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 60
|
||||
shell: pwsh
|
||||
command: |
|
||||
choco install visualstudio2022buildtools --yes --params "'--add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --add Microsoft.VisualStudio.Component.Windows11SDK.22000 --includeRecommended --includeOptional'"
|
||||
|
||||
- name: Setup Windows build environment (for Windows only)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
run: |
|
||||
echo "Setting Windows SDK version for ConPty support"
|
||||
echo "WindowsTargetPlatformVersion=10.0.19041.0" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup MSBuild (for Windows only)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
vs-version: '17.0'
|
||||
|
||||
- name: Rebuild native modules for Electron
|
||||
if: "!startsWith(matrix.platform, 'windows')"
|
||||
run: bunx electron-builder install-app-deps
|
||||
env:
|
||||
npm_config_runtime: electron
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||
|
||||
- name: Print build configuration
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "BUILD CONFIGURATION"
|
||||
echo "=========================================="
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "Target Architecture: ${{ matrix.arch }}"
|
||||
echo "Build Command: ${{ matrix.command }}"
|
||||
echo "Runner OS: ${{ runner.os }}"
|
||||
echo "Runner Arch: ${{ runner.arch }}"
|
||||
echo "Ref: ${{ inputs.ref || '(default)' }}"
|
||||
echo "Commit: ${{ steps.commit.outputs.short }}"
|
||||
echo "=========================================="
|
||||
|
||||
- name: Resolve Sentry release name
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
RELEASE_NAME="${GITHUB_REF_NAME}"
|
||||
elif [[ "$GITHUB_REF_NAME" == "dev" ]]; then
|
||||
RELEASE_NAME="v${VERSION}-dev-${{ steps.commit.outputs.short }}"
|
||||
else
|
||||
RELEASE_NAME="v${VERSION}-${{ steps.commit.outputs.short }}"
|
||||
fi
|
||||
|
||||
echo "SENTRY_RELEASE=$RELEASE_NAME" >> "$GITHUB_ENV"
|
||||
echo "Sentry release: $RELEASE_NAME"
|
||||
|
||||
- name: Configure Sentry source map upload owner
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ matrix.platform }}" == "linux-x64" ]]; then
|
||||
echo "SENTRY_UPLOAD_SOURCE_MAPS=true" >> "$GITHUB_ENV"
|
||||
echo "Sentry source maps will be uploaded by linux-x64"
|
||||
else
|
||||
echo "SENTRY_UPLOAD_SOURCE_MAPS=false" >> "$GITHUB_ENV"
|
||||
echo "Sentry source map upload skipped for ${{ matrix.platform }}"
|
||||
fi
|
||||
|
||||
- name: Validate Sentry source map upload configuration
|
||||
if: matrix.platform == 'linux-x64'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
missing=0
|
||||
|
||||
for name in SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT SENTRY_RELEASE; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
echo "::error title=Sentry configuration missing::$name is required for desktop source map uploads"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Sentry source map upload configured for $SENTRY_ORG/$SENTRY_PROJECT ($SENTRY_RELEASE)"
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
# macOS code signing certificate installation
|
||||
- name: Setup macOS code signing (macOS only)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD || 'temp-keychain-password' }}
|
||||
run: |
|
||||
if [ -n "$BUILD_CERTIFICATE_BASE64" ]; then
|
||||
echo "Installing code signing certificate..."
|
||||
|
||||
# Create certificate file
|
||||
echo $BUILD_CERTIFICATE_BASE64 | base64 --decode > certificate.p12
|
||||
|
||||
# Create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Import certificate
|
||||
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Clean up certificate file
|
||||
rm certificate.p12
|
||||
|
||||
echo "Certificate imported successfully"
|
||||
else
|
||||
echo "No certificate provided - building unsigned application"
|
||||
echo "To enable code signing, add BUILD_CERTIFICATE_BASE64 and P12_PASSWORD secrets"
|
||||
fi
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: startsWith(matrix.platform, 'linux')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential python3 python3-pip pkg-config libsqlite3-dev fakeroot dpkg-dev rpm libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxss1 libatspi2.0-dev libgtk-3-dev libxrandr2 libasound2-dev
|
||||
|
||||
- name: Rebuild native modules for Electron (Windows)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
shell: pwsh
|
||||
continue-on-error: false
|
||||
run: |
|
||||
$arch = "${{ matrix.arch }}"
|
||||
$electronVer = (node -p "require('./package.json').devDependencies.electron.replace(/[\^~]/g, '')").Trim()
|
||||
|
||||
Write-Host "=========================================="
|
||||
Write-Host "Rebuilding native modules for Electron $electronVer ($arch)"
|
||||
Write-Host "=========================================="
|
||||
|
||||
# Strategy: Try prebuild-install first (faster, more reliable)
|
||||
# Fallback to electron-rebuild only if prebuild-install fails
|
||||
$success = $false
|
||||
|
||||
# Step 1: Try prebuild-install (downloads prebuilt binaries)
|
||||
Write-Host ""
|
||||
Write-Host "[Step 1] Trying prebuild-install..."
|
||||
$prebuildResult = $null
|
||||
try {
|
||||
$prebuildResult = bun x --yes prebuild-install --runtime=electron --target=$electronVer --platform=win32 --arch=$arch --force 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ prebuild-install succeeded"
|
||||
$success = $true
|
||||
} else {
|
||||
Write-Host "⚠️ prebuild-install exited with code $LASTEXITCODE"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ prebuild-install failed: $_"
|
||||
}
|
||||
|
||||
# Step 2: Verify or fallback to electron-rebuild
|
||||
$sqliteNode = "node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if (-not (Test-Path $sqliteNode)) {
|
||||
Write-Host ""
|
||||
Write-Host "[Step 2] prebuild-install didn't produce binary, trying electron-rebuild..."
|
||||
try {
|
||||
bunx electron-rebuild -f -w better-sqlite3
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ electron-rebuild succeeded"
|
||||
$success = $true
|
||||
}
|
||||
} catch {
|
||||
Write-Host "❌ electron-rebuild failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Final verification
|
||||
Write-Host ""
|
||||
Write-Host "Verifying native modules..."
|
||||
if (Test-Path $sqliteNode) {
|
||||
$size = [math]::Round((Get-Item $sqliteNode).Length / 1MB, 1)
|
||||
Write-Host "✅ better-sqlite3 found: $sqliteNode ($size MB)"
|
||||
} else {
|
||||
Write-Error "❌ better-sqlite3 native module not found at: $sqliteNode"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=========================================="
|
||||
Write-Host "✅ All critical native modules verified"
|
||||
Write-Host "=========================================="
|
||||
exit 0
|
||||
env:
|
||||
npm_config_runtime: electron
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
# Don't force build_from_source - let prebuild-install try first
|
||||
MSVS_VERSION: 2022
|
||||
GYP_MSVS_VERSION: 2022
|
||||
|
||||
- name: Prepare aioncore binary
|
||||
shell: bash
|
||||
run: node scripts/prepareAioncore.js
|
||||
env:
|
||||
# Version is pinned in repo-root package.json (aioncoreVersion).
|
||||
# Set AIONUI_BACKEND_VERSION here only to override for ad-hoc builds.
|
||||
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
|
||||
AIONUI_BACKEND_ARCH: ${{ matrix.arch }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build with electron-builder (Windows)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
id: windows-build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Command = $env:WINDOWS_BUILD_COMMAND
|
||||
$BuildFailed = $false
|
||||
|
||||
try {
|
||||
Write-Host "Running Windows build command: $Command"
|
||||
iex $Command
|
||||
Write-Host "Windows build completed successfully"
|
||||
} catch {
|
||||
$BuildFailed = $true
|
||||
Write-Warning "${{ matrix.platform }} build failed but will not block the workflow"
|
||||
Write-Host "::notice title=Windows build skipped::${{ matrix.platform }} build failed (see logs above) but workflow is allowed to continue."
|
||||
}
|
||||
|
||||
if ($BuildFailed) {
|
||||
"result=failure" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
} else {
|
||||
"result=success" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
env:
|
||||
WINDOWS_BUILD_COMMAND: ${{ matrix.command }}
|
||||
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
|
||||
# Node.js memory limit for vite bundling (prevents OOM during build)
|
||||
# Node.js 内存限制,防止 vite 打包时内存不足
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
# Target arch / 目标架构
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
# Native module compilation / 原生模块编译
|
||||
npm_config_runtime: electron
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
npm_config_build_from_source: true
|
||||
npm_config_cache: ${{ runner.temp }}/.npm
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||
# Signing & IDs / 签名与应用 ID 配置
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
appleId: ${{ secrets.APPLE_ID }}
|
||||
appleIdPassword: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
teamId: ${{ secrets.TEAM_ID }}
|
||||
identity: ${{ secrets.IDENTITY }}
|
||||
CSC_NAME: ${{ secrets.IDENTITY }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
# Windows build flags / Windows 构建参数
|
||||
MSVS_VERSION: 2022
|
||||
GYP_MSVS_VERSION: 2022
|
||||
WindowsTargetPlatformVersion: 10.0.19041.0
|
||||
_WIN32_WINNT: 0x0A00
|
||||
# Sentry DSN (injected at build time via vite define)
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
# CI flag & GitHub token / CI 标识与 GitHub Token
|
||||
CI: true
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Clean up any stale disk images before DMG creation (macOS only)
|
||||
# 清理可能残留的磁盘镜像,避免 hdiutil "Device not configured" 错误
|
||||
- name: Clean up stale disk images (macOS only)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
run: |
|
||||
echo "Cleaning up any stale disk images..."
|
||||
# Detach any mounted disk images
|
||||
hdiutil info 2>/dev/null | grep '/dev/disk' | awk '{print $1}' | while read disk; do
|
||||
echo "Detaching $disk..."
|
||||
hdiutil detach "$disk" -force 2>/dev/null || true
|
||||
done
|
||||
# Clean up any temporary DMG files
|
||||
rm -f /tmp/*.dmg 2>/dev/null || true
|
||||
echo "Cleanup complete"
|
||||
|
||||
# macOS: Build with notarization - DMG failure = CI failure, notarization failure = warning only
|
||||
# macOS: 构建并公证 - DMG 失败 = CI 失败,公证失败 = 仅警告
|
||||
- name: Build with electron-builder (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
id: macos-build
|
||||
shell: bash
|
||||
run: |
|
||||
set +e # Handle errors manually
|
||||
|
||||
# Run build command
|
||||
${{ matrix.command }} 2>&1 | tee "${RUNNER_TEMP}/build.log"
|
||||
BUILD_EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
# Check if DMG was created (most important artifact)
|
||||
DMG_EXISTS=false
|
||||
if ls out/*.dmg 1>/dev/null 2>&1; then
|
||||
DMG_EXISTS=true
|
||||
echo "✅ DMG created successfully"
|
||||
fi
|
||||
|
||||
# If build succeeded completely
|
||||
if [ $BUILD_EXIT_CODE -eq 0 ]; then
|
||||
echo "✅ Build completed successfully"
|
||||
echo "notarization_status=success" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build failed - check if it's just notarization failure
|
||||
if [ "$DMG_EXISTS" = true ]; then
|
||||
# DMG exists but build failed - likely notarization issue
|
||||
if grep -qiE "notariz|staple" "${RUNNER_TEMP}/build.log"; then
|
||||
echo "⚠️ DMG created but notarization failed"
|
||||
echo "notarization_status=failed" >> $GITHUB_OUTPUT
|
||||
echo "::warning title=Notarization Failed::DMG was built and signed successfully, but **notarization failed**."
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## ⚠️ Notarization Warning" >> $GITHUB_STEP_SUMMARY
|
||||
echo "DMG was built and signed successfully, but **notarization failed**." >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Users may see Gatekeeper warnings when opening the app for the first time." >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "To fix: Right-click the app → Open → Open" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0 # Allow CI to continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# DMG doesn't exist or other fatal error - fail CI
|
||||
echo "❌ Build failed - DMG was not created"
|
||||
echo "notarization_status=build_failed" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
appleId: ${{ secrets.APPLE_ID }}
|
||||
appleIdPassword: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
teamId: ${{ secrets.TEAM_ID }}
|
||||
identity: ${{ secrets.IDENTITY }}
|
||||
CSC_NAME: ${{ secrets.IDENTITY }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
npm_config_cache: ${{ runner.temp }}/.npm
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||
ELECTRON_MIRROR: ''
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
npm_config_runtime: electron
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
CI: true
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Linux: Standard build without special error handling
|
||||
# Linux: 标准构建,无特殊错误处理
|
||||
- name: Build with electron-builder (Linux)
|
||||
if: startsWith(matrix.platform, 'linux')
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 60
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
command: ${{ matrix.command }}
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_cache: ${{ runner.temp }}/.npm
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||
ELECTRON_MIRROR: ''
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
npm_config_runtime: electron
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
CI: true
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# macOS 钥匙串清理
|
||||
- name: Clean up keychain (macOS only)
|
||||
if: startsWith(matrix.platform, 'macos') && always()
|
||||
run: |
|
||||
if security list-keychains | grep -q build.keychain; then
|
||||
security delete-keychain build.keychain
|
||||
echo "Temporary keychain deleted"
|
||||
fi
|
||||
|
||||
# macOS 构建失败通知
|
||||
# macOS build failure notification
|
||||
- name: Notify on macOS build failure
|
||||
if: startsWith(matrix.platform, 'macos') && failure()
|
||||
run: |
|
||||
echo "::error title=macOS Build Failed::Build failed. Check the logs for details."
|
||||
echo ""
|
||||
echo "❌ macOS Build Failed"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Common causes:"
|
||||
echo " • hdiutil/DMG creation failure (Device not configured)"
|
||||
echo " • Code signing issues"
|
||||
echo " • Notarization timeout"
|
||||
echo " • Out of memory (OOM)"
|
||||
echo ""
|
||||
echo "Check the build logs above for the specific error."
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
- name: List build artifacts
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "BUILD ARTIFACTS"
|
||||
echo "=========================================="
|
||||
if [ -d "out" ]; then
|
||||
ls -lh out/ || true
|
||||
echo "=========================================="
|
||||
echo "Total artifacts:"
|
||||
find out/ -type f \( -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.deb" -o -name "*.zip" -o -name "*.yml" \) -exec basename {} \; || true
|
||||
else
|
||||
echo "No out directory found!"
|
||||
fi
|
||||
|
||||
# Verify desktop asar does not contain packages/web-cli/ contamination
|
||||
# 验证桌面 asar 没有被 web-cli 代码污染
|
||||
- name: Verify desktop asar isolation
|
||||
if: >-
|
||||
success() &&
|
||||
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ASAR=$(find out -type f -name 'app.asar' -not -path '*node_modules*' | head -n 1 || true)
|
||||
if [ -z "$ASAR" ]; then
|
||||
echo "::notice::No app.asar found for ${{ matrix.platform }}, skipping isolation check"
|
||||
exit 0
|
||||
fi
|
||||
echo "Inspecting $ASAR for web-cli leakage..."
|
||||
LEAK_COUNT=$(bunx @electron/asar list "$ASAR" 2>/dev/null | grep -cE '^/packages/web-cli(/|$)' || true)
|
||||
if [ "$LEAK_COUNT" != "0" ]; then
|
||||
echo "::error::Desktop asar contains $LEAK_COUNT web-cli entries:"
|
||||
bunx @electron/asar list "$ASAR" | grep -E '^/packages/web-cli(/|$)' | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ asar isolation verified: no packages/web-cli entries"
|
||||
|
||||
# Upload artifacts when:
|
||||
# - Build succeeded (all platforms)
|
||||
# - macOS: notarization timeout but artifacts exist (handled in build step)
|
||||
# - Windows: only if windows-build step succeeded
|
||||
# 上传产物条件:
|
||||
# - 构建成功(所有平台)
|
||||
# - macOS: 公证超时但有产物(已在构建步骤中处理)
|
||||
# - Windows: 仅当 windows-build 步骤成功
|
||||
- name: Clean up non-installer artifacts
|
||||
if: >-
|
||||
inputs.upload_installers_only && success() &&
|
||||
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Removing non-installer files from out/ ..."
|
||||
rm -f out/*.blockmap out/*.zip out/*.yml out/builder-debug.yml 2>/dev/null || true
|
||||
echo "Remaining artifacts:"
|
||||
ls -lh out/ || true
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
if: >-
|
||||
success() &&
|
||||
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
|
||||
with:
|
||||
name: ${{ inputs.append_commit_hash && format('{0}-{1}', matrix.artifact-name, steps.commit.outputs.short) || matrix.artifact-name }}
|
||||
path: |
|
||||
out/*.exe
|
||||
out/*.msi
|
||||
out/*.dmg
|
||||
out/*.deb
|
||||
out/AionUi-*-mac-*.zip
|
||||
out/*.yml
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
build-summary:
|
||||
name: Build Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, build]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Write build summary
|
||||
env:
|
||||
MATRIX: ${{ inputs.matrix }}
|
||||
run: |
|
||||
PLATFORMS=$(echo "$MATRIX" | jq -r '[.include[].platform] | join(", ")')
|
||||
|
||||
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Key | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-----|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Ref | \`${{ inputs.ref || '(default)' }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Commit | \`${{ needs.build.outputs.commit-short }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platforms | $PLATFORMS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Code Quality | ${{ needs.code-quality.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,314 @@
|
||||
name: Build and Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: read
|
||||
pull-requests: read
|
||||
actions: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
|
||||
jobs:
|
||||
# 独立 code-quality job,供桌面构建和 web-cli 打包共用,避免重复跑
|
||||
code-quality:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-'))
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: bun run postinstall || true
|
||||
|
||||
- name: Run ESLint
|
||||
run: bun run lint
|
||||
|
||||
- name: Check Prettier formatting
|
||||
run: bun run format:check
|
||||
|
||||
- name: TypeScript type check
|
||||
run: bunx tsc --noEmit
|
||||
|
||||
- name: Run unit tests
|
||||
run: bunx vitest run
|
||||
|
||||
build-pipeline:
|
||||
name: Build Pipeline
|
||||
uses: ./.github/workflows/_build-reusable.yml
|
||||
needs: code-quality
|
||||
# dev 分支或正式 tag 触发构建(排除 -dev- tag,避免重复构建)
|
||||
if: needs.code-quality.result == 'success' && (github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
|
||||
with:
|
||||
skip_code_quality: true
|
||||
matrix: >-
|
||||
{"include":[
|
||||
{"platform":"macos-arm64","os":"macos-14","command":"node scripts/build-with-builder.js arm64 --mac --arm64","artifact-name":"macos-build-arm64","arch":"arm64"},
|
||||
{"platform":"macos-x64","os":"macos-14","command":"node scripts/build-with-builder.js x64 --mac --x64","artifact-name":"macos-build-x64","arch":"x64"},
|
||||
{"platform":"windows-x64","os":"windows-2022","command":"node scripts/build-with-builder.js x64 --win --x64","artifact-name":"windows-build-x64","arch":"x64"},
|
||||
{"platform":"windows-arm64","os":"windows-11-arm","command":"node scripts/build-with-builder.js arm64 --win --arm64","artifact-name":"windows-build-arm64","arch":"arm64"},
|
||||
{"platform":"linux-x64","os":"ubuntu-latest","command":"node scripts/build-with-builder.js x64 --linux --x64","artifact-name":"linux-build-x64","arch":"x64"},
|
||||
{"platform":"linux-arm64","os":"ubuntu-24.04-arm","command":"node scripts/build-with-builder.js arm64 --linux --arm64","artifact-name":"linux-build-arm64","arch":"arm64"}
|
||||
]}
|
||||
secrets: inherit
|
||||
|
||||
pack-web-cli:
|
||||
name: Pack Web CLI
|
||||
uses: ./.github/workflows/pack-web-cli.yml
|
||||
needs: code-quality
|
||||
# 与桌面构建同频触发:dev 分支或正式 tag(排除 -dev- tag)
|
||||
if: needs.code-quality.result == 'success' && (github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
|
||||
with:
|
||||
ref: ''
|
||||
append_commit_hash: false
|
||||
skip_code_quality: true
|
||||
secrets: inherit
|
||||
|
||||
# 自动重试 workflow(当构建失败时)
|
||||
auto-retry-workflow:
|
||||
name: Auto Retry on Build Failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-pipeline
|
||||
# 关键:只在首次失败时触发,避免无限循环
|
||||
if: |
|
||||
failure() &&
|
||||
github.run_attempt == 1 &&
|
||||
(github.event_name == 'push' || github.event_name == 'schedule')
|
||||
|
||||
steps:
|
||||
- name: Log retry information
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "🔄 Auto retry triggered (first failure)"
|
||||
echo "=========================================="
|
||||
echo "Build failed on first attempt, preparing auto retry..."
|
||||
echo "Current attempt: ${{ github.run_attempt }}"
|
||||
echo "Wait strategy: 5 minutes cooldown before retry"
|
||||
echo "=========================================="
|
||||
|
||||
- name: Wait before retry (5 min cooldown)
|
||||
run: |
|
||||
echo "⏳ Waiting 5 minutes before retry..."
|
||||
echo "Start: $(date)"
|
||||
sleep 300
|
||||
echo "End: $(date)"
|
||||
echo "Triggering retry..."
|
||||
|
||||
- name: Trigger workflow rerun
|
||||
run: |
|
||||
echo "🔄 Triggering full workflow rerun (attempt 2)..."
|
||||
|
||||
# Use re-run API (not rerun-failed-jobs, to avoid loops)
|
||||
response=$(curl -X POST \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-w "\n%{http_code}" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/rerun)
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
|
||||
if [ "$http_code" = "201" ]; then
|
||||
echo ""
|
||||
echo "✅ Retry triggered successfully"
|
||||
echo "This will be attempt 2"
|
||||
echo "Details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Retry trigger failed, HTTP status: $http_code"
|
||||
echo "Response:"
|
||||
echo "$response" | head -n-1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 自动创建tag(仅 dev 分支推送时)
|
||||
create-tag:
|
||||
name: Create Tag from Branch
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-pipeline]
|
||||
if: success() && github.ref == 'refs/heads/dev'
|
||||
outputs:
|
||||
tag_name: ${{ steps.create_tag.outputs.tag_name }}
|
||||
is_dev: ${{ steps.create_tag.outputs.is_dev }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# GH_TOKEN is a PAT with `repo` + `workflow` scopes — required because
|
||||
# tag pushes whose reachable history contains .github/workflows/
|
||||
# changes are rejected for the built-in GITHUB_TOKEN (lacks the
|
||||
# `workflow` scope, which GITHUB_TOKEN can never grant).
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Create and push tag
|
||||
id: create_tag
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
COMMIT_SHORT=$(git rev-parse --short HEAD)
|
||||
BRANCH_NAME=${GITHUB_REF#refs/heads/}
|
||||
|
||||
# 根据分支确定 tag 格式
|
||||
if [ "$BRANCH_NAME" = "dev" ]; then
|
||||
TAG_NAME="v${VERSION}-dev-${COMMIT_SHORT}"
|
||||
IS_DEV="true"
|
||||
echo "🔧 Development release: $TAG_NAME"
|
||||
else
|
||||
TAG_NAME="v$VERSION"
|
||||
IS_DEV="false"
|
||||
echo "🚀 Production release: $TAG_NAME"
|
||||
fi
|
||||
|
||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
echo "is_dev=$IS_DEV" >> $GITHUB_OUTPUT
|
||||
|
||||
# 检查远程tag是否已存在
|
||||
if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then
|
||||
if [ "$IS_DEV" = "false" ]; then
|
||||
# Main 分支:需要递增版本并修改 package.json
|
||||
echo "⚠️ Tag $TAG_NAME already exists, auto-incrementing version..."
|
||||
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
|
||||
echo "📝 Updating package.json version to $NEW_VERSION"
|
||||
|
||||
# 更新 package.json 版本号
|
||||
bun pm version $NEW_VERSION
|
||||
|
||||
# 配置git用户信息
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# 提交版本更新
|
||||
git add package.json bun.lock
|
||||
git commit -m "chore: bump version to $NEW_VERSION"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git push origin $BRANCH_NAME
|
||||
|
||||
# 获取新的 commit ID
|
||||
COMMIT_SHORT=$(git rev-parse --short HEAD)
|
||||
TAG_NAME="v${NEW_VERSION}-${COMMIT_SHORT}"
|
||||
|
||||
echo "🚀 New tag with updated version: $TAG_NAME"
|
||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
else
|
||||
TAG_NAME="v${VERSION}-${COMMIT_SHORT}"
|
||||
echo "⚠️ Fallback: creating tag with commit ID: $TAG_NAME"
|
||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
# Dev 分支 tag 已存在则失败(不应该发生)
|
||||
echo "⚠️ Dev tag $TAG_NAME already exists, this shouldn't happen"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 配置git用户信息(如果前面没配置)
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# 创建并推送tag
|
||||
echo "Creating tag: $TAG_NAME"
|
||||
git tag $TAG_NAME
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git push origin $TAG_NAME
|
||||
echo "✅ Successfully created and pushed tag: $TAG_NAME"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
# 发布到 GitHub Releases
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-pipeline, create-tag, pack-web-cli]
|
||||
# Tag 推送时 create-tag 会被跳过,所以需要检查两种情况(排除 -dev- tag 避免重复)
|
||||
if: always() && needs.build-pipeline.result == 'success' && needs.pack-web-cli.result == 'success' && (needs.create-tag.result == 'success' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
|
||||
# dev 分支使用 dev-release 环境,正式 tag 使用 release 环境
|
||||
environment: ${{ needs.create-tag.outputs.is_dev == 'true' && 'dev-release' || 'release' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version for release
|
||||
id: version
|
||||
run: |
|
||||
# 判断触发来源:正式 tag 推送 or dev 分支
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
# 正式 Tag 推送触发(-dev- tag 已被排除,不会到达这里)
|
||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||
IS_DEV="false"
|
||||
echo "🚀 Creating stable release from tag: $TAG_NAME"
|
||||
else
|
||||
# dev 分支触发,使用 create-tag job 的输出
|
||||
TAG_NAME="${{ needs.create-tag.outputs.tag_name }}"
|
||||
IS_DEV="${{ needs.create-tag.outputs.is_dev }}"
|
||||
echo "🔧 Creating development release: $TAG_NAME"
|
||||
fi
|
||||
|
||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
echo "is_dev=$IS_DEV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download all build artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: build-artifacts
|
||||
|
||||
- name: Prepare release assets (normalize updater metadata)
|
||||
shell: bash
|
||||
run: bash scripts/prepare-release-assets.sh build-artifacts release-assets
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag_name }}
|
||||
name: ${{ steps.version.outputs.is_dev == 'true' && format('Development Build {0}', steps.version.outputs.tag_name) || steps.version.outputs.tag_name }}
|
||||
files: |
|
||||
release-assets/**/*.exe
|
||||
release-assets/**/*.msi
|
||||
release-assets/**/*.dmg
|
||||
release-assets/**/*.deb
|
||||
release-assets/**/*.zip
|
||||
release-assets/**/*.yml
|
||||
release-assets/**/*.tar.gz
|
||||
release-assets/**/*.sha256
|
||||
release-assets/install-web.sh
|
||||
generate_release_notes: true
|
||||
draft: true
|
||||
prerelease: ${{ steps.version.outputs.is_dev == 'true' || contains(steps.version.outputs.tag_name, 'beta') || contains(steps.version.outputs.tag_name, 'alpha') || contains(steps.version.outputs.tag_name, 'rc') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
@@ -0,0 +1,93 @@
|
||||
name: '🔨 Manual Build'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to build from'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
platform:
|
||||
description: 'Platform to build'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- macos-arm64
|
||||
- macos-x64
|
||||
- windows-x64
|
||||
- windows-arm64
|
||||
- linux-x64
|
||||
- linux-arm64
|
||||
- all
|
||||
default: 'macos-arm64'
|
||||
skip_code_quality:
|
||||
description: 'Skip code quality checks (faster builds for debugging)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
aioncore_run_id:
|
||||
description: 'Optional AionCore Manual Build workflow run id to bundle instead of the pinned release'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
|
||||
jobs:
|
||||
prepare-matrix:
|
||||
name: Prepare Build Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
steps:
|
||||
- name: Generate build matrix
|
||||
id: set-matrix
|
||||
shell: bash
|
||||
run: |
|
||||
PLATFORM="${{ inputs.platform }}"
|
||||
|
||||
MACOS_ARM64='{"platform":"macos-arm64","os":"macos-14","command":"node scripts/build-with-builder.js arm64 --mac --arm64","artifact-name":"macos-build-arm64","arch":"arm64"}'
|
||||
MACOS_X64='{"platform":"macos-x64","os":"macos-14","command":"node scripts/build-with-builder.js x64 --mac --x64","artifact-name":"macos-build-x64","arch":"x64"}'
|
||||
WINDOWS_X64='{"platform":"windows-x64","os":"windows-2022","command":"node scripts/build-with-builder.js x64 --win --x64","artifact-name":"windows-build-x64","arch":"x64"}'
|
||||
WINDOWS_ARM64='{"platform":"windows-arm64","os":"windows-11-arm","command":"node scripts/build-with-builder.js arm64 --win --arm64","artifact-name":"windows-build-arm64","arch":"arm64"}'
|
||||
LINUX_X64='{"platform":"linux-x64","os":"ubuntu-latest","command":"node scripts/build-with-builder.js x64 --linux --x64","artifact-name":"linux-build-x64","arch":"x64"}'
|
||||
LINUX_ARM64='{"platform":"linux-arm64","os":"ubuntu-24.04-arm","command":"node scripts/build-with-builder.js arm64 --linux --arm64","artifact-name":"linux-build-arm64","arch":"arm64"}'
|
||||
|
||||
if [ "$PLATFORM" = "all" ]; then
|
||||
MATRIX="{\"include\":[$MACOS_ARM64,$MACOS_X64,$WINDOWS_X64,$WINDOWS_ARM64,$LINUX_X64,$LINUX_ARM64]}"
|
||||
elif [ "$PLATFORM" = "macos-arm64" ]; then
|
||||
MATRIX="{\"include\":[$MACOS_ARM64]}"
|
||||
elif [ "$PLATFORM" = "macos-x64" ]; then
|
||||
MATRIX="{\"include\":[$MACOS_X64]}"
|
||||
elif [ "$PLATFORM" = "windows-x64" ]; then
|
||||
MATRIX="{\"include\":[$WINDOWS_X64]}"
|
||||
elif [ "$PLATFORM" = "windows-arm64" ]; then
|
||||
MATRIX="{\"include\":[$WINDOWS_ARM64]}"
|
||||
elif [ "$PLATFORM" = "linux-x64" ]; then
|
||||
MATRIX="{\"include\":[$LINUX_X64]}"
|
||||
elif [ "$PLATFORM" = "linux-arm64" ]; then
|
||||
MATRIX="{\"include\":[$LINUX_ARM64]}"
|
||||
fi
|
||||
|
||||
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
|
||||
echo "Generated matrix for platform '$PLATFORM':"
|
||||
echo "$MATRIX" | python3 -m json.tool
|
||||
|
||||
build-pipeline:
|
||||
needs: prepare-matrix
|
||||
uses: ./.github/workflows/_build-reusable.yml
|
||||
with:
|
||||
matrix: ${{ needs.prepare-matrix.outputs.matrix }}
|
||||
ref: ${{ inputs.branch }}
|
||||
skip_code_quality: ${{ inputs.skip_code_quality }}
|
||||
aioncore_run_id: ${{ inputs.aioncore_run_id }}
|
||||
append_commit_hash: true
|
||||
upload_installers_only: true
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,132 @@
|
||||
name: Verify Homebrew Cask
|
||||
|
||||
# The aionui cask is on Homebrew's autobump list — BrewTestBot automatically
|
||||
# creates version-bump PRs every ~3 hours. This workflow only *verifies* that
|
||||
# the cask has been updated, it no longer submits bump PRs itself.
|
||||
|
||||
on:
|
||||
# Daily check at 08:00 UTC
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to verify (e.g., 1.8.20). Leave empty to use latest release.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify-cask:
|
||||
name: Verify Homebrew Cask
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get expected version
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ -n "${{ inputs.version }}" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
VERSION="${VERSION#v}"
|
||||
else
|
||||
# Fetch latest non-prerelease release tag
|
||||
TAG=$(gh release view --repo "${{ github.repository }}" --json tagName --jq '.tagName' 2>/dev/null || echo "")
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "::error::Could not determine latest release tag"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${TAG#v}"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Expected Homebrew cask version: $VERSION"
|
||||
|
||||
- name: Verify cask version
|
||||
id: verify
|
||||
run: |
|
||||
EXPECTED="${{ steps.release.outputs.version }}"
|
||||
|
||||
# Fetch the cask file directly from the Homebrew repo
|
||||
CASK_CONTENT=$(curl -fsSL "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/a/aionui.rb" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$CASK_CONTENT" ]; then
|
||||
echo "::error::Failed to fetch cask file from Homebrew repo"
|
||||
echo "status=fetch_failed" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version from cask file
|
||||
CASK_VERSION=$(echo "$CASK_CONTENT" | grep -oP 'version\s+"\K[^"]+' | head -1)
|
||||
echo " Expected: $EXPECTED"
|
||||
echo " Homebrew: $CASK_VERSION"
|
||||
echo "cask_version=$CASK_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$CASK_VERSION" = "$EXPECTED" ]; then
|
||||
echo "status=match" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=mismatch" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check for pending PR
|
||||
if: steps.verify.outputs.status == 'mismatch'
|
||||
id: check_pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
EXPECTED="${{ steps.release.outputs.version }}"
|
||||
|
||||
# Search for open PRs updating aionui in homebrew-cask
|
||||
PR_LIST=$(gh pr list --repo Homebrew/homebrew-cask \
|
||||
--search "aionui $EXPECTED in:title" --state open \
|
||||
--json number,title,url --limit 5 2>/dev/null || echo "[]")
|
||||
PR_COUNT=$(echo "$PR_LIST" | jq length)
|
||||
|
||||
if [ "$PR_COUNT" -gt 0 ]; then
|
||||
echo "Found pending PR(s):"
|
||||
echo "$PR_LIST" | jq -r '.[] | " #\(.number) \(.title) — \(.url)"'
|
||||
echo "status=pr_pending" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=no_pr" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Report result
|
||||
if: always() && steps.release.outputs.version != ''
|
||||
run: |
|
||||
EXPECTED="${{ steps.release.outputs.version }}"
|
||||
STATUS="${{ steps.verify.outputs.status }}"
|
||||
CASK_VERSION="${{ steps.verify.outputs.cask_version }}"
|
||||
PR_STATUS="${{ steps.check_pr.outputs.status }}"
|
||||
|
||||
echo ""
|
||||
echo "======================================="
|
||||
echo " Homebrew Cask Verification Report"
|
||||
echo "======================================="
|
||||
echo ""
|
||||
|
||||
if [ "$STATUS" = "match" ]; then
|
||||
echo "Homebrew cask is up to date!"
|
||||
echo " Version: $CASK_VERSION"
|
||||
echo " Install: brew install --cask aionui"
|
||||
elif [ "$STATUS" = "mismatch" ] && [ "$PR_STATUS" = "pr_pending" ]; then
|
||||
echo "Homebrew bump PR is pending review."
|
||||
echo " Current cask: $CASK_VERSION"
|
||||
echo " Expected: $EXPECTED"
|
||||
echo " BrewTestBot has created a PR, awaiting Homebrew maintainer merge."
|
||||
elif [ "$STATUS" = "mismatch" ]; then
|
||||
echo "::warning::Homebrew cask has NOT been updated to $EXPECTED (current: $CASK_VERSION)"
|
||||
echo " Current cask: $CASK_VERSION"
|
||||
echo " Expected: $EXPECTED"
|
||||
echo ""
|
||||
echo " If this persists, manually create a PR:"
|
||||
echo " 1. Fork https://github.com/Homebrew/homebrew-cask"
|
||||
echo " 2. Update Casks/a/aionui.rb with new version and SHA256"
|
||||
echo " 3. Submit PR to Homebrew/homebrew-cask"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::Failed to verify — could not fetch cask file from Homebrew repo"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,389 @@
|
||||
name: '📋 GPT PR Assessment'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to assess'
|
||||
required: false
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to assess'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: 'gpt-pr-assessment-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
assess:
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'write'
|
||||
issues: 'read'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Gather PR diff and changed files'
|
||||
id: 'gather'
|
||||
uses: './.github/actions/gather-pr-diff'
|
||||
with:
|
||||
pr_number: '${{ inputs.pr_number }}'
|
||||
|
||||
- name: 'Fetch PR metadata and linked issues'
|
||||
id: 'metadata'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
const prNumber = context.payload.pull_request?.number
|
||||
|| Number(process.env.INPUT_PR_NUMBER);
|
||||
|
||||
// Fetch full PR data via API (needed for manual triggers too)
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
const prMeta = {
|
||||
title: pr.title || '',
|
||||
body: pr.body || '',
|
||||
author: pr.user.login || '',
|
||||
labels: (pr.labels || []).map(l => l.name),
|
||||
head_branch: pr.head.ref || '',
|
||||
base_branch: pr.base.ref || '',
|
||||
state: pr.state,
|
||||
created_at: pr.created_at,
|
||||
updated_at: pr.updated_at,
|
||||
};
|
||||
|
||||
fs.writeFileSync(`${tmpDir}/pr_meta.json`, JSON.stringify(prMeta));
|
||||
|
||||
// Parse linked issues from PR body
|
||||
const prBody = prMeta.body;
|
||||
const linkedIssueNumbers = new Set();
|
||||
const referencedIssueNumbers = new Set();
|
||||
|
||||
// Explicit linking keywords: Fixes #N, Closes #N, Resolves #N
|
||||
const explicitPatterns = [
|
||||
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi,
|
||||
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/gi,
|
||||
];
|
||||
for (const pattern of explicitPatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(prBody)) !== null) {
|
||||
linkedIssueNumbers.add(Number(match[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// General #N references (not already captured)
|
||||
const refPattern = /#(\d+)/g;
|
||||
let refMatch;
|
||||
while ((refMatch = refPattern.exec(prBody)) !== null) {
|
||||
const num = Number(refMatch[1]);
|
||||
if (!linkedIssueNumbers.has(num)) {
|
||||
referencedIssueNumbers.add(num);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch issue details, filtering out PRs
|
||||
const EXPLICIT_BODY_LIMIT = 5000;
|
||||
const REFERENCE_BODY_LIMIT = 3000;
|
||||
|
||||
async function fetchIssue(issueNum, bodyLimit) {
|
||||
try {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNum,
|
||||
});
|
||||
|
||||
// Skip if this is actually a PR (issues API returns PRs too)
|
||||
if (issue.pull_request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = (issue.body || '').substring(0, bodyLimit);
|
||||
const truncated = (issue.body || '').length > bodyLimit;
|
||||
|
||||
return {
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
labels: (issue.labels || []).map(l => typeof l === 'string' ? l : l.name),
|
||||
body: truncated ? body + '\n... [truncated]' : body,
|
||||
};
|
||||
} catch (err) {
|
||||
core.warning(`Failed to fetch issue #${issueNum}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const linkedIssues = [];
|
||||
for (const num of linkedIssueNumbers) {
|
||||
const issue = await fetchIssue(num, EXPLICIT_BODY_LIMIT);
|
||||
if (issue) linkedIssues.push({ ...issue, relation: 'explicit' });
|
||||
}
|
||||
|
||||
for (const num of referencedIssueNumbers) {
|
||||
const issue = await fetchIssue(num, REFERENCE_BODY_LIMIT);
|
||||
if (issue) linkedIssues.push({ ...issue, relation: 'referenced' });
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${tmpDir}/linked_issues.json`, JSON.stringify(linkedIssues));
|
||||
core.info(`Found ${linkedIssues.length} linked issue(s)`);
|
||||
|
||||
- name: 'Read changed file contents'
|
||||
id: 'read_contents'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: './.github/actions/read-file-contents'
|
||||
|
||||
- name: 'Construct GPT prompts'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
FILE_COUNT: '${{ steps.gather.outputs.file_count }}'
|
||||
ADDITIONS: '${{ steps.gather.outputs.additions }}'
|
||||
DELETIONS: '${{ steps.gather.outputs.deletions }}'
|
||||
TOTAL_LINES: '${{ steps.gather.outputs.total_lines }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
const prMeta = JSON.parse(fs.readFileSync(`${tmpDir}/pr_meta.json`, 'utf-8'));
|
||||
const linkedIssues = JSON.parse(fs.readFileSync(`${tmpDir}/linked_issues.json`, 'utf-8'));
|
||||
const diff = fs.readFileSync(`${tmpDir}/pr_diff.txt`, 'utf-8');
|
||||
const fileListRaw = fs.readFileSync(`${tmpDir}/file_list.json`, 'utf-8');
|
||||
|
||||
let fileContents = '';
|
||||
try {
|
||||
fileContents = fs.readFileSync(`${tmpDir}/file_contents.txt`, 'utf-8');
|
||||
} catch {
|
||||
core.warning('Could not read file contents, proceeding with diff only');
|
||||
}
|
||||
|
||||
const fileCount = process.env.FILE_COUNT || '0';
|
||||
const additions = process.env.ADDITIONS || '0';
|
||||
const deletions = process.env.DELETIONS || '0';
|
||||
const totalLines = process.env.TOTAL_LINES || '0';
|
||||
|
||||
// Build linked issues section for user prompt
|
||||
let issuesSection = '';
|
||||
if (linkedIssues.length > 0) {
|
||||
const issueTexts = linkedIssues.map(issue => {
|
||||
const relation = issue.relation === 'explicit' ? 'Explicitly linked' : 'Referenced';
|
||||
return [
|
||||
`### Issue #${issue.number}: ${issue.title}`,
|
||||
`- **Relation**: ${relation}`,
|
||||
`- **State**: ${issue.state}`,
|
||||
`- **Labels**: ${issue.labels.length > 0 ? issue.labels.join(', ') : 'None'}`,
|
||||
'',
|
||||
'**Body**:',
|
||||
issue.body || '(No body)',
|
||||
].join('\n');
|
||||
});
|
||||
issuesSection = [
|
||||
'## Linked Issues',
|
||||
'',
|
||||
...issueTexts,
|
||||
].join('\n');
|
||||
} else {
|
||||
issuesSection = [
|
||||
'## Linked Issues',
|
||||
'',
|
||||
'No linked issues found. The PR does not reference any issues via "Fixes #N", "Closes #N", "Resolves #N", or "#N" patterns.',
|
||||
'Please assess the PR based on its title, description, and code changes.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const systemPrompt = [
|
||||
'You are a senior project maintainer and technical lead for the AionUi project — a cross-platform Electron desktop application that provides a unified AI agent graphical interface.',
|
||||
'',
|
||||
'Your role is to provide a structured PR assessment to help maintainers quickly evaluate the value, correctness, and merge priority of pull requests.',
|
||||
'',
|
||||
'## Tech Stack',
|
||||
'- Electron 37.x + React 19.x + TypeScript 5.8.x (strict mode)',
|
||||
'- Express 5.x (WebUI server), Better SQLite3 (local DB)',
|
||||
'- Arco Design 2.x (UI), UnoCSS 66.x (atomic CSS), Monaco Editor 4.x',
|
||||
'- Anthropic SDK, Google GenAI, OpenAI SDK, MCP SDK',
|
||||
'- Webpack 6.x, Electron Forge 7.8.x',
|
||||
'',
|
||||
'## Architecture',
|
||||
'- Multi-process: Main (Electron + DB + IPC), Renderer (React UI), Worker (background AI tasks)',
|
||||
'- IPC via secure contextBridge isolation',
|
||||
'- WebUI: Express + WebSocket + JWT auth',
|
||||
'- Agent system: channels/, agent/ directories',
|
||||
'',
|
||||
'## Priority Definitions',
|
||||
'',
|
||||
'| Priority | Meaning | Examples |',
|
||||
'|----------|---------|----------|',
|
||||
'| P0 Critical | Must merge immediately | Crash fix, data loss, security vulnerability |',
|
||||
'| P1 High | Should merge within days | Core bug fix, urgently needed feature |',
|
||||
'| P2 Medium | Merge in regular cycle | Enhancement, minor bug, UX improvement |',
|
||||
'| P3 Low | Merge when convenient | Cosmetic, docs, minor refactor |',
|
||||
'',
|
||||
'## Output Format',
|
||||
'',
|
||||
'**CRITICAL: Detect the language of the PR title and body. Write your ENTIRE assessment in that same language.** If the PR is in Chinese, write in Chinese. If in English, write in English. If mixed or unclear, default to English.',
|
||||
'',
|
||||
'Use this EXACT structure:',
|
||||
'',
|
||||
'# PR Assessment',
|
||||
'',
|
||||
'## Change Overview',
|
||||
'[2-4 sentences summarizing what this PR does and why]',
|
||||
'',
|
||||
'## Linked Issue Analysis',
|
||||
'[If linked issues exist]:',
|
||||
'| Issue | Status | Addressed? |',
|
||||
'|-------|--------|------------|',
|
||||
'| #N: [title] | Open/Closed | Yes/Partially/No |',
|
||||
'',
|
||||
'[For each issue, analyze whether the PR actually solves the problem described]',
|
||||
'',
|
||||
'[If no linked issues]:',
|
||||
'State that no issues are linked, then infer the PR\'s purpose and motivation from its title, description, and code changes.',
|
||||
'',
|
||||
'## Change Type',
|
||||
'- **Type**: Bug Fix / New Feature / Enhancement / Refactor / Docs / Build / Test / ...',
|
||||
'- **Scope**: Core / UI / Backend / Build / Config / ...',
|
||||
'- **Breaking Changes**: Yes/No [explain if yes]',
|
||||
'',
|
||||
'## Merge Recommendation',
|
||||
'One of: ✅ Recommend Merge / ⚠️ Conditional Merge / ❌ Do Not Merge',
|
||||
'',
|
||||
'**Rationale**: [Specific reasons for the recommendation]',
|
||||
'',
|
||||
'## Merge Priority',
|
||||
'**Priority**: P0-P3 — Critical/High/Medium/Low',
|
||||
'',
|
||||
'**Rationale**: [Why this priority level]',
|
||||
'',
|
||||
'## Risk Assessment',
|
||||
'| Risk | Level | Details |',
|
||||
'|------|-------|---------|',
|
||||
'| Regression risk | Low/Medium/High | ... |',
|
||||
'| Security impact | None/Low/Medium/High | ... |',
|
||||
'| Performance impact | None/Low/Medium/High | ... |',
|
||||
'| Compatibility | None/Low/Medium/High | ... |',
|
||||
'',
|
||||
'## Reviewer Tips',
|
||||
'- [Advice for human reviewers]',
|
||||
'- [Key files or logic to focus on]',
|
||||
'',
|
||||
'## Rules',
|
||||
'- Be objective and evidence-based. Base your assessment on actual code changes and issue content.',
|
||||
'- Do NOT inflate risk or severity. If the PR is straightforward and safe, say so.',
|
||||
'- Do NOT fabricate concerns. It is fine to have a short, positive assessment for clean PRs.',
|
||||
'- For Linked Issue Analysis, carefully compare the issue description with the actual code changes to determine if the problem is truly addressed.',
|
||||
'- Be specific in Reviewer Tips — point to exact files, functions, or patterns that need attention.',
|
||||
'',
|
||||
'## Footer',
|
||||
'After the Reviewer Tips section, ALWAYS append this exact footer:',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'*🤖 This assessment was generated by AI and serves as a reference for maintainers. Please use your own judgment for final merge decisions.*',
|
||||
].join('\n');
|
||||
|
||||
const userPrompt = [
|
||||
'## Pull Request',
|
||||
'',
|
||||
`**Title**: ${prMeta.title}`,
|
||||
`**Author**: ${prMeta.author}`,
|
||||
`**Labels**: ${prMeta.labels.length > 0 ? prMeta.labels.join(', ') : 'None'}`,
|
||||
`**Branch**: ${prMeta.head_branch} → ${prMeta.base_branch}`,
|
||||
`**Stats**: ${fileCount} files changed, +${additions} -${deletions} (${totalLines} total lines)`,
|
||||
'',
|
||||
'**Description**:',
|
||||
prMeta.body || '(No description provided)',
|
||||
'',
|
||||
issuesSection,
|
||||
'',
|
||||
'**Changed Files**:',
|
||||
fileListRaw,
|
||||
'',
|
||||
'## Diff',
|
||||
'',
|
||||
diff,
|
||||
'',
|
||||
'## Full File Contents (for cross-file analysis)',
|
||||
'',
|
||||
fileContents || '(No file contents available)',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(`${tmpDir}/system_prompt.txt`, systemPrompt);
|
||||
fs.writeFileSync(`${tmpDir}/user_prompt.txt`, userPrompt);
|
||||
|
||||
- name: 'Call GPT for PR assessment'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: './.github/actions/call-openai'
|
||||
with:
|
||||
openai_api_key: '${{ secrets.OPENAI_API_KEY }}'
|
||||
output_file: 'assessment_body.txt'
|
||||
diff_truncated: '${{ steps.gather.outputs.diff_truncated }}'
|
||||
contents_truncated: '${{ steps.read_contents.outputs.contents_truncated }}'
|
||||
|
||||
- name: 'Post or update assessment comment'
|
||||
if: steps.gather.outputs.skip != 'true' && success()
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
const assessmentBody = fs.readFileSync(`${tmpDir}/assessment_body.txt`, 'utf-8');
|
||||
const prNumber = context.payload.pull_request?.number
|
||||
|| Number(process.env.INPUT_PR_NUMBER);
|
||||
|
||||
const MARKER = '<!-- gpt-pr-assessment-bot -->';
|
||||
const fullBody = `${MARKER}\n\n${assessmentBody}`;
|
||||
|
||||
// Check for existing assessment comment to update
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const existingComment = comments.data.find(comment =>
|
||||
comment.body.includes(MARKER)
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body: fullBody,
|
||||
});
|
||||
core.info(`Updated existing assessment comment (id: ${existingComment.id}) on PR #${prNumber}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: fullBody,
|
||||
});
|
||||
core.info(`Created new assessment comment on PR #${prNumber}`);
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
name: '🤖 GPT Review'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: false
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: 'gpt-review-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Gather PR diff and changed files'
|
||||
id: 'gather'
|
||||
uses: './.github/actions/gather-pr-diff'
|
||||
with:
|
||||
pr_number: '${{ inputs.pr_number }}'
|
||||
|
||||
- name: 'Read changed file contents'
|
||||
id: 'read_contents'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: './.github/actions/read-file-contents'
|
||||
|
||||
- name: 'Construct GPT prompts'
|
||||
id: 'prompts'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
|
||||
FILE_COUNT: '${{ steps.gather.outputs.file_count }}'
|
||||
ADDITIONS: '${{ steps.gather.outputs.additions }}'
|
||||
DELETIONS: '${{ steps.gather.outputs.deletions }}'
|
||||
TOTAL_LINES: '${{ steps.gather.outputs.total_lines }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
// Read PR metadata: from event context or fetch via API for manual trigger
|
||||
let prTitle, prBody, prAuthor;
|
||||
if (context.payload.pull_request) {
|
||||
prTitle = context.payload.pull_request.title || '';
|
||||
prBody = context.payload.pull_request.body || '';
|
||||
prAuthor = context.payload.pull_request.user.login || '';
|
||||
} else {
|
||||
const prNum = Number(process.env.INPUT_PR_NUMBER);
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNum,
|
||||
});
|
||||
prTitle = pr.title || '';
|
||||
prBody = pr.body || '';
|
||||
prAuthor = pr.user.login || '';
|
||||
}
|
||||
|
||||
const fileCount = process.env.FILE_COUNT || '0';
|
||||
const additions = process.env.ADDITIONS || '0';
|
||||
const deletions = process.env.DELETIONS || '0';
|
||||
const totalLines = process.env.TOTAL_LINES || '0';
|
||||
|
||||
// Read large data from temp files
|
||||
const diff = fs.readFileSync(`${tmpDir}/pr_diff.txt`, 'utf-8');
|
||||
const fileListRaw = fs.readFileSync(`${tmpDir}/file_list.json`, 'utf-8');
|
||||
let fileContents = '';
|
||||
try {
|
||||
fileContents = fs.readFileSync(`${tmpDir}/file_contents.txt`, 'utf-8');
|
||||
} catch {
|
||||
core.warning('Could not read file contents, proceeding with diff only');
|
||||
}
|
||||
|
||||
const systemPrompt = [
|
||||
'You are a world-class code review expert for the AionUi project — a cross-platform Electron desktop application that provides a unified AI agent graphical interface.',
|
||||
'',
|
||||
'## Tech Stack',
|
||||
'- Electron 37.x + React 19.x + TypeScript 5.8.x (strict mode)',
|
||||
'- Express 5.x (WebUI server), Better SQLite3 (local DB)',
|
||||
'- Arco Design 2.x (UI), UnoCSS 66.x (atomic CSS), Monaco Editor 4.x',
|
||||
'- Anthropic SDK, Google GenAI, OpenAI SDK, MCP SDK',
|
||||
'- Webpack 6.x, Electron Forge 7.8.x',
|
||||
'',
|
||||
'## Architecture',
|
||||
'- Multi-process: Main (Electron + DB + IPC), Renderer (React UI), Worker (background AI tasks)',
|
||||
'- IPC via secure contextBridge isolation',
|
||||
'- WebUI: Express + WebSocket + JWT auth',
|
||||
'- Agent system: channels/, agent/ directories',
|
||||
'- Security-sensitive paths: packages/desktop/src/process/, packages/desktop/src/process/agent/, packages/desktop/src/process/webserver/auth/',
|
||||
'',
|
||||
'## Code Conventions',
|
||||
'- TypeScript strict mode, prefer `type` over `interface`',
|
||||
'- Functional React components only, hooks with `use*` prefix',
|
||||
'- IMMUTABILITY: always create new objects, never mutate',
|
||||
'- Path aliases: @/*, @process/*, @renderer/*, @worker/*',
|
||||
'- UnoCSS atomic classes + CSS modules',
|
||||
'- English code comments, conventional commits',
|
||||
'',
|
||||
'## ESLint Key Rules',
|
||||
'- @typescript-eslint/consistent-type-definitions: prefer type',
|
||||
'- @typescript-eslint/no-explicit-any: warn',
|
||||
'- no-console: warn (no console.log in production code — flag any NEW console.log added by the PR)',
|
||||
'- react-hooks/rules-of-hooks: error',
|
||||
'- react-hooks/exhaustive-deps: warn',
|
||||
'',
|
||||
'## Review Dimensions (Priority Order)',
|
||||
'1. **Correctness** — Logic errors, unhandled edge cases, race conditions, incorrect API usage',
|
||||
'2. **Security** — Injection, insecure storage, access control, secrets exposure, OWASP Top 10',
|
||||
'3. **Performance** — Bottlenecks, memory leaks, unnecessary computation, inefficient data structures',
|
||||
'4. **Maintainability** — Readability, modularity, naming, adherence to project conventions',
|
||||
'5. **Immutability** — Object mutations, array mutations, state mutations (CRITICAL for this project)',
|
||||
'6. **Error Handling** — Missing try/catch, swallowed errors, unhelpful error messages',
|
||||
'7. **Type Safety** — any usage, missing types, incorrect type assertions, unsafe casts',
|
||||
'8. **Debug Hygiene** — New console.log/console.debug statements left in production code (flag as HIGH if clearly debug-only, MEDIUM if arguable)',
|
||||
'',
|
||||
'## Cross-File Analysis',
|
||||
'You are provided with both the diff AND the full content of changed files. Use the full file content to:',
|
||||
'- Trace function call chains across files',
|
||||
'- Verify exported/imported symbols exist and are correct',
|
||||
'- Check that cache/state management is consistent',
|
||||
'- Identify dead code introduced by the changes',
|
||||
'- Verify error handling propagation across module boundaries',
|
||||
'',
|
||||
'## Output Format',
|
||||
'',
|
||||
'**CRITICAL: Detect the language of the PR title and body. Write your ENTIRE review in that same language.** If the PR is in Chinese, write in Chinese. If in English, write in English. If mixed or unclear, default to English.',
|
||||
'',
|
||||
'Structure your review as a single comprehensive comment using this EXACT format:',
|
||||
'',
|
||||
'# Code Review',
|
||||
'',
|
||||
'## CRITICAL Issues',
|
||||
'',
|
||||
'### 1. [Issue Title]',
|
||||
'',
|
||||
'**File**: `path/to/file.ts:LINE-LINE`',
|
||||
'',
|
||||
'```typescript',
|
||||
'// problematic code snippet',
|
||||
'```',
|
||||
'',
|
||||
'**Problem**: [Detailed explanation of why this is critical]',
|
||||
'',
|
||||
'**Fix**: [Concrete fix suggestion with code if applicable]',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## HIGH Issues',
|
||||
'',
|
||||
'### N. [Issue Title]',
|
||||
'',
|
||||
'**File**: `path/to/file.ts:LINE-LINE`',
|
||||
'',
|
||||
'**Problem**: [Explanation]',
|
||||
'',
|
||||
'**Fix**: [Suggestion]',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## MEDIUM Issues',
|
||||
'',
|
||||
'### N. [Issue Title]',
|
||||
'',
|
||||
'**File**: `path/to/file.ts`',
|
||||
'',
|
||||
'[Description of the issue and recommendation]',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Summary',
|
||||
'',
|
||||
'| Level | Count |',
|
||||
'|-------|-------|',
|
||||
'| CRITICAL | N |',
|
||||
'| HIGH | N |',
|
||||
'| MEDIUM | N |',
|
||||
'',
|
||||
'## Rules',
|
||||
'- ONLY report issues you are **highly confident** about. If you are unsure whether something is a real problem, DO NOT report it.',
|
||||
'- **CRITICAL** means the code WILL break, crash, or cause data loss/security breach in production. Theoretical edge cases or defensive programming suggestions are NOT critical.',
|
||||
'- **HIGH** means the code has a clear bug or significant problem that will likely manifest in normal usage. Stylistic preferences or "could be improved" suggestions are NOT high.',
|
||||
'- **MEDIUM** is for genuine improvements, not hypothetical concerns.',
|
||||
'- Do NOT inflate severity. If you cannot point to a specific, realistic scenario where the issue causes a failure, lower the severity or omit it.',
|
||||
'- Do NOT fabricate issues to fill sections. It is perfectly fine — and preferred — to report fewer issues or none at all.',
|
||||
'- If a section has no issues, OMIT that section entirely (do not write "None found").',
|
||||
'- Include code snippets from the diff to pinpoint exact locations.',
|
||||
'- Each issue must have a concrete, actionable fix suggestion.',
|
||||
'- Number issues sequentially across all sections (1, 2, 3...).',
|
||||
'- For CRITICAL and HIGH issues, always include the specific file path and line numbers.',
|
||||
'- Do NOT comment on: lock files, auto-generated files, license headers, formatting-only changes, or well-known API parameters used correctly.',
|
||||
'- If the PR has NO issues at any level, output exactly: "# Code Review\\n\\nNo issues found. The changes are clean and well-implemented."',
|
||||
'',
|
||||
'## Footer',
|
||||
'After the Summary section, ALWAYS append this exact footer:',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'*🤖 This review was generated by AI and may contain inaccuracies. Please focus on issues you agree with and feel free to disregard any that seem incorrect. Thank you for your contribution!*',
|
||||
].join('\n');
|
||||
|
||||
const userPrompt = [
|
||||
'## Pull Request',
|
||||
'',
|
||||
`**Title**: ${prTitle}`,
|
||||
`**Author**: ${prAuthor}`,
|
||||
`**Stats**: ${fileCount} files changed, +${additions} -${deletions} (${totalLines} total lines)`,
|
||||
'',
|
||||
'**Description**:',
|
||||
prBody || '(No description provided)',
|
||||
'',
|
||||
'**Changed Files**:',
|
||||
fileListRaw,
|
||||
'',
|
||||
'## Diff',
|
||||
'',
|
||||
diff,
|
||||
'',
|
||||
'## Full File Contents (for cross-file analysis)',
|
||||
'',
|
||||
fileContents || '(No file contents available)',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(`${tmpDir}/system_prompt.txt`, systemPrompt);
|
||||
fs.writeFileSync(`${tmpDir}/user_prompt.txt`, userPrompt);
|
||||
|
||||
- name: 'Call OpenAI GPT for code review'
|
||||
if: steps.gather.outputs.skip != 'true'
|
||||
uses: './.github/actions/call-openai'
|
||||
with:
|
||||
openai_api_key: '${{ secrets.OPENAI_API_KEY }}'
|
||||
output_file: 'review_body.txt'
|
||||
diff_truncated: '${{ steps.gather.outputs.diff_truncated }}'
|
||||
contents_truncated: '${{ steps.read_contents.outputs.contents_truncated }}'
|
||||
|
||||
- name: 'Submit review to PR'
|
||||
if: steps.gather.outputs.skip != 'true' && success()
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
|
||||
|
||||
const reviewBody = fs.readFileSync(`${tmpDir}/review_body.txt`, 'utf-8');
|
||||
const prNumber = context.payload.pull_request?.number
|
||||
|| Number(process.env.INPUT_PR_NUMBER);
|
||||
|
||||
core.info(`Submitting COMMENT review to PR #${prNumber}`);
|
||||
|
||||
try {
|
||||
await github.rest.pulls.createReview({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
event: 'COMMENT',
|
||||
body: reviewBody,
|
||||
});
|
||||
core.info('Review submitted successfully via createReview');
|
||||
} catch (reviewError) {
|
||||
core.warning(`createReview failed: ${reviewError.message}, falling back to comment`);
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: `<!-- gpt-review-bot -->\n\n${reviewBody}`,
|
||||
});
|
||||
core.info('Review posted as comment (fallback)');
|
||||
} catch (commentError) {
|
||||
core.setFailed(`Failed to post review: ${commentError.message}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
name: Pack Web CLI
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Git ref to checkout (leave empty to use the triggering commit)'
|
||||
type: string
|
||||
default: ''
|
||||
append_commit_hash:
|
||||
description: 'Append short commit hash to artifact names'
|
||||
type: boolean
|
||||
default: false
|
||||
skip_code_quality:
|
||||
description: 'Skip code quality checks (caller already ran them)'
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Git ref to checkout (leave empty to use the triggering commit)'
|
||||
type: string
|
||||
default: ''
|
||||
skip_code_quality:
|
||||
description: 'Skip code quality checks'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !inputs.skip_code_quality }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: bun run postinstall || true
|
||||
|
||||
- name: Run ESLint
|
||||
run: bun run lint
|
||||
|
||||
- name: Check Prettier formatting
|
||||
run: bun run format:check
|
||||
|
||||
- name: TypeScript type check
|
||||
run: bunx tsc --noEmit
|
||||
|
||||
- name: Run unit tests
|
||||
run: bunx vitest run
|
||||
|
||||
pack-web-cli:
|
||||
name: Pack web-cli ${{ matrix.platform }}-${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: code-quality
|
||||
if: ${{ always() && (inputs.skip_code_quality || needs.code-quality.result == 'success') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { platform: darwin, arch: arm64, os: macos-14 }
|
||||
- { platform: darwin, arch: x64, os: macos-14 }
|
||||
- { platform: linux, arch: x64, os: ubuntu-latest }
|
||||
- { platform: linux, arch: arm64, os: ubuntu-24.04-arm }
|
||||
- { platform: win32, arch: x64, os: windows-2022 }
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT=$(git rev-parse --short HEAD)
|
||||
echo "short=$SHORT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build desktop renderer (for static files)
|
||||
run: bunx electron-vite build --config packages/desktop/electron.vite.config.ts
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
|
||||
- name: Pack web-cli tarball
|
||||
shell: bash
|
||||
run: node scripts/pack-web-cli.js
|
||||
env:
|
||||
PACK_PLATFORM: ${{ matrix.platform }}
|
||||
PACK_ARCH: ${{ matrix.arch }}
|
||||
# Version is pinned in repo-root package.json (aioncoreVersion).
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload tarball artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.append_commit_hash && format('web-cli-{0}-{1}-{2}', matrix.platform, matrix.arch, steps.commit.outputs.short) || format('web-cli-{0}-{1}', matrix.platform, matrix.arch) }}
|
||||
path: |
|
||||
dist-web-cli/*.tar.gz
|
||||
dist-web-cli/*.sha256
|
||||
retention-days: 7
|
||||
|
||||
prepare-install-script:
|
||||
name: Prepare install-web.sh for release
|
||||
runs-on: ubuntu-latest
|
||||
needs: pack-web-cli
|
||||
if: ${{ always() && needs.pack-web-cli.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT=$(git rev-parse --short HEAD)
|
||||
echo "short=$SHORT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Replace __VERSION__ placeholder in install-web.sh
|
||||
run: |
|
||||
mkdir -p dist-scripts
|
||||
sed "s/__VERSION__/${{ steps.version.outputs.version }}/g" scripts/install-web.sh > dist-scripts/install-web.sh
|
||||
chmod +x dist-scripts/install-web.sh
|
||||
|
||||
- name: Upload install-web.sh artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ inputs.append_commit_hash && format('install-web-script-{0}', steps.commit.outputs.short) || 'install-web-script' }}
|
||||
path: dist-scripts/install-web.sh
|
||||
retention-days: 7
|
||||
|
||||
smoke-test:
|
||||
name: Smoke test web-cli tarball (Linux x86_64)
|
||||
runs-on: ubuntu-latest
|
||||
needs: pack-web-cli
|
||||
if: ${{ always() && needs.pack-web-cli.result == 'success' }}
|
||||
container:
|
||||
image: debian:bookworm-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl tar gzip nodejs
|
||||
|
||||
- name: Download linux-x86_64 tarball
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: web-cli-linux-x64
|
||||
path: dist-web-cli
|
||||
|
||||
- name: Run smoke test
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x scripts/smoke-test-web-cli.sh
|
||||
TARBALL=$(ls dist-web-cli/*.tar.gz | head -1)
|
||||
bash scripts/smoke-test-web-cli.sh "$TARBALL"
|
||||
|
||||
smoke-test-install:
|
||||
name: Smoke test install-web.sh (Linux x86_64)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [pack-web-cli, prepare-install-script]
|
||||
if: ${{ always() && needs.pack-web-cli.result == 'success' && needs.prepare-install-script.result == 'success' }}
|
||||
container:
|
||||
image: debian:bookworm-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl tar gzip nodejs coreutils
|
||||
|
||||
- name: Resolve version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Resolved version: $VERSION"
|
||||
|
||||
- name: Download linux-x86_64 tarball
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: web-cli-linux-x64
|
||||
path: /tmp/releases/v${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Download install-web.sh
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: install-web-script
|
||||
path: /tmp/releases
|
||||
|
||||
- name: Run smoke test
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x scripts/smoke-test-install-web.sh
|
||||
bash scripts/smoke-test-install-web.sh file:///tmp/releases ${{ steps.version.outputs.version }}
|
||||
@@ -0,0 +1,66 @@
|
||||
name: 'PR Checks (docs only)'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, edited, ready_for_review]
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- '.vscode/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: Code Quality
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Install prek
|
||||
run: npm install -g @j178/prek
|
||||
|
||||
- name: Run prek checks
|
||||
run: prek run --from-ref origin/${{ github.base_ref }} --to-ref HEAD
|
||||
|
||||
unit-tests-ubuntu:
|
||||
name: Unit Tests (ubuntu-latest)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Docs-only PR, skipping unit tests."
|
||||
|
||||
unit-tests-macos:
|
||||
name: Unit Tests (macos-14)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Docs-only PR, skipping unit tests."
|
||||
|
||||
unit-tests-windows:
|
||||
name: Unit Tests (windows-2022)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Docs-only PR, skipping unit tests."
|
||||
@@ -0,0 +1,535 @@
|
||||
name: 'PR Checks'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to run checks on'
|
||||
required: true
|
||||
type: string
|
||||
skip_build_test:
|
||||
description: 'Skip the build test job (saves ~45 min)'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
pull_request:
|
||||
types: [opened, synchronize, edited, closed, ready_for_review]
|
||||
branches: [main, dev]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- '.vscode/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
checks: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
|
||||
jobs:
|
||||
# Cancel in-progress runs when PR is closed (merged or manually closed)
|
||||
cancel-if-closed:
|
||||
name: Cancel if PR closed
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "PR closed, cancelling in-progress runs via concurrency."
|
||||
|
||||
# Job 1: Code quality checks (TypeScript, Oxlint, Oxfmt)
|
||||
code-quality:
|
||||
name: Code Quality
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve PR context
|
||||
uses: ./.github/actions/checkout-pr
|
||||
with:
|
||||
pr_number: ${{ inputs.pr_number }}
|
||||
github_token: ${{ github.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Install prek
|
||||
run: npm install -g @j178/prek
|
||||
|
||||
- name: Run prek checks
|
||||
run: prek run --from-ref origin/${{ env.PR_BASE_REF }} --to-ref HEAD
|
||||
|
||||
# Job 2: Unit tests across all platforms
|
||||
unit-tests:
|
||||
name: Unit Tests (${{ matrix.os }})
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-14, windows-2022]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve PR context
|
||||
uses: ./.github/actions/checkout-pr
|
||||
with:
|
||||
pr_number: ${{ inputs.pr_number }}
|
||||
github_token: ${{ github.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
cache: true
|
||||
|
||||
- name: Disable Windows Defender (Windows only)
|
||||
if: matrix.os == 'windows-2022'
|
||||
shell: pwsh
|
||||
continue-on-error: true
|
||||
run: |
|
||||
try { Set-MpPreference -DisableRealtimeMonitoring $true } catch { Write-Host "::warning::Failed to disable real-time monitoring: $_" }
|
||||
try { Add-MpPreference -ExclusionPath "${{ github.workspace }}" } catch { Write-Host "::warning::Failed to add exclusion path: $_" }
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Run extension system tests
|
||||
run: bunx vitest run
|
||||
|
||||
# Job 3: Coverage test (Linux only)
|
||||
coverage-tests:
|
||||
name: Coverage Test
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve PR context
|
||||
uses: ./.github/actions/checkout-pr
|
||||
with:
|
||||
pr_number: ${{ inputs.pr_number }}
|
||||
github_token: ${{ github.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Run coverage tests
|
||||
id: coverage
|
||||
continue-on-error: true
|
||||
run: bun run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov (Linux coverage only)
|
||||
if: always() && hashFiles('coverage/lcov.info') != '' && github.repository == 'iOfficeAI/AionUi'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
use_oidc: ${{ secrets.CODECOV_TOKEN == '' }}
|
||||
files: coverage/lcov.info
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
- name: Skip Codecov upload when preconditions are not met
|
||||
if: always() && hashFiles('coverage/lcov.info') != '' && github.repository != 'iOfficeAI/AionUi'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'Skipping Codecov upload due to unmet preconditions.'
|
||||
echo "Repository: ${{ github.repository }}"
|
||||
- name: Coverage result summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ steps.coverage.outcome }}" = "failure" ]; then
|
||||
echo "::warning::Coverage tests failed (non-blocking). Check logs for failed test details."
|
||||
echo "## Coverage check (non-blocking warning)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Coverage command failed in this run. Please review test failures in logs." >> $GITHUB_STEP_SUMMARY
|
||||
if [ -f coverage/lcov.info ]; then
|
||||
echo "lcov.info exists, Codecov upload can still run." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "lcov.info not found, Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "## Coverage check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Coverage command passed." >> $GITHUB_STEP_SUMMARY
|
||||
if [ -f coverage/lcov.info ]; then
|
||||
echo "Uploaded Linux (ubuntu-latest) coverage to Codecov." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Coverage passed but lcov.info is missing; Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload coverage artifacts
|
||||
if: always() && hashFiles('coverage/lcov.info') != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
i18n-check:
|
||||
name: I18n Check
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Run i18n validation (warning-only)
|
||||
id: i18n
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
node scripts/check-i18n.js 2>&1 | tee i18n-check.log
|
||||
|
||||
- name: Publish i18n summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "## i18n validation" >> $GITHUB_STEP_SUMMARY
|
||||
if grep -q "⚠️" i18n-check.log; then
|
||||
echo "Missing/incomplete translations found. Please review warnings below." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No i18n warnings detected." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "<details><summary>i18n log</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```text' >> $GITHUB_STEP_SUMMARY
|
||||
cat i18n-check.log >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Job 4: Build test across representative PR platforms (parallel with code-quality and unit-tests)
|
||||
build-test:
|
||||
name: Build Test (${{ matrix.platform }})
|
||||
if: github.event.action != 'closed' && github.event.pull_request.draft == false && inputs.skip_build_test != true
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: 'macos-arm64'
|
||||
os: 'macos-14'
|
||||
arch: 'arm64'
|
||||
target: '--mac'
|
||||
build_args: '--mac --arm64'
|
||||
unpacked_dir: 'mac-arm64'
|
||||
- platform: 'windows-x64'
|
||||
os: 'windows-2022'
|
||||
arch: 'x64'
|
||||
target: '--win'
|
||||
build_args: '--win --x64'
|
||||
unpacked_dir: 'win-unpacked'
|
||||
- platform: 'linux-x64'
|
||||
os: 'ubuntu-latest'
|
||||
arch: 'x64'
|
||||
target: '--linux'
|
||||
build_args: '--linux --x64'
|
||||
unpacked_dir: 'linux-unpacked'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
cache: true
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'linux-x64'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential python3 python3-pip pkg-config libsqlite3-dev fakeroot dpkg-dev rpm libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxss1 libatspi2.0-dev libgtk-3-dev libxrandr2 libasound2-dev
|
||||
|
||||
- name: Get Electron version
|
||||
id: electron-version
|
||||
shell: bash
|
||||
run: |
|
||||
ELECTRON_VERSION=$(node -p "require('./package.json').devDependencies.electron.replace(/[\^~]/g, '')")
|
||||
echo "version=$ELECTRON_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Electron version: $ELECTRON_VERSION"
|
||||
|
||||
# Restore Electron/Electron-Builder caches before install-app-deps
|
||||
- name: Cache Electron artifacts
|
||||
id: electron-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/.cache/electron
|
||||
${{ runner.temp }}/.cache/electron-builder
|
||||
~/.cache/electron
|
||||
~/.cache/electron-builder
|
||||
${{ env.LOCALAPPDATA }}\electron-builder\Cache
|
||||
key: electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('package.json', 'bun.lock') }}
|
||||
restore-keys: |
|
||||
electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-
|
||||
electron-cache-${{ matrix.platform }}-
|
||||
|
||||
- name: Cache status
|
||||
run: echo "electron-cache-hit=${{ steps.electron-cache.outputs.cache-hit }}"
|
||||
|
||||
- name: Rebuild native modules for Electron (non-Windows)
|
||||
if: "!startsWith(matrix.platform, 'windows')"
|
||||
run: bunx electron-builder install-app-deps
|
||||
env:
|
||||
npm_config_runtime: electron
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||
|
||||
- name: Setup MSBuild (Windows only)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
vs-version: '17.0'
|
||||
|
||||
- name: Build test (Windows x64)
|
||||
if: matrix.platform == 'windows-x64'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "=========================================="
|
||||
Write-Host "BUILD TEST: ${{ matrix.platform }}"
|
||||
Write-Host "=========================================="
|
||||
node scripts/build-with-builder.js x64 --win --x64
|
||||
Write-Host "✓Build test passed for ${{ matrix.platform }}"
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
npm_config_runtime: electron
|
||||
npm_config_arch: x64
|
||||
npm_config_target_arch: x64
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
npm_config_build_from_source: true
|
||||
MSVS_VERSION: 2022
|
||||
GYP_MSVS_VERSION: 2022
|
||||
WindowsTargetPlatformVersion: 10.0.19041.0
|
||||
CI: true
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Build test (non-Windows)
|
||||
if: "!startsWith(matrix.platform, 'windows')"
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "BUILD TEST: ${{ matrix.platform }}"
|
||||
echo "=========================================="
|
||||
node scripts/build-with-builder.js auto ${{ matrix.build_args }}
|
||||
echo "✓Build test passed for ${{ matrix.platform }}"
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
npm_config_target_arch: ${{ matrix.arch }}
|
||||
npm_config_runtime: electron
|
||||
npm_config_target: ${{ steps.electron-version.outputs.version }}
|
||||
npm_config_disturl: https://electronjs.org/headers
|
||||
CI: true
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Verify build artifacts exist
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "VERIFY ARTIFACTS: ${{ matrix.platform }}"
|
||||
echo "=========================================="
|
||||
ls -lah out || true
|
||||
|
||||
case "${{ matrix.platform }}" in
|
||||
windows-*)
|
||||
ls out/*.exe out/*latest*.yml >/dev/null
|
||||
;;
|
||||
macos-*)
|
||||
ls out/*.dmg out/*.zip out/*latest*.yml >/dev/null
|
||||
;;
|
||||
linux-x64)
|
||||
ls out/*.deb out/*latest*.yml >/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Silent install smoke test (Windows x64)
|
||||
if: matrix.platform == 'windows-x64'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "=========================================="
|
||||
Write-Host "SMOKE INSTALL: windows-x64"
|
||||
Write-Host "=========================================="
|
||||
|
||||
$installer = Get-ChildItem -Path out -Filter "AionUi-*-win-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $installer) {
|
||||
throw "No Windows installer found in out/"
|
||||
}
|
||||
|
||||
Write-Host "Using installer: $($installer.FullName)"
|
||||
Start-Process -FilePath $installer.FullName -ArgumentList '/S' -Wait -NoNewWindow
|
||||
|
||||
$candidates = @(
|
||||
"$env:LOCALAPPDATA\\Programs\\AionUi\\AionUi.exe",
|
||||
"$env:ProgramFiles\\AionUi\\AionUi.exe",
|
||||
"$env:ProgramFiles(x86)\\AionUi\\AionUi.exe"
|
||||
)
|
||||
|
||||
$installedExe = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $installedExe) {
|
||||
throw "Silent install finished but app executable not found in expected locations"
|
||||
}
|
||||
|
||||
Write-Host "Installed executable: $installedExe"
|
||||
|
||||
- name: Install smoke test (macOS arm64)
|
||||
if: matrix.platform == 'macos-arm64'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=========================================="
|
||||
echo "SMOKE INSTALL: macos-arm64"
|
||||
echo "=========================================="
|
||||
|
||||
DMG_FILE=$(ls out/*.dmg | head -n 1)
|
||||
MOUNT_POINT="/tmp/aionui-smoke-mount"
|
||||
APP_DIR="/tmp/aionui-smoke-app"
|
||||
|
||||
rm -rf "$MOUNT_POINT" "$APP_DIR"
|
||||
mkdir -p "$MOUNT_POINT" "$APP_DIR"
|
||||
|
||||
hdiutil attach "$DMG_FILE" -nobrowse -mountpoint "$MOUNT_POINT"
|
||||
cp -R "$MOUNT_POINT"/*.app "$APP_DIR"/
|
||||
hdiutil detach "$MOUNT_POINT"
|
||||
|
||||
APP_PATH=$(ls -d "$APP_DIR"/*.app | head -n 1)
|
||||
APP_BIN="$APP_PATH/Contents/MacOS/AionUi"
|
||||
|
||||
test -x "$APP_BIN"
|
||||
"$APP_BIN" --version || true
|
||||
|
||||
- name: Install smoke test (Linux)
|
||||
if: matrix.platform == 'linux-x64'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=========================================="
|
||||
echo "SMOKE INSTALL: linux-x64"
|
||||
echo "=========================================="
|
||||
|
||||
DEB_FILE=$(ls out/*.deb | head -n 1)
|
||||
PKG_NAME=$(dpkg-deb -f "$DEB_FILE" Package)
|
||||
sudo dpkg -i "$DEB_FILE" || sudo apt-get install -f -y
|
||||
|
||||
INSTALLED_BIN=$(dpkg -L "$PKG_NAME" | grep -Ei '/(bin|opt)/.*(aionui)$' | head -n 1 || true)
|
||||
if [ -z "$INSTALLED_BIN" ]; then
|
||||
echo "Package files:"
|
||||
dpkg -L "$PKG_NAME" | head -n 50
|
||||
echo "No installed executable path matched expected pattern"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test -x "$INSTALLED_BIN"
|
||||
|
||||
# Job 5: Test release scripts (fast, no build required)
|
||||
release-script-test:
|
||||
name: Release Script Test
|
||||
if: github.event.action != 'closed' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create mock build artifacts
|
||||
shell: bash
|
||||
run: bash scripts/create-mock-release-artifacts.sh build-artifacts
|
||||
|
||||
- name: Run prepare-release-assets script
|
||||
shell: bash
|
||||
env:
|
||||
MOCK_VERSION: '1.0.0'
|
||||
run: bash scripts/prepare-release-assets.sh build-artifacts release-assets
|
||||
|
||||
- name: Validate script output
|
||||
shell: bash
|
||||
run: bash scripts/verify-release-assets.sh release-assets
|
||||
@@ -0,0 +1,68 @@
|
||||
name: '✓PR E2E Artifacts'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: pr-e2e-artifacts-${{ github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
|
||||
|
||||
jobs:
|
||||
e2e-artifacts:
|
||||
name: E2E Artifacts (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup just
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run postinstall
|
||||
run: npm run postinstall || true
|
||||
|
||||
- name: Install Linux display dependencies (E2E)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgbm-dev xvfb
|
||||
|
||||
- name: Run E2E tests (with screenshots)
|
||||
run: xvfb-run --auto-servernum just e2e-test
|
||||
env:
|
||||
CI: true
|
||||
E2E_DEV: '1'
|
||||
E2E_SCREENSHOTS: '1'
|
||||
AIONUI_EXTENSIONS_PATH: ${{ github.workspace }}/examples
|
||||
|
||||
- name: Upload E2E report (self-contained with screenshots & traces)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: e2e-report-linux-pr
|
||||
path: |
|
||||
tests/e2e/report/
|
||||
tests/e2e/results/
|
||||
tests/e2e/screenshots/
|
||||
retention-days: 14
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,355 @@
|
||||
name: '📋 Project Automation'
|
||||
|
||||
on:
|
||||
workflow_dispatch: # disabled: requires GitHub App credentials (APP_ID + APP_PRIVATE_KEY)
|
||||
|
||||
# Minimal permissions at workflow level, jobs use PROJECT_TOKEN secret
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
env:
|
||||
PROJECT_NUMBER: 5
|
||||
ORG_NAME: iOfficeAI
|
||||
PROJECT_ID: PVT_kwDOCKhK-M4BN1Ah
|
||||
STATUS_FIELD_ID: PVTSSF_lADOCKhK-M4BN1Ahzg8tcak
|
||||
# Status option IDs
|
||||
STATUS_TO_TRIAGE: 8e8f2d59
|
||||
STATUS_BLOCKED: cd8368e3
|
||||
STATUS_READY_FOR_WORK: 864da50e
|
||||
STATUS_IN_PROGRESS: a5c1c67b
|
||||
STATUS_CLOSED: 91437030
|
||||
STATUS_DONE: 1aa2425f
|
||||
|
||||
jobs:
|
||||
# ============================================
|
||||
# Issue Events: Add to Project & manage status
|
||||
# ============================================
|
||||
|
||||
add-issue-to-project:
|
||||
name: Add Issue to Project
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
outputs:
|
||||
item_id: ${{ steps.add.outputs.itemId }}
|
||||
steps:
|
||||
- name: Mint identity token
|
||||
id: mint_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add issue to project
|
||||
id: add
|
||||
uses: actions/add-to-project@v1.0.2
|
||||
with:
|
||||
project-url: https://github.com/orgs/iOfficeAI/projects/5
|
||||
github-token: ${{ steps.mint_token.outputs.token }}
|
||||
|
||||
set-issue-initial-status:
|
||||
name: Set Issue Initial Status
|
||||
runs-on: ubuntu-latest
|
||||
needs: add-issue-to-project
|
||||
if: github.event_name == 'issues' && github.event.action == 'opened' && needs.add-issue-to-project.outputs.item_id
|
||||
permissions:
|
||||
contents: 'read'
|
||||
steps:
|
||||
- name: Mint identity token
|
||||
id: mint_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set status to To Triage
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.mint_token.outputs.token }}
|
||||
script: |
|
||||
const mutation = `
|
||||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
||||
updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $projectId
|
||||
itemId: $itemId
|
||||
fieldId: $fieldId
|
||||
value: { singleSelectOptionId: $optionId }
|
||||
}) {
|
||||
projectV2Item { id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await github.graphql(mutation, {
|
||||
projectId: process.env.PROJECT_ID,
|
||||
itemId: '${{ needs.add-issue-to-project.outputs.item_id }}',
|
||||
fieldId: process.env.STATUS_FIELD_ID,
|
||||
optionId: process.env.STATUS_TO_TRIAGE
|
||||
});
|
||||
|
||||
core.info('Issue added to project with status: To Triage');
|
||||
|
||||
update-issue-on-close:
|
||||
name: Update Issue on Close
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'issues' && github.event.action == 'closed'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
steps:
|
||||
- name: Mint identity token
|
||||
id: mint_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Update issue status to Done
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.mint_token.outputs.token }}
|
||||
script: |
|
||||
const contentId = context.payload.issue.node_id;
|
||||
|
||||
// Find the item in project
|
||||
const query = `
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org) {
|
||||
projectV2(number: $number) {
|
||||
items(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
content { ... on Issue { id } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
|
||||
const item = result.organization.projectV2.items.nodes.find(i => i.content?.id === contentId);
|
||||
|
||||
if (!item) {
|
||||
core.info('Issue not found in project, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
||||
updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $projectId
|
||||
itemId: $itemId
|
||||
fieldId: $fieldId
|
||||
value: { singleSelectOptionId: $optionId }
|
||||
}) {
|
||||
projectV2Item { id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await github.graphql(mutation, {
|
||||
projectId: process.env.PROJECT_ID,
|
||||
itemId: item.id,
|
||||
fieldId: process.env.STATUS_FIELD_ID,
|
||||
optionId: process.env.STATUS_DONE
|
||||
});
|
||||
|
||||
core.info('Issue closed, status updated to Done');
|
||||
|
||||
update-issue-on-reopen:
|
||||
name: Update Issue on Reopen
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'issues' && github.event.action == 'reopened'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
steps:
|
||||
- name: Mint identity token
|
||||
id: mint_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Update issue status to To Triage
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.mint_token.outputs.token }}
|
||||
script: |
|
||||
const contentId = context.payload.issue.node_id;
|
||||
|
||||
const query = `
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org) {
|
||||
projectV2(number: $number) {
|
||||
items(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
content { ... on Issue { id } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
|
||||
const item = result.organization.projectV2.items.nodes.find(i => i.content?.id === contentId);
|
||||
|
||||
if (!item) {
|
||||
core.info('Issue not found in project, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
||||
updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $projectId
|
||||
itemId: $itemId
|
||||
fieldId: $fieldId
|
||||
value: { singleSelectOptionId: $optionId }
|
||||
}) {
|
||||
projectV2Item { id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await github.graphql(mutation, {
|
||||
projectId: process.env.PROJECT_ID,
|
||||
itemId: item.id,
|
||||
fieldId: process.env.STATUS_FIELD_ID,
|
||||
optionId: process.env.STATUS_TO_TRIAGE
|
||||
});
|
||||
|
||||
core.info('Issue reopened, status updated to To Triage');
|
||||
|
||||
# ============================================
|
||||
# PR Events: Update linked Issue status
|
||||
# ============================================
|
||||
|
||||
pr-update-linked-issue:
|
||||
name: Update Linked Issue Status
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'read'
|
||||
steps:
|
||||
- name: Mint identity token
|
||||
id: mint_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Update linked issue status based on PR event
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.mint_token.outputs.token }}
|
||||
script: |
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const action = context.payload.action;
|
||||
const isMerged = context.payload.pull_request.merged === true;
|
||||
|
||||
// Extract linked issue numbers from PR body
|
||||
const issuePatterns = [
|
||||
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi,
|
||||
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/gi
|
||||
];
|
||||
|
||||
const linkedIssues = new Set();
|
||||
for (const pattern of issuePatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(prBody)) !== null) {
|
||||
linkedIssues.add(parseInt(match[1], 10));
|
||||
}
|
||||
}
|
||||
|
||||
if (linkedIssues.size === 0) {
|
||||
core.info('No linked issues found in PR body, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Found linked issues: ${[...linkedIssues].join(', ')}`);
|
||||
|
||||
// Determine target status based on PR action
|
||||
let targetStatusId;
|
||||
let statusName;
|
||||
|
||||
if (action === 'opened' || action === 'reopened') {
|
||||
// PR opened/reopened -> Issue is In Progress
|
||||
targetStatusId = process.env.STATUS_IN_PROGRESS;
|
||||
statusName = 'In Progress';
|
||||
} else if (action === 'closed') {
|
||||
if (isMerged) {
|
||||
// PR merged -> Issue is Done
|
||||
targetStatusId = process.env.STATUS_DONE;
|
||||
statusName = 'Done';
|
||||
} else {
|
||||
// PR closed without merge -> Issue back to Ready for Work
|
||||
targetStatusId = process.env.STATUS_READY_FOR_WORK;
|
||||
statusName = 'Ready for Work';
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetStatusId) {
|
||||
core.info(`No status change needed for action: ${action}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query to find issues in project
|
||||
const query = `
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org) {
|
||||
projectV2(number: $number) {
|
||||
items(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
content {
|
||||
... on Issue {
|
||||
id
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
|
||||
const projectItems = result.organization.projectV2.items.nodes;
|
||||
|
||||
const mutation = `
|
||||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
||||
updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $projectId
|
||||
itemId: $itemId
|
||||
fieldId: $fieldId
|
||||
value: { singleSelectOptionId: $optionId }
|
||||
}) {
|
||||
projectV2Item { id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Update each linked issue
|
||||
for (const issueNumber of linkedIssues) {
|
||||
const item = projectItems.find(i => i.content?.number === issueNumber);
|
||||
|
||||
if (!item) {
|
||||
core.info(`Issue #${issueNumber} not found in project, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await github.graphql(mutation, {
|
||||
projectId: process.env.PROJECT_ID,
|
||||
itemId: item.id,
|
||||
fieldId: process.env.STATUS_FIELD_ID,
|
||||
optionId: targetStatusId
|
||||
});
|
||||
|
||||
core.info(`Issue #${issueNumber} status updated to ${statusName} (PR #${prNumber} ${action}${isMerged ? ' merged' : ''})`);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
name: Distribute Release Assets
|
||||
|
||||
# Triggered after a GitHub Release is officially published.
|
||||
# Mirrors installer assets to the configured distribution endpoint for external download.
|
||||
#
|
||||
# Why `published` (not `released`)?
|
||||
# - `released` is supposed to fire when a non-prerelease release is published,
|
||||
# but GitHub sometimes only emits `published` (not `released`) when a pre-existing
|
||||
# draft is publicly released, causing the workflow to be silently skipped.
|
||||
# - `published` reliably fires on every draft → public transition, including
|
||||
# prereleases. The `if` guard on the job filters out prereleases explicitly.
|
||||
#
|
||||
# All credentials / identifiers are read from repository secrets:
|
||||
# - AWS_REGION
|
||||
# - AWS_ROLE_ARN
|
||||
# - AWS_S3_BUCKET
|
||||
# Do NOT hardcode these values in this file.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
# Manual trigger for smoke testing or for retrying a missed release:
|
||||
# pick any existing release tag to re-run the distribution against.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing release tag to redistribute (e.g. v1.9.21)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
id-token: write # required for OIDC token
|
||||
contents: read
|
||||
|
||||
env:
|
||||
S3_RELEASE_PREFIX: releases
|
||||
|
||||
jobs:
|
||||
distribute:
|
||||
name: Distribute release assets
|
||||
runs-on: ubuntu-latest
|
||||
# Skip prereleases: manual dispatch always runs, automatic event runs only when not a prerelease.
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
env:
|
||||
EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
TAG="${EVENT_TAG:-$INPUT_TAG}"
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "::error::No tag provided (event did not supply one)."
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${TAG#v}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Tag: $TAG / Version: $VERSION"
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Guard against same-version overwrite
|
||||
env:
|
||||
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
PREFIX="${S3_RELEASE_PREFIX}/${VERSION}/"
|
||||
DEST="s3://${S3_BUCKET}/${PREFIX}"
|
||||
COUNT=$(aws s3api list-objects-v2 \
|
||||
--bucket "$S3_BUCKET" \
|
||||
--prefix "$PREFIX" \
|
||||
--no-paginate \
|
||||
--query 'KeyCount' \
|
||||
--output text)
|
||||
if [ "$COUNT" -gt 0 ]; then
|
||||
echo "::error::Version directory already contains $COUNT file(s). Refusing to overwrite."
|
||||
echo "::error::Same-version re-publish is not allowed (downstream caches would serve stale files). Release a new version instead."
|
||||
aws s3 ls "$DEST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download release assets from GitHub
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p dist
|
||||
gh release download "${{ steps.version.outputs.tag }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--dir dist \
|
||||
--pattern "*.dmg" \
|
||||
--pattern "*.exe" \
|
||||
--pattern "*.msi" \
|
||||
--pattern "*.deb" \
|
||||
--pattern "*.zip" \
|
||||
--pattern "latest*.yml"
|
||||
echo "Downloaded files:"
|
||||
ls -la dist
|
||||
|
||||
- name: Validate updater metadata
|
||||
id: metadata
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
metadata_files=(
|
||||
dist/latest.yml
|
||||
dist/latest-win-arm64.yml
|
||||
dist/latest-mac.yml
|
||||
dist/latest-arm64-mac.yml
|
||||
dist/latest-linux.yml
|
||||
dist/latest-linux-arm64.yml
|
||||
)
|
||||
|
||||
for file in "${metadata_files[@]}"; do
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "::error::Missing updater metadata file: $file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
metadata_version=$(grep -E "^version:" "$file" | head -n 1 | sed -E "s/^version:[[:space:]]*['\"]?([^'\"]+).*/\1/")
|
||||
if [ "$metadata_version" != "$VERSION" ]; then
|
||||
echo "::error::$file version is $metadata_version, expected $VERSION."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
{
|
||||
echo "files<<EOF"
|
||||
for file in "${metadata_files[@]}"; do
|
||||
basename "$file"
|
||||
done
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload assets
|
||||
env:
|
||||
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
aws s3 cp dist/ "s3://${S3_BUCKET}/${S3_RELEASE_PREFIX}/${VERSION}/" \
|
||||
--recursive
|
||||
echo "Uploaded release ${VERSION}"
|
||||
|
||||
- name: Upload updater metadata
|
||||
env:
|
||||
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
aws s3 cp dist/ "s3://${S3_BUCKET}/${S3_RELEASE_PREFIX}/" \
|
||||
--recursive \
|
||||
--exclude "*" \
|
||||
--include "latest*.yml" \
|
||||
--content-type "text/yaml" \
|
||||
--cache-control "public, max-age=60"
|
||||
echo "Uploaded updater metadata"
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
FILES: ${{ steps.metadata.outputs.files }}
|
||||
run: |
|
||||
{
|
||||
echo "### Release assets distributed"
|
||||
echo ""
|
||||
echo "- Tag: \`${TAG}\`"
|
||||
echo "- Version: \`${VERSION}\`"
|
||||
echo "- Asset prefix: \`${S3_RELEASE_PREFIX}/${VERSION}/\`"
|
||||
echo "- Metadata prefix: \`${S3_RELEASE_PREFIX}/\`"
|
||||
echo ""
|
||||
echo "Uploaded metadata:"
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && echo "- \`${S3_RELEASE_PREFIX}/$file\`"
|
||||
done <<< "$FILES"
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.DS_Store
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
node_modules
|
||||
jspm_packages/
|
||||
|
||||
# Lock files for other package managers (project uses bun.lock)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Webpack
|
||||
.webpack/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Electron-Forge
|
||||
out/
|
||||
|
||||
# IDE and Editor files
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# AI CLI tool configurations (may contain sensitive information)
|
||||
**/.claude/settings.local.json
|
||||
**/.claude/settings.json
|
||||
.qwen/
|
||||
.qwen/*/
|
||||
.specify/scripts/
|
||||
.gemini/
|
||||
.gemini/*/
|
||||
|
||||
# Keep important project documentation
|
||||
!.specify/memory/constitution.md
|
||||
|
||||
# Temporary and backup files
|
||||
*.tmp
|
||||
.tmp_*
|
||||
tmp-webui.err
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
.#*
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Build and output directories
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
out-fast-builds/
|
||||
|
||||
# Keep extension example dist (pre-built distributable assets)
|
||||
!examples/**/dist/
|
||||
.webpack/
|
||||
|
||||
# Debug and test files
|
||||
debug.log
|
||||
test-*.js
|
||||
!examples/**/test-*.js
|
||||
diagnose-*.js
|
||||
|
||||
# Keep .gemini config for AI code review (but ignore local cache)
|
||||
!.gemini/
|
||||
.gemini/cache/
|
||||
gha-creds-*.json
|
||||
|
||||
# Spec-Kit working files
|
||||
specs/**/.DS_Store
|
||||
specs/**/temp/
|
||||
specs/**/tmp/
|
||||
specs/**/*.tmp
|
||||
specs/**/*.cache
|
||||
specs/**/draft-*
|
||||
specs/**/work-*
|
||||
.spec-cache/
|
||||
.tmp/
|
||||
my-video
|
||||
${env.ELECTRON_CACHE}/
|
||||
|
||||
# Agent Kit
|
||||
.agent
|
||||
.codebuddy/
|
||||
wiki/
|
||||
|
||||
# Local design docs and screenshots
|
||||
desdoc/
|
||||
|
||||
# Local kanban (协作工作簿,不入库)
|
||||
kanban/
|
||||
|
||||
# E2E test artifacts (Playwright)
|
||||
tests/e2e/screenshots/
|
||||
tests/e2e/results/
|
||||
tests/e2e/report/
|
||||
|
||||
# PowerShell accidentally creates a literal file named '$null' when a command
|
||||
# redirects output to $null (e.g. `cmd > $null`) on Windows runners.
|
||||
# This file has no meaning and should never be committed.
|
||||
$null
|
||||
|
||||
# vx tool manager (local development only, not used in CI)
|
||||
vx.lock
|
||||
vx.toml
|
||||
.taichuy/*
|
||||
kylindoc/
|
||||
.workspace/
|
||||
lint-results.txt
|
||||
|
||||
# Git worktrees
|
||||
.worktree/
|
||||
.worktrees/
|
||||
worktree/
|
||||
worktrees/
|
||||
|
||||
# Superpowers brainstorm tool (runtime files, session data)
|
||||
.superpowers/
|
||||
docs/superpowers/
|
||||
resources/bundled-bun
|
||||
resources/bundled-aioncore
|
||||
resources/hub
|
||||
/resources/windows/support/_sentry-dsn.generated.nsh
|
||||
|
||||
# Server build output
|
||||
dist-server/
|
||||
.omc/
|
||||
.superset/
|
||||
.claude/teams/
|
||||
iterations/
|
||||
graphify-out/
|
||||
.graphify_python
|
||||
|
||||
.snow/
|
||||
|
||||
# Local scratch/analysis (wireframes, candidate mockups)
|
||||
.analysis/
|
||||
@@ -0,0 +1,4 @@
|
||||
# Electron mirror is set via environment variable in CI workflows
|
||||
# Do not set ELECTRON_MIRROR here as it causes electron-builder to auto-derive
|
||||
# an incorrect binaries mirror path (cdn.npmmirror.com/binaries/electron/ instead of electron-builder-binaries/)
|
||||
# See: https://github.com/electron-userland/electron-builder/issues/3954
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": true,
|
||||
"bracketSameLine": false
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn",
|
||||
"pedantic": "off",
|
||||
"perf": "warn",
|
||||
"style": "off",
|
||||
"nursery": "off"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-explicit-any": "warn",
|
||||
"no-empty-pattern": "warn",
|
||||
"no-constant-condition": "warn",
|
||||
"consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"no-floating-promises": "error",
|
||||
"no-await-thenable": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js"],
|
||||
"rules": {
|
||||
"no-floating-promises": "off",
|
||||
"no-await-thenable": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["forge.config.ts", "vitest.config.ts", "tests/**/*.ts", "tests/**/*.tsx"],
|
||||
"rules": {
|
||||
"no-floating-promises": "off",
|
||||
"no-await-thenable": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/gemini/cli/**/*.ts"],
|
||||
"rules": {
|
||||
"no-floating-promises": "off",
|
||||
"no-await-thenable": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"build/",
|
||||
".webpack/",
|
||||
"out/",
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"*.log",
|
||||
"public/",
|
||||
"resources/",
|
||||
"docs/",
|
||||
"config/webpack/",
|
||||
"scripts/",
|
||||
"mobile/"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# Pre-commit configuration for AionUi
|
||||
# Ensures local checks match CI checks (TypeScript, Oxlint, Oxfmt)
|
||||
#
|
||||
# Installation:
|
||||
# pip install pre-commit (or: brew install pre-commit / choco install pre-commit)
|
||||
# prek install (or: pre-commit install)
|
||||
#
|
||||
# Run manually on staged files (same as CI pre-commit):
|
||||
# prek run
|
||||
#
|
||||
# Run on changed files vs main (same as CI PR check):
|
||||
# prek run --from-ref origin/main --to-ref HEAD
|
||||
#
|
||||
# Run on all files:
|
||||
# prek run --all-files
|
||||
|
||||
default_install_hook_types:
|
||||
- pre-commit
|
||||
- commit-msg
|
||||
|
||||
repos:
|
||||
# General file checks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: [--unsafe]
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1000]
|
||||
- id: end-of-file-fixer
|
||||
# Only fix text files, exclude binary files and special OS files
|
||||
exclude: '^(\.DS_Store|Thumbs\.db|.*\.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|pdf|zip|tar|gz|bz2|7z|exe|dll|so|dylib|node|bin))$'
|
||||
- id: trailing-whitespace
|
||||
# Auto-fix trailing whitespace in text files
|
||||
# Excludes binary files (matching end-of-file-fixer pattern)
|
||||
exclude: '^(\.DS_Store|Thumbs\.db|.*\.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|pdf|zip|tar|gz|bz2|7z|exe|dll|so|dylib|node|bin|patch))$'
|
||||
|
||||
# TypeScript type checking (full project check required)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: typecheck
|
||||
name: TypeScript Check
|
||||
entry: bunx tsc --noEmit
|
||||
language: system
|
||||
files: \.(ts|tsx)$
|
||||
pass_filenames: false
|
||||
|
||||
# Oxlint (replaces ESLint — checks staged files only)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: oxlint
|
||||
name: Oxlint
|
||||
entry: bun run lint
|
||||
language: system
|
||||
files: \.(ts|tsx|js|jsx)$
|
||||
pass_filenames: false
|
||||
|
||||
# Oxfmt (replaces Prettier — auto-fixes staged files only)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: oxfmt
|
||||
name: Oxfmt
|
||||
entry: bun run format
|
||||
language: system
|
||||
files: \.(ts|tsx|js|jsx|json|css|md|yaml|yml|toml)$
|
||||
pass_filenames: true
|
||||
|
||||
# i18n translation files validation
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: i18n-check
|
||||
name: i18n Check
|
||||
entry: node scripts/check-i18n.js
|
||||
language: system
|
||||
files: ^packages/desktop/src/renderer/services/i18n/locales/
|
||||
pass_filenames: false
|
||||
|
||||
# Commit message format (Conventional Commits)
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: [--strict, --force-scope, feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert]
|
||||
@@ -0,0 +1 @@
|
||||
mobile/
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": true,
|
||||
"bracketSameLine": false
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
# AionUi Constitution
|
||||
|
||||
## Core Principles
|
||||
|
||||
### I. Multi-Agent AI Integration
|
||||
|
||||
AionUi serves as a unified desktop interface for multiple AI terminal agents (Gemini CLI, Claude Code, Qwen Code, etc.). Each AI agent integration must be:
|
||||
|
||||
- Protocol-agnostic with standardized adapters
|
||||
- Independently manageable and configurable
|
||||
- Cross-platform compatible (macOS, Windows, Linux)
|
||||
- Real-time streaming capable for live interaction
|
||||
|
||||
### II. Modular Architecture First
|
||||
|
||||
Every major feature is implemented as an independent, testable module:
|
||||
|
||||
- Bridge pattern for IPC communication (dialog, fs, conversation, auth, etc.)
|
||||
- Agent managers as separate, swappable components
|
||||
- UI components with clear separation of concerns
|
||||
- Shared utilities and common interfaces
|
||||
|
||||
### III. User Experience Excellence
|
||||
|
||||
User interaction must be intuitive and efficient:
|
||||
|
||||
- Chat-based interface with file drag-and-drop support
|
||||
- Multi-conversation management with context isolation
|
||||
- Workspace integration for seamless file operations
|
||||
- Responsive UI with proper loading states and error handling
|
||||
|
||||
### IV. Security and Privacy First
|
||||
|
||||
All user data and AI interactions must be secure:
|
||||
|
||||
- Local storage of conversation history and settings
|
||||
- Secure API key management with encryption
|
||||
- No data transmission without explicit user consent
|
||||
- Proper credential isolation between different AI providers
|
||||
|
||||
### V. Developer Experience and Maintainability
|
||||
|
||||
Code must be maintainable and extensible:
|
||||
|
||||
- TypeScript for type safety across the entire stack
|
||||
- ESLint and Prettier for consistent code quality
|
||||
- Modular commit message format (feat/fix/chore/docs/refactor)
|
||||
- Clear documentation for architectural decisions
|
||||
|
||||
## Technology Standards
|
||||
|
||||
### Electron Framework
|
||||
|
||||
- Use Electron Forge for build and packaging management
|
||||
- Maintain main process and renderer process separation
|
||||
- Leverage IPC bridges for secure communication
|
||||
- Support hot reload in development for rapid iteration
|
||||
|
||||
### React and TypeScript
|
||||
|
||||
- React with functional components and hooks
|
||||
- Strict TypeScript configuration with comprehensive type checking
|
||||
- UnoCSS for atomic CSS styling
|
||||
- Arco Design components for consistent UI patterns
|
||||
|
||||
### State Management
|
||||
|
||||
- React Context + SWR for data fetching and caching
|
||||
- Local electron-store for persistent application settings
|
||||
- File-system based storage for conversation history
|
||||
- Event-driven communication between components
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Quality Gates
|
||||
|
||||
- Pre-commit hooks with lint-staged for automatic formatting
|
||||
- ESLint warnings must be addressed before merge
|
||||
- No console.log statements in production code
|
||||
- All public interfaces must have TypeScript documentation
|
||||
|
||||
### Version Management
|
||||
|
||||
- Semantic versioning (MAJOR.MINOR.PATCH) strictly enforced
|
||||
- Automated version updates via release scripts
|
||||
- CI/CD pipeline handles building and code signing
|
||||
- Git tag creation automated on version changes
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- Feature branches for new functionality development
|
||||
- Main branch for production-ready code
|
||||
- No direct commits to main branch
|
||||
- Pull request reviews required for all changes
|
||||
|
||||
## Governance
|
||||
|
||||
### Architecture Decisions
|
||||
|
||||
- Constitutional principles supersede implementation preferences
|
||||
- Breaking changes require architectural review and migration plan
|
||||
- New AI agent integrations must follow established adapter patterns
|
||||
- Performance regressions require justification and timeline for resolution
|
||||
|
||||
### Compliance Requirements
|
||||
|
||||
- All features must work across supported platforms (macOS, Windows, Linux)
|
||||
- User data privacy and security standards are non-negotiable
|
||||
- Accessibility considerations for all UI components
|
||||
- Regular dependency updates for security patches
|
||||
|
||||
**Version**: 1.0.0 | **Ratified**: 2025-01-22 | **Last Amended**: 2025-01-22
|
||||
@@ -0,0 +1,28 @@
|
||||
# [PROJECT NAME] Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: [DATE]
|
||||
|
||||
## Active Technologies
|
||||
|
||||
[EXTRACTED FROM ALL PLAN.MD FILES]
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
[ACTUAL STRUCTURE FROM PLANS]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
|
||||
|
||||
## Code Style
|
||||
|
||||
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
|
||||
|
||||
## Recent Changes
|
||||
|
||||
[LAST 3 FEATURES AND WHAT THEY ADDED]
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
@@ -0,0 +1,229 @@
|
||||
# Implementation Plan: [FEATURE]
|
||||
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
## Execution Flow (/plan command scope)
|
||||
|
||||
```
|
||||
1. Load feature spec from Input path
|
||||
→ If not found: ERROR "No feature spec at {path}"
|
||||
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
|
||||
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
|
||||
→ Set Structure Decision based on project type
|
||||
3. Fill the Constitution Check section based on the content of the constitution document.
|
||||
4. Evaluate Constitution Check section below
|
||||
→ If violations exist: Document in Complexity Tracking
|
||||
→ If no justification possible: ERROR "Simplify approach first"
|
||||
→ Update Progress Tracking: Initial Constitution Check
|
||||
5. Execute Phase 0 → research.md
|
||||
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
|
||||
6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, `QWEN.md` for Qwen Code or `AGENTS.md` for opencode).
|
||||
7. Re-evaluate Constitution Check section
|
||||
→ If new violations: Refactor design, return to Phase 1
|
||||
→ Update Progress Tracking: Post-Design Constitution Check
|
||||
8. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
|
||||
9. STOP - Ready for /tasks command
|
||||
```
|
||||
|
||||
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
|
||||
|
||||
- Phase 2: /tasks command creates tasks.md
|
||||
- Phase 3-4: Implementation execution (manual or via tools)
|
||||
|
||||
## Summary
|
||||
|
||||
[Extract from feature spec: primary requirement + technical approach from research]
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
|
||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||
**Project Type**: [single/web/mobile - determines source structure]
|
||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
||||
|
||||
## Constitution Check
|
||||
|
||||
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/plan command output)
|
||||
├── research.md # Phase 0 output (/plan command)
|
||||
├── data-model.md # Phase 1 output (/plan command)
|
||||
├── quickstart.md # Phase 1 output (/plan command)
|
||||
├── contracts/ # Phase 1 output (/plan command)
|
||||
└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```
|
||||
# Option 1: Single project (DEFAULT)
|
||||
src/
|
||||
├── models/
|
||||
├── services/
|
||||
├── cli/
|
||||
└── lib/
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
├── integration/
|
||||
└── unit/
|
||||
|
||||
# Option 2: Web application (when "frontend" + "backend" detected)
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── api/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── services/
|
||||
└── tests/
|
||||
|
||||
# Option 3: Mobile + API (when "iOS/Android" detected)
|
||||
api/
|
||||
└── [same as backend above]
|
||||
|
||||
ios/ or android/
|
||||
└── [platform-specific structure]
|
||||
```
|
||||
|
||||
**Structure Decision**: [DEFAULT to Option 1 unless Technical Context indicates web/mobile app]
|
||||
|
||||
## Phase 0: Outline & Research
|
||||
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
|
||||
```
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
## Phase 1: Design & Contracts
|
||||
|
||||
_Prerequisites: research.md complete_
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Generate API contracts** from functional requirements:
|
||||
- For each user action → endpoint
|
||||
- Use standard REST/GraphQL patterns
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/`
|
||||
|
||||
3. **Generate contract tests** from contracts:
|
||||
- One test file per endpoint
|
||||
- Assert request/response schemas
|
||||
- Tests must fail (no implementation yet)
|
||||
|
||||
4. **Extract test scenarios** from user stories:
|
||||
- Each story → integration test scenario
|
||||
- Quickstart test = story validation steps
|
||||
|
||||
5. **Update agent file incrementally** (O(1) operation):
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh claude`
|
||||
**IMPORTANT**: Execute it exactly as specified above. Do not add or remove any arguments.
|
||||
- If exists: Add only NEW tech from current plan
|
||||
- Preserve manual additions between markers
|
||||
- Update recent changes (keep last 3)
|
||||
- Keep under 150 lines for token efficiency
|
||||
- Output to repository root
|
||||
|
||||
**Output**: data-model.md, /contracts/\*, failing tests, quickstart.md, agent-specific file
|
||||
|
||||
## Phase 2: Task Planning Approach
|
||||
|
||||
_This section describes what the /tasks command will do - DO NOT execute during /plan_
|
||||
|
||||
**Task Generation Strategy**:
|
||||
|
||||
- Load `.specify/templates/tasks-template.md` as base
|
||||
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
|
||||
- Each contract → contract test task [P]
|
||||
- Each entity → model creation task [P]
|
||||
- Each user story → integration test task
|
||||
- Implementation tasks to make tests pass
|
||||
|
||||
**Ordering Strategy**:
|
||||
|
||||
- TDD order: Tests before implementation
|
||||
- Dependency order: Models before services before UI
|
||||
- Mark [P] for parallel execution (independent files)
|
||||
|
||||
**Estimated Output**: 25-30 numbered, ordered tasks in tasks.md
|
||||
|
||||
**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
|
||||
|
||||
## Phase 3+: Future Implementation
|
||||
|
||||
_These phases are beyond the scope of the /plan command_
|
||||
|
||||
**Phase 3**: Task execution (/tasks command creates tasks.md)
|
||||
**Phase 4**: Implementation (execute tasks.md following constitutional principles)
|
||||
**Phase 5**: Validation (run tests, execute quickstart.md, performance validation)
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
_Fill ONLY if Constitution Check has violations that must be justified_
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
| -------------------------- | ------------------ | ------------------------------------ |
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
_This checklist is updated during execution flow_
|
||||
|
||||
**Phase Status**:
|
||||
|
||||
- [ ] Phase 0: Research complete (/plan command)
|
||||
- [ ] Phase 1: Design complete (/plan command)
|
||||
- [ ] Phase 2: Task planning complete (/plan command - describe approach only)
|
||||
- [ ] Phase 3: Tasks generated (/tasks command)
|
||||
- [ ] Phase 4: Implementation complete
|
||||
- [ ] Phase 5: Validation passed
|
||||
|
||||
**Gate Status**:
|
||||
|
||||
- [ ] Initial Constitution Check: PASS
|
||||
- [ ] Post-Design Constitution Check: PASS
|
||||
- [ ] All NEEDS CLARIFICATION resolved
|
||||
- [ ] Complexity deviations documented
|
||||
|
||||
---
|
||||
|
||||
_Based on Constitution v2.1.1 - See `/memory/constitution.md`_
|
||||
@@ -0,0 +1,131 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Created**: [DATE]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## Execution Flow (main)
|
||||
|
||||
```
|
||||
1. Parse user description from Input
|
||||
→ If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
→ Identify: actors, actions, data, constraints
|
||||
3. For each unclear aspect:
|
||||
→ Mark with [NEEDS CLARIFICATION: specific question]
|
||||
4. Fill User Scenarios & Testing section
|
||||
→ If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
→ Each requirement must be testable
|
||||
→ Mark ambiguous requirements
|
||||
6. Identify Key Entities (if data involved)
|
||||
7. Run Review Checklist
|
||||
→ If any [NEEDS CLARIFICATION]: WARN "Spec has uncertainties"
|
||||
→ If implementation details found: ERROR "Remove tech details"
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick Guidelines
|
||||
|
||||
- ✅ Focus on WHAT users need and WHY
|
||||
- ❌ Avoid HOW to implement (no tech stack, APIs, code structure)
|
||||
- 👥 Written for business stakeholders, not developers
|
||||
|
||||
### Section Requirements
|
||||
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
|
||||
When creating this spec from a user prompt:
|
||||
|
||||
1. **Mark all ambiguities**: Use [NEEDS CLARIFICATION: specific question] for any assumption you'd need to make
|
||||
2. **Don't guess**: If the prompt doesn't specify something (e.g., "login system" without auth method), mark it
|
||||
3. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
4. **Common underspecified areas**:
|
||||
- User types and permissions
|
||||
- Data retention/deletion policies
|
||||
- Performance targets and scale
|
||||
- Error handling behaviors
|
||||
- Integration requirements
|
||||
- Security/compliance needs
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing _(mandatory)_
|
||||
|
||||
### Primary User Story
|
||||
|
||||
[Describe the main user journey in plain language]
|
||||
|
||||
### Acceptance Scenarios
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- What happens when [boundary condition]?
|
||||
- How does system handle [error scenario]?
|
||||
|
||||
## Requirements _(mandatory)_
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
|
||||
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
|
||||
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
|
||||
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
|
||||
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
|
||||
|
||||
_Example of marking unclear requirements:_
|
||||
|
||||
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
|
||||
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
|
||||
|
||||
### Key Entities _(include if feature involves data)_
|
||||
|
||||
- **[Entity 1]**: [What it represents, key attributes without implementation]
|
||||
- **[Entity 2]**: [What it represents, relationships to other entities]
|
||||
|
||||
---
|
||||
|
||||
## Review & Acceptance Checklist
|
||||
|
||||
_GATE: Automated checks run during main() execution_
|
||||
|
||||
### Content Quality
|
||||
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
### Requirement Completeness
|
||||
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
---
|
||||
|
||||
## Execution Status
|
||||
|
||||
_Updated by main() during processing_
|
||||
|
||||
- [ ] User description parsed
|
||||
- [ ] Key concepts extracted
|
||||
- [ ] Ambiguities marked
|
||||
- [ ] User scenarios defined
|
||||
- [ ] Requirements generated
|
||||
- [ ] Entities identified
|
||||
- [ ] Review checklist passed
|
||||
|
||||
---
|
||||
@@ -0,0 +1,141 @@
|
||||
# Tasks: [FEATURE NAME]
|
||||
|
||||
**Input**: Design documents from `/specs/[###-feature-name]/`
|
||||
**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/
|
||||
|
||||
## Execution Flow (main)
|
||||
|
||||
```
|
||||
1. Load plan.md from feature directory
|
||||
→ If not found: ERROR "No implementation plan found"
|
||||
→ Extract: tech stack, libraries, structure
|
||||
2. Load optional design documents:
|
||||
→ data-model.md: Extract entities → model tasks
|
||||
→ contracts/: Each file → contract test task
|
||||
→ research.md: Extract decisions → setup tasks
|
||||
3. Generate tasks by category:
|
||||
→ Setup: project init, dependencies, linting
|
||||
→ Tests: contract tests, integration tests
|
||||
→ Core: models, services, CLI commands
|
||||
→ Integration: DB, middleware, logging
|
||||
→ Polish: unit tests, performance, docs
|
||||
4. Apply task rules:
|
||||
→ Different files = mark [P] for parallel
|
||||
→ Same file = sequential (no [P])
|
||||
→ Tests before implementation (TDD)
|
||||
5. Number tasks sequentially (T001, T002...)
|
||||
6. Generate dependency graph
|
||||
7. Create parallel execution examples
|
||||
8. Validate task completeness:
|
||||
→ All contracts have tests?
|
||||
→ All entities have models?
|
||||
→ All endpoints implemented?
|
||||
9. Return: SUCCESS (tasks ready for execution)
|
||||
```
|
||||
|
||||
## Format: `[ID] [P?] Description`
|
||||
|
||||
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||
- Include exact file paths in descriptions
|
||||
|
||||
## Path Conventions
|
||||
|
||||
- **Single project**: `src/`, `tests/` at repository root
|
||||
- **Web app**: `backend/src/`, `frontend/src/`
|
||||
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
|
||||
- Paths shown below assume single project - adjust based on plan.md structure
|
||||
|
||||
## Phase 3.1: Setup
|
||||
|
||||
- [ ] T001 Create project structure per implementation plan
|
||||
- [ ] T002 Initialize [language] project with [framework] dependencies
|
||||
- [ ] T003 [P] Configure linting and formatting tools
|
||||
|
||||
## Phase 3.2: Tests First (TDD) ⚠️ MUST COMPLETE BEFORE 3.3
|
||||
|
||||
**CRITICAL: These tests MUST be written and MUST FAIL before ANY implementation**
|
||||
|
||||
- [ ] T004 [P] Contract test POST /api/users in tests/contract/test_users_post.py
|
||||
- [ ] T005 [P] Contract test GET /api/users/{id} in tests/contract/test_users_get.py
|
||||
- [ ] T006 [P] Integration test user registration in tests/integration/test_registration.py
|
||||
- [ ] T007 [P] Integration test auth flow in tests/integration/test_auth.py
|
||||
|
||||
## Phase 3.3: Core Implementation (ONLY after tests are failing)
|
||||
|
||||
- [ ] T008 [P] User model in src/models/user.py
|
||||
- [ ] T009 [P] UserService CRUD in src/services/user_service.py
|
||||
- [ ] T010 [P] CLI --create-user in src/cli/user_commands.py
|
||||
- [ ] T011 POST /api/users endpoint
|
||||
- [ ] T012 GET /api/users/{id} endpoint
|
||||
- [ ] T013 Input validation
|
||||
- [ ] T014 Error handling and logging
|
||||
|
||||
## Phase 3.4: Integration
|
||||
|
||||
- [ ] T015 Connect UserService to DB
|
||||
- [ ] T016 Auth middleware
|
||||
- [ ] T017 Request/response logging
|
||||
- [ ] T018 CORS and security headers
|
||||
|
||||
## Phase 3.5: Polish
|
||||
|
||||
- [ ] T019 [P] Unit tests for validation in tests/unit/test_validation.py
|
||||
- [ ] T020 Performance tests (<200ms)
|
||||
- [ ] T021 [P] Update docs/api.md
|
||||
- [ ] T022 Remove duplication
|
||||
- [ ] T023 Run manual-testing.md
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Tests (T004-T007) before implementation (T008-T014)
|
||||
- T008 blocks T009, T015
|
||||
- T016 blocks T018
|
||||
- Implementation before polish (T019-T023)
|
||||
|
||||
## Parallel Example
|
||||
|
||||
```
|
||||
# Launch T004-T007 together:
|
||||
Task: "Contract test POST /api/users in tests/contract/test_users_post.py"
|
||||
Task: "Contract test GET /api/users/{id} in tests/contract/test_users_get.py"
|
||||
Task: "Integration test registration in tests/integration/test_registration.py"
|
||||
Task: "Integration test auth in tests/integration/test_auth.py"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- [P] tasks = different files, no dependencies
|
||||
- Verify tests fail before implementing
|
||||
- Commit after each task
|
||||
- Avoid: vague tasks, same file conflicts
|
||||
|
||||
## Task Generation Rules
|
||||
|
||||
_Applied during main() execution_
|
||||
|
||||
1. **From Contracts**:
|
||||
- Each contract file → contract test task [P]
|
||||
- Each endpoint → implementation task
|
||||
|
||||
2. **From Data Model**:
|
||||
- Each entity → model creation task [P]
|
||||
- Relationships → service layer tasks
|
||||
|
||||
3. **From User Stories**:
|
||||
- Each story → integration test [P]
|
||||
- Quickstart scenarios → validation tasks
|
||||
|
||||
4. **Ordering**:
|
||||
- Setup → Tests → Models → Services → Endpoints → Polish
|
||||
- Dependencies block parallel execution
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
_GATE: Checked by main() before returning_
|
||||
|
||||
- [ ] All contracts have corresponding tests
|
||||
- [ ] All entities have model tasks
|
||||
- [ ] All tests come before implementation
|
||||
- [ ] Parallel tasks truly independent
|
||||
- [ ] Each task specifies exact file path
|
||||
- [ ] No task modifies same file as another [P] task
|
||||
@@ -0,0 +1,154 @@
|
||||
# AionUi - Project Guide
|
||||
|
||||
All contributors (human and AI) must follow [CONTRIBUTING.md](CONTRIBUTING.md) before opening a PR. ([Chinese version](CONTRIBUTING.zh.md))
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### File & Directory Structure
|
||||
|
||||
- **Directory size limit**: Prefer ≤ **10** direct children per directory; new or substantially reorganized directories must satisfy this.
|
||||
|
||||
See [docs/contributing/file-structure.md](docs/contributing/file-structure.md) for complete rules. Agents must also follow the `architecture` skill (`.claude/skills/architecture/SKILL.md`) when creating files or modules.
|
||||
|
||||
### Naming
|
||||
|
||||
- **Components**: PascalCase (`Button.tsx`, `Modal.tsx`)
|
||||
- **Utilities**: camelCase (`formatDate.ts`)
|
||||
- **Hooks**: camelCase with `use` prefix (`useTheme.ts`)
|
||||
- **Constants files**: camelCase (`constants.ts`) — values inside use UPPER_SNAKE_CASE
|
||||
- **Type files**: camelCase (`types.ts`)
|
||||
- **Style files**: kebab-case or `ComponentName.module.css`
|
||||
- **Unused params**: prefix with `_`
|
||||
|
||||
### UI Library & Icons
|
||||
|
||||
- **Components**: `@arco-design/web-react` — no raw interactive HTML (`<button>`, `<input>`, `<select>`, etc.)
|
||||
- **Icons**: `@icon-park/react`
|
||||
|
||||
### CSS
|
||||
|
||||
- Prefer **UnoCSS utility classes**; complex styles use **CSS Modules** (`ComponentName.module.css`)
|
||||
- Colors must use **semantic tokens** from `uno.config.ts` or CSS variables — no hardcoded values
|
||||
- Arco theme overrides go in `packages/desktop/src/renderer/styles/arco-override.css`; component-scoped Arco overrides use CSS Module with `:global()`
|
||||
- Global styles only in `packages/desktop/src/renderer/styles/`
|
||||
|
||||
Formatting rules (Oxfmt, Prettier-compatible):
|
||||
|
||||
- Single-element arrays that fit on one line → inline: `[{ id: 'a', value: 'b' }]`
|
||||
- Trailing commas required in multi-line arrays/objects
|
||||
- Single quotes for strings
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Strict mode enabled — no `any`, no implicit returns
|
||||
- Use path aliases: `@/*`, `@process/*`, `@renderer/*`
|
||||
- Prefer `type` over `interface` (per Oxlint config)
|
||||
- English for code comments; JSDoc for public functions
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
New or changed user-facing text must use i18n keys; do not introduce hardcoded strings. Languages and modules are defined in `packages/desktop/src/common/config/i18n-config.json`.
|
||||
|
||||
See the `i18n` skill (`.claude/skills/i18n/SKILL.md`) for complete workflow, key naming, and validation steps.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two process types — never mix their APIs:
|
||||
|
||||
| Process | Path | Restriction |
|
||||
| -------- | -------------------------------- | --------------- |
|
||||
| Main | `packages/desktop/src/process/` | No DOM APIs |
|
||||
| Renderer | `packages/desktop/src/renderer/` | No Node.js APIs |
|
||||
|
||||
Cross-process communication must go through the IPC bridge (`packages/desktop/src/preload/`).
|
||||
See [docs/architecture/overview.md](docs/architecture/overview.md) for details.
|
||||
|
||||
## Testing
|
||||
|
||||
**Framework**: Vitest 4 (`vitest.config.ts`). Project coverage target is ≥ 80%; ordinary changes should add focused tests for changed behavior.
|
||||
|
||||
```bash
|
||||
bun run test # run all tests
|
||||
bun run test:coverage # with coverage report
|
||||
```
|
||||
|
||||
See the `testing` skill (`.claude/skills/testing/SKILL.md`) for complete workflow and quality rules.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Scope & Enforcement
|
||||
|
||||
- **Hard blockers**: process boundary violations, TypeScript errors, failing tests, unsafe IPC usage, missing i18n for new or changed user-facing text, and raw interactive HTML in new UI.
|
||||
- **Current-change requirements**: naming, CSS, file placement, tests, docs, directory size, and single-file-directory rules apply to files created or meaningfully modified by the current change.
|
||||
- **Ratchet rules**: existing directory size or single-file-directory violations do not require cleanup during ordinary feature work or bugfixes, but the current change must not make them worse.
|
||||
- **No scope expansion**: implementation plans and reviews must not create extra tasks, phases, or acceptance criteria for cleanup unless the user asks for that scope.
|
||||
- **Ignored working docs**: `docs/superpowers/` is intentionally gitignored for local Superpowers specs and plans. Do not force-add or otherwise commit files from this directory.
|
||||
|
||||
### During Development
|
||||
|
||||
Auto-fix as you edit:
|
||||
|
||||
```bash
|
||||
bun run lint:fix # auto-fix lint issues (oxlint)
|
||||
bun run format # auto-format all files (oxfmt)
|
||||
bunx tsc --noEmit # verify no type errors
|
||||
```
|
||||
|
||||
If your changes touch `packages/desktop/src/renderer/`, `locales/`, or `packages/desktop/src/common/config/i18n`, also run:
|
||||
|
||||
```bash
|
||||
bun run i18n:types
|
||||
node scripts/check-i18n.js
|
||||
```
|
||||
|
||||
### Before Pushing
|
||||
|
||||
AI agents must not push unless explicitly asked. When pushing, use `just push`, never `git push`:
|
||||
|
||||
```bash
|
||||
just push # lint → format-check → typecheck → test → git push
|
||||
just push -u origin feat/branch # same checks, with extra git push args
|
||||
```
|
||||
|
||||
Any step that fails aborts the push. Fix the issue, commit, then retry.
|
||||
|
||||
> **Note for AI agents**: `just push` uses `--quiet` for lint — only errors cause failure. The project has many pre-existing lint _warnings_ which do NOT indicate failure. Judge success by exit code, not by output volume.
|
||||
|
||||
### Before PR (optional stricter check)
|
||||
|
||||
`prek` replicates the **exact CI pipeline** (includes end-of-file, trailing whitespace checks on all file types):
|
||||
|
||||
```bash
|
||||
# One-time setup
|
||||
npm install -g @j178/prek
|
||||
|
||||
# Run
|
||||
prek run --from-ref origin/main --to-ref HEAD
|
||||
```
|
||||
|
||||
> `prek` is read-only — it reports but does not fix. If it reports issues, run the auto-fix commands above, commit, then re-run.
|
||||
|
||||
### Commit & PR Format
|
||||
|
||||
Commits and PR titles must follow the Conventional Commit format defined in [CONTRIBUTING.md](CONTRIBUTING.md):
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
Allowed types: `feat`, `fix`, `perf`, `refactor`, `docs`, `style`, `chore`, `test`, `ci`, `build`.
|
||||
|
||||
When opening a PR, fill in the PR body using [.github/pull_request_template.md](.github/pull_request_template.md) and complete its checklists honestly (only check items you actually ran or verified).
|
||||
|
||||
**NEVER add AI signatures** (Co-Authored-By, Generated with, etc.).
|
||||
|
||||
## Skills Index
|
||||
|
||||
| Skill | Purpose | Triggers |
|
||||
| ---------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| **architecture** | File & directory structure conventions for all process types | Creating files, adding modules, architectural decisions |
|
||||
| **i18n** | Internationalization workflow and standards | Adding or changing user-facing text, modifying `locales/` or `packages/desktop/src/common/config/i18n` |
|
||||
| **testing** | Testing workflow and quality standards | Writing tests, changing runtime behavior, fixing bugs, or claiming behavior is verified |
|
||||
| **bump-version** | Version bump workflow: update package.json, checks, branch, PR, tag release | Bumping version, `/bump-version` |
|
||||
|
||||
> Skills are located in `.claude/skills/` and contain project conventions that apply to **all** agents and contributors.
|
||||
+947
@@ -0,0 +1,947 @@
|
||||
# Changelog
|
||||
|
||||
## [2.1.33](https://github.com/iOfficeAI/AionUi/compare/v2.1.32...v2.1.33) (2026-07-11)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **build:** align Codex installer verifier (#3561)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.32](https://github.com/iOfficeAI/AionUi/compare/v2.1.31...v2.1.32) (2026-07-10)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **i18n:** update Russian localization (#3541)
|
||||
|
||||
#### Features
|
||||
|
||||
- **i18n:** add French locale (#2731)
|
||||
- **guid:** move mobile home input controls into a + action sheet (#3554)
|
||||
- **team:** add manual teammate management (#3532)
|
||||
- **conversation:** rework model selector into a two-level menu (#3550)
|
||||
- **conversation:** rework message queue into a send draft box (#3547)
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **conversation:** fold draft box help into the mode toggle (#3553)
|
||||
|
||||
### Core ([v0.1.45](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.45))
|
||||
|
||||
#### Features
|
||||
|
||||
- **ai-agent:** adapt to aionrs v0.2.2 config changes
|
||||
- **cli:** add agent-facing config and diagnose commands (#595)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **ai-agent:** cap provider health check tokens
|
||||
- **ai-agent:** set default aionrs thinking cli args
|
||||
- **model_fetcher:** extract first key from multi-line api_key for HTTP requests (#593)
|
||||
- **runtime:** update Claude ACP package (#599)
|
||||
- **runtime:** update managed Codex ACP package (#598)
|
||||
- stop defaulting aionrs max tokens
|
||||
|
||||
---
|
||||
|
||||
## [2.1.31](https://github.com/iOfficeAI/AionUi/compare/v2.1.30...v2.1.31) (2026-07-08)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **installer:** harden Windows failure reporting and self-lock handling (#3533)
|
||||
- prepare backend startup directories (#3536)
|
||||
- **settings:** avoid Arco tooltip crash in skills page (#3535)
|
||||
|
||||
#### Features
|
||||
|
||||
- **feedback:** attach core diagnostics to reports (#3529)
|
||||
- **settings:** assistant editor and settings UI polish (#3528)
|
||||
|
||||
### Core ([v0.1.44](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.44))
|
||||
|
||||
#### Features
|
||||
|
||||
- **agent:** use aionrs runtime env API (#586)
|
||||
- **ai-agent:** surface upstream 429 body in AgentSendError detail (#591)
|
||||
- **system:** add feedback diagnostics report (#585)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** preserve ACP error cause detail (#581)
|
||||
- **skills:** correct aionui-config butler skill drift (2026-07) (#584)
|
||||
- use provider and model protocol to determine llm request
|
||||
|
||||
---
|
||||
|
||||
## [2.1.30](https://github.com/iOfficeAI/AionUi/compare/v2.1.29...v2.1.30) (2026-07-06)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- wrong OpenAI SDK param name, throttle cleanup leak, missing alt text (#3512)
|
||||
- **installer:** harden Windows NSIS update failure handling (#3523)
|
||||
|
||||
#### Features
|
||||
|
||||
- **guid:** add slash command menu (#3524)
|
||||
- **assistant:** add thought level defaults to assistant UI (#3522)
|
||||
- **settings:** add inline link to model config when no image model is available
|
||||
- **settings:** default to the Agents tab when opening settings
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **settings:** describe skill origins per tab instead of per-card badges
|
||||
- **settings:** split skills/tools entries and unify page header paradigm
|
||||
|
||||
#### Styling
|
||||
|
||||
- **settings:** match agent availability filter to assistant home tabs
|
||||
|
||||
### Core ([v0.1.43](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.43))
|
||||
|
||||
#### Features
|
||||
|
||||
- **assistant:** persist thought-level defaults (#574)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** project available commands in management rows (#579)
|
||||
- **assistant:** filter generated assistants by installed agents (#578)
|
||||
- **cron:** enforce full-auto mode for scheduled tasks (#576)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.29](https://github.com/iOfficeAI/AionUi/compare/v2.1.28...v2.1.29) (2026-07-03)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **assistant:** use management catalog for editor engines (#3511)
|
||||
- **cron:** improve scheduled task conversation history (#3510)
|
||||
- show unchecked agents and rotate frontend logs by message date (#3507)
|
||||
- **assistant:** return to My Assistants after duplicating/creating
|
||||
|
||||
#### Features
|
||||
|
||||
- **assistant:** promote assistants to a top-level sidebar entry
|
||||
- **assistant:** unify selection-list ordering, keep CLI agents on top
|
||||
- **assistant:** rebuild management page into My / Official tabs
|
||||
- **assistant:** reword official read-only banner and make copy link inline
|
||||
- **assistant:** custom-empty state, return-to-official on save, field polish
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **layout:** move conversation search into the titlebar toolbar
|
||||
|
||||
#### Styling
|
||||
|
||||
- **assistant:** apply oxfmt formatting to assistant home components
|
||||
|
||||
### Core ([v0.1.42](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.42))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** align unchecked availability with team runtime selection (#571)
|
||||
- **agent:** avoid full availability refresh on reads (#566)
|
||||
- **cron:** preserve existing conversation jobs across lifecycle changes (#572)
|
||||
- **mcp:** support aionrs config path subcommand with legacy fallback (#568)
|
||||
- preserve ACP config catalogs on resume (#570)
|
||||
- preserve Linux GLIBC baselines (#573)
|
||||
|
||||
#### Features
|
||||
|
||||
- **assistant:** 官方助手默认关闭 + 固定顺序 + 一次性重置迁移 (#567)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.28](https://github.com/iOfficeAI/AionUi/compare/v2.1.27...v2.1.28) (2026-07-02)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **i18n:** resolve main locale gaps (#3503)
|
||||
- **startup:** confirm corrupted database rebuild (#3502)
|
||||
- **team:** pass capabilities to team chat send box (#3501)
|
||||
- **runtime:** coordinate foreground leases and runtime ensure (#3497)
|
||||
- **cron:** lock team cron task editing (#3496)
|
||||
- **desktop:** support dated frontend log layout (#3495)
|
||||
- **assistant:** render empty avatars consistently (#3493)
|
||||
- **cron:** support team context job navigation (#3492)
|
||||
- **acp:** dedupe runtime option requests (#3490)
|
||||
- **assistant:** correct engine section badge tone to warning
|
||||
- **cron:** sync manual task assistant selection (#3485)
|
||||
- **desktop:** wait for macOS update install readiness (#3484)
|
||||
|
||||
#### Features
|
||||
|
||||
- **i18n:** add Persian (fa-IR) locale support (#3284)
|
||||
- **i18n:** add complete Spanish (es-ES) translation (#3402)
|
||||
- **conversation:** keep batch-selection panel pinned while scrolling
|
||||
- **conversation:** keep project folder header sticky while scrolling
|
||||
- **conversation:** reveal active conversation by expanding its section and folder
|
||||
- **conversation:** surface session skills in slash command menu
|
||||
|
||||
### Core ([v0.1.41](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.41))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **assistant:** normalize avatar storage and identity (#558)
|
||||
- **conversation:** derive assistant runtime type from metadata (#555)
|
||||
- **conversation:** partition temp workspaces and logs by date (#560)
|
||||
- **cron:** apply custom assistant rules in scheduled runs (#495)
|
||||
- **cron:** lock team cron execution mode (#562)
|
||||
- **cron:** route skill scheduling through helper (#553)
|
||||
- **database:** require explicit corrupted database recovery (#563)
|
||||
- resolve ACP backends from metadata (#559)
|
||||
- **runtime:** harden managed Node command resolution (#565)
|
||||
- **runtime:** protect active ACP tasks from idle cleanup (#561)
|
||||
- **skill:** raise import size limits (#564)
|
||||
- **skills:** correct AionUi Butler skill drift against current backend (#557)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.27](https://github.com/iOfficeAI/AionUi/compare/v2.1.26...v2.1.27) (2026-06-30)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **team:** reconcile stale run state (#3480)
|
||||
- **cron:** preserve scheduled task conversations (#3479)
|
||||
- **cron:** restore scheduled conversations to history (#3478)
|
||||
- **mcp:** isolate backend cwd for stdio tools (#3476)
|
||||
- **agent:** show ACP model descriptions (#3463)
|
||||
|
||||
### Core ([v0.1.40](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.40))
|
||||
|
||||
#### Features
|
||||
|
||||
- **team:** add run state snapshot endpoint (#549)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** preserve selectors for partial config snapshots (#548)
|
||||
- **cron:** restore create command heading (#547)
|
||||
- **cron:** run jobs through conversation service (#546)
|
||||
- **skills:** repair butler endpoint drift + add cron scheduling (#550)
|
||||
- **windows:** handle runtime process lifecycle
|
||||
|
||||
---
|
||||
|
||||
## [2.1.26](https://github.com/iOfficeAI/AionUi/compare/v2.1.25...v2.1.26) (2026-06-29)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** tighten repair save and test flow (#3470)
|
||||
- **guid:** remember last selected assistant (#3468)
|
||||
- **assistant:** prefer runtime config options for defaults (#3466)
|
||||
- **conversation:** restore team chat full width (#3464)
|
||||
- **fs:** pass workspace roots to local fs routes (#3451)
|
||||
|
||||
#### Styling
|
||||
|
||||
- **settings:** clean up assistant card more-button
|
||||
|
||||
### Core ([v0.1.39](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.39))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** adapt aionrs compat API (#528)
|
||||
- **agent:** guard internal Aion CLI command overrides (#538)
|
||||
- **app:** reuse conversation service for channel messages (#531)
|
||||
- **assistant:** preserve builtin override selections (#535)
|
||||
- **file:** trust local workspace roots for fs routes (#527)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.25](https://github.com/iOfficeAI/AionUi/compare/v2.1.24...v2.1.25) (2026-06-26)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **assistant:** add TalkToButler entry-point infrastructure
|
||||
- **cron:** add create-via-chat path to scheduled tasks page
|
||||
- **cron:** use TalkToButlerButton for create + align button styles
|
||||
- **feedback:** add "solve via chat" to bug report
|
||||
- **settings:** wire "via chat" into create/add flows
|
||||
- **web-host:** remove single-chat team upgrade path (#3441)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **avatar:** prevent local avatar path rendering (#3439)
|
||||
- **conversation:** make chat width fluid (#3436)
|
||||
- **cron:** consume create-via-chat prefill only once per navigation
|
||||
- **desktop:** classify agent metadata cache repair failures (#3450)
|
||||
- **guid:** improve dark-mode contrast for inactive agent selector labels (#3430)
|
||||
- **guid:** load runtime catalog from agent metadata (#3440)
|
||||
- **guid:** remove static codex runtime catalog (#3443)
|
||||
- **guid:** resolve assistant skill defaults from config (#3445)
|
||||
- **guid:** stop showing stale Codex model fallback (#3432)
|
||||
- **installer:** verify bundled resources (#3444)
|
||||
- **linux:** align desktop icon name (#3449)
|
||||
- **settings:** clarify custom agent acp requirement (#3448)
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **cron:** hide conversation header entry when no scheduled task exists
|
||||
|
||||
### Core ([v0.1.38](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.38))
|
||||
|
||||
#### Features
|
||||
|
||||
- remove single-chat team upgrade path (#524)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** expose runtime catalogs from metadata (#523)
|
||||
- **assistant:** expose auto-inject skills and preserve assistant rules (#525)
|
||||
- repair invalid UTF-8 agent metadata cache fields (#526)
|
||||
- **skills:** sync AionUi Butler skills + rule with current backend (#520)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.24](https://github.com/iOfficeAI/AionUi/compare/v2.1.23...v2.1.24) (2026-06-25)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **agent:** connection testing and assistant availability surfacing (phase 2) (#3395)
|
||||
- **conversation:** add cursor message pagination (#3422)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **conversation:** localize structured agent errors (#3426)
|
||||
- **desktop:** repair legacy database handoff startup (#3423)
|
||||
- **release:** restore mac zip artifacts (#3415)
|
||||
- **settings:** prevent capabilities tab flicker (#3414)
|
||||
|
||||
### Core ([v0.1.37](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.37))
|
||||
|
||||
#### Features
|
||||
|
||||
- **agent:** detect availability via session/new probe and assistant-first identity (#500)
|
||||
- **conversation:** add cursor pagination for messages (#515)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** classify ACP and provider errors (#518)
|
||||
- **aionrs:** adapt runtime guard config (#510)
|
||||
- **conversation:** recover dead ACP turns after agent process loss (#514)
|
||||
- **db:** repair legacy handoff schema drift (#516)
|
||||
- validate skill frontmatter as yaml (#512)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.23](https://github.com/iOfficeAI/AionUi/compare/v2.1.22...v2.1.23) (2026-06-23)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **webui:** add browser notifications for permission requests and turn completion (#3401)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **preview:** correct OfficeCLI repo slug casing and de-DE install hint (#3399)
|
||||
|
||||
### Core ([v0.1.36](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.36))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **deps:** update quinn-proto for RustSec advisory (#508)
|
||||
- load skills in custom workspaces (#506)
|
||||
- **agent:** support aionrs 0.1.31 (#503)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.22](https://github.com/iOfficeAI/AionUi/compare/v2.1.21...v2.1.22) (2026-06-22)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **acp:** preserve redacted raw error in AIONUI_INTERNAL_ERROR fallback (#3393)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **markdown:** support local file hash line links (#3396)
|
||||
- **conversation:** localize OpenClaw Gateway startup error (#3392)
|
||||
- **mcp:** guard message calls against use-after-unmount crash (#3376)
|
||||
- **preview:** improve file diffs and local file links (#3379)
|
||||
- **installer:** harden win arm64 install (#3387)
|
||||
|
||||
### Core ([v0.1.34](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.34))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** expose aionrs mode config option (#501)
|
||||
- **agent:** surface OpenClaw Gateway unreachable errors (#498)
|
||||
- **aionrs:** classify engine errors structurally (#494)
|
||||
- **aionrs:** drop malformed tool-call events (#486)
|
||||
- **channel:** reuse stored credentials when re-enabling a plugin (#458)
|
||||
|
||||
---
|
||||
|
||||
## [2.1.21](https://github.com/iOfficeAI/AionUi/compare/v2.1.20...v2.1.21) (2026-06-18)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **i18n:** add German (de-DE) locale (#3370)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **preview:** restore local html and selected file reopen (#3369)
|
||||
- **preview:** build valid file:// URL for PDF preview on Windows (#3366)
|
||||
- **i18n:** wire pt-BR into language pickers and main-process loader (#3361)
|
||||
|
||||
### Core ([v0.1.32](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.32))
|
||||
|
||||
#### Features
|
||||
|
||||
- **team:** centralize team MCP prompt governance ([#490](https://github.com/iOfficeAI/AionCore/issues/490))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** recover dead ACP connections ([#487](https://github.com/iOfficeAI/AionCore/issues/487))
|
||||
- **conversation:** upsert streaming tool calls (AIO-30) ([#484](https://github.com/iOfficeAI/AionCore/issues/484))
|
||||
|
||||
#### Documentation
|
||||
|
||||
- **skills:** add cross-platform notes so Windows users translate shell examples ([#489](https://github.com/iOfficeAI/AionCore/issues/489))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.20](https://github.com/iOfficeAI/AionUi/compare/v2.1.19...v2.1.20) (2026-06-17)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **agent:** combine header model thinking selector (#3358)
|
||||
- **update:** add singleton update notification (#3351)
|
||||
- **team:** handle queued team runtime metadata (#3349)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **team:** wait for solo turn before handoff queue drain (#3353)
|
||||
- **assistant:** remove leftover gap above assistant list (#3344)
|
||||
|
||||
### Core ([v0.1.31](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.31))
|
||||
|
||||
#### Features
|
||||
|
||||
- **assistant:** add built-in AionUi self-management assistant ([#474](https://github.com/iOfficeAI/AionCore/issues/474))
|
||||
- **assistant:** expand AionUi assistant into a butler with remote-access ([#481](https://github.com/iOfficeAI/AionCore/issues/481))
|
||||
- enforce TeamRun ownership for agent turns ([#483](https://github.com/iOfficeAI/AionCore/issues/483))
|
||||
- **team:** support queued team_send_message semantics ([#479](https://github.com/iOfficeAI/AionCore/issues/479))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** persist runtime model and mode into assistant preferences ([#482](https://github.com/iOfficeAI/AionCore/issues/482))
|
||||
- harden ACP image path handling ([#477](https://github.com/iOfficeAI/AionCore/issues/477))
|
||||
- **team:** retry handoff turns after runtime release ([#480](https://github.com/iOfficeAI/AionCore/issues/480))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.19](https://github.com/iOfficeAI/AionUi/compare/v2.1.18...v2.1.19) (2026-06-15)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **team:** support slot-scoped stop controls (#3334)
|
||||
- **desktop:** report installation integrity diagnostics (#3333)
|
||||
- **update:** use CDN metadata for stable auto updates (#3244)
|
||||
- **acp:** add observed config option selectors (#3324)
|
||||
- **layout:** make sider wordmark a back-to-chat control in settings (#3320)
|
||||
- **preview:** actionable server-side install guidance for officecli errors in web mode (#3310)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- align team workspace display fallback (#3340)
|
||||
- **team:** prefer assistant avatars in team chats (#3338)
|
||||
- repair assistant cron and guid metadata flows (#3336)
|
||||
- **assistant:** remove star office ui remnants (#3329)
|
||||
- **startup:** hydrate windows path for cli detection (#3308)
|
||||
- **docker:** install libicu so officecli preview works on Linux server deployments (#3323)
|
||||
- **agents:** keep disabled custom agents visible in settings (#3319)
|
||||
- **stt:** keep recording when streaming fails before it establishes (#3317)
|
||||
|
||||
### Core ([v0.1.30](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.30))
|
||||
|
||||
#### Features
|
||||
|
||||
- **acp:** use observed config options for preferences ([#468](https://github.com/iOfficeAI/AionCore/issues/468))
|
||||
- align team shared workspace resolution ([#475](https://github.com/iOfficeAI/AionCore/issues/475))
|
||||
- **team:** support slot-scoped team pause and wake flow ([#472](https://github.com/iOfficeAI/AionCore/issues/472))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** send non-empty clientInfo in ACP initialize handshake ([#471](https://github.com/iOfficeAI/AionCore/issues/471))
|
||||
- **agent:** wait for task shutdown during clear ([#446](https://github.com/iOfficeAI/AionCore/issues/446))
|
||||
- **assistant:** remove star office helper remnants ([#470](https://github.com/iOfficeAI/AionCore/issues/470))
|
||||
- **office:** fetch officecli installer from official mirror before GitHub ([#463](https://github.com/iOfficeAI/AionCore/issues/463))
|
||||
- preserve assistant snapshot and skill wiring for cron ([#473](https://github.com/iOfficeAI/AionCore/issues/473))
|
||||
- **shell:** reveal file via FileManager1 D-Bus on Linux ([#466](https://github.com/iOfficeAI/AionCore/issues/466))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.18](https://github.com/iOfficeAI/AionUi/compare/v2.1.17...v2.1.18) (2026-06-12)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **stt:** streaming voice input with live transcript (#3291)
|
||||
- **assistant:** deliver phase-1 governance settings (#3277)
|
||||
- stabilize team mode conversation runtime (#3309)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **updater:** wait for backend shutdown before install (#3270)
|
||||
- **windows-installer:** recover from long-path uninstall failures (#3296)
|
||||
- **macos:** add audio-input entitlement so microphone works (#3294)
|
||||
- **preview:** drop bare trailing slash from office watch proxy url (#3287)
|
||||
- **workspace:** float directory picker above team/cron create modals
|
||||
- **workspace:** enable clickable folder picker in webui
|
||||
|
||||
#### Styling
|
||||
|
||||
- **titlebar:** nudge feedback icon up to align with neighbors
|
||||
- **markdown:** tighten desktop paragraph spacing
|
||||
- **markdown:** tighten desktop chat body line-height
|
||||
- **conversation:** show AI copy/timestamp row only at turn end
|
||||
- **display:** tighten factory default font sizes and zoom
|
||||
|
||||
### Core ([v0.1.29](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.29))
|
||||
|
||||
#### Features
|
||||
|
||||
- converge team mode runtime architecture ([#464](https://github.com/iOfficeAI/AionCore/issues/464))
|
||||
- **stt:** streaming transcription proxy over websocket ([#455](https://github.com/iOfficeAI/AionCore/issues/455))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** validate managed ACP platform binaries ([#462](https://github.com/iOfficeAI/AionCore/issues/462))
|
||||
- **cron:** retry busy jobs from runtime state ([#459](https://github.com/iOfficeAI/AionCore/issues/459))
|
||||
- isolate ACP cancel turn completion ([#461](https://github.com/iOfficeAI/AionCore/issues/461))
|
||||
- **office:** probe star-office preferred_url host as given ([#456](https://github.com/iOfficeAI/AionCore/issues/456))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **assistant:** finalize unified governance storage ([#449](https://github.com/iOfficeAI/AionCore/issues/449))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.17](https://github.com/iOfficeAI/AionUi/compare/v2.1.16...v2.1.17) (2026-06-11)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **settings:** voice input settings revamp and home page mic button (#3283)
|
||||
- **titlebar:** add global feedback/report entry to toolbar
|
||||
- **theme:** add Follow System theme mode to gallery (#3282)
|
||||
- **settings:** support multi-select models when adding a model platform
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **webui:** normalize Windows verbatim paths from directory picker (#3286)
|
||||
- **model-selector:** keep sticky platform title above scrolling items
|
||||
- **settings:** allow editing Base URL when editing a model platform
|
||||
- **stt:** send multipart request matching backend /api/stt contract (#3274)
|
||||
|
||||
#### Styling
|
||||
|
||||
- **model-selector:** sticky platform group titles in scrollable dropdown
|
||||
|
||||
### Core ([v0.1.28](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.28))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **auth:** allow same-origin framing on office preview proxy routes ([#454](https://github.com/iOfficeAI/AionCore/issues/454))
|
||||
- **file:** strip Windows verbatim prefix from /api/fs/browse paths ([#453](https://github.com/iOfficeAI/AionCore/issues/453))
|
||||
- **stt:** STT compatibility fixes for Groq Whisper and AionUI web frontend ([#400](https://github.com/iOfficeAI/AionCore/issues/400))
|
||||
- **stt:** treat blank base_url as unset and log malformed config ([#448](https://github.com/iOfficeAI/AionCore/issues/448))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.16](https://github.com/iOfficeAI/AionUi/compare/v2.1.15...v2.1.16) (2026-06-10)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **preview:** point OfficeCLI install help to official releases (#3264)
|
||||
- **http:** read error response body once to avoid double consumption (#3262)
|
||||
- **ci:** handle empty release prefix check (#3263)
|
||||
|
||||
### Core ([v0.1.27](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.27))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **ai-agent:** auto approve team mcp permissions ([#447](https://github.com/iOfficeAI/AionCore/issues/447))
|
||||
- **ai-agent:** trim stderr buffer at UTF-8 char boundary ([#443](https://github.com/iOfficeAI/AionCore/issues/443))
|
||||
- **office:** resolve officecli shim from node_modules/.bin after npm prefix install ([#440](https://github.com/iOfficeAI/AionCore/issues/440))
|
||||
- **office:** restore OfficeCLI installer resolution ([#444](https://github.com/iOfficeAI/AionCore/issues/444))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.15](https://github.com/iOfficeAI/AionUi/compare/v2.1.14...v2.1.15) (2026-06-09)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- enforce agent runtime policy and turn-aware UI state (#3253)
|
||||
- render localized ACP empty-turn info tips (#3251)
|
||||
- **conversation:** hide all conversation export UI entries
|
||||
- make log directory configurable (#3233)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **conversation:** align header model label with selector (#3257)
|
||||
- **sendbox:** stop button glow clipped by mobile panel corner
|
||||
- **login:** move mobile language selector to its own row to avoid logo overlap
|
||||
- **desktop:** pass parent pid to bundled backend (#3250)
|
||||
|
||||
### Core ([v0.1.26](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.26))
|
||||
|
||||
#### Features
|
||||
|
||||
- enforce agent runtime policy and turn-aware state ([#436](https://github.com/iOfficeAI/AionCore/issues/436))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **app:** use process synchronize access for parent watcher ([#438](https://github.com/iOfficeAI/AionCore/issues/438))
|
||||
- **acp:** preserve confirmed model selection ([#437](https://github.com/iOfficeAI/AionCore/issues/437))
|
||||
- **app:** stop backend when desktop exits ([#433](https://github.com/iOfficeAI/AionCore/issues/433))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.14](https://github.com/iOfficeAI/AionUi/compare/v2.1.13...v2.1.14) (2026-06-08)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **bootstrap:** block wrong macOS package architecture at startup (#3232)
|
||||
|
||||
### Core ([v0.1.24](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.24))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** prefer config options catalogs ([#425](https://github.com/iOfficeAI/AionCore/issues/425))
|
||||
- expose managed resource preparation failure details ([#430](https://github.com/iOfficeAI/AionCore/issues/430))
|
||||
- handle Hermes yolo fallback correctly ([#428](https://github.com/iOfficeAI/AionCore/issues/428))
|
||||
- harden managed ACP bundle preparation and builtin CLI availability ([#426](https://github.com/iOfficeAI/AionCore/issues/426))
|
||||
- scope bundled ACP output under tool directories ([#431](https://github.com/iOfficeAI/AionCore/issues/431))
|
||||
- **shell:** support UNC paths in Windows terminal ([#411](https://github.com/iOfficeAI/AionCore/issues/411))
|
||||
- validate managed ACP packages via real entrypoints ([#429](https://github.com/iOfficeAI/AionCore/issues/429))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **app:** organize CLI command boundaries ([#423](https://github.com/iOfficeAI/AionCore/issues/423))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.13](https://github.com/iOfficeAI/AionUi/compare/v2.1.12...v2.1.13) (2026-06-07)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **appearance:** configurable font sizes & display→appearance rename (#3223)
|
||||
- **theme:** unify theme system into a single Theme concept (#3219)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **messages:** keep message list scrollbar flush to window edge (#3226)
|
||||
- **preview:** default zoom to 100% and hide snapshot/history entry (#3222)
|
||||
- **bootstrap:** preserve backend startup error codes (#3218)
|
||||
- **runtime:** validate packaged node runtime layout (#3221)
|
||||
- **runtime:** align installation integrity dialogs (#3220)
|
||||
- **realtime:** canonicalize boundary errors (#3217)
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- stabilize conversation runtime view contract (#3224)
|
||||
|
||||
### Core ([v0.1.23](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.23))
|
||||
|
||||
#### Features
|
||||
|
||||
- **cli:** canonicalize CLI and bootstrap boundary errors ([#417](https://github.com/iOfficeAI/AionCore/issues/417))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **error:** canonicalize boundary errors ([#415](https://github.com/iOfficeAI/AionCore/issues/415))
|
||||
- **runtime:** report bundled resource installation failures ([#420](https://github.com/iOfficeAI/AionCore/issues/420))
|
||||
- **team:** inherit workspace for spawned agents ([#413](https://github.com/iOfficeAI/AionCore/issues/413))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- centralize agent runtime session context building ([#419](https://github.com/iOfficeAI/AionCore/issues/419))
|
||||
- centralize runtime turn lifecycle ([#421](https://github.com/iOfficeAI/AionCore/issues/421))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.12](https://github.com/iOfficeAI/AionUi/compare/v2.1.11...v2.1.12) (2026-06-05)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **i18n:** add Brazilian Portuguese (pt-BR) translation (#3209)
|
||||
- **preview:** native Streamdown markdown rendering + full theming (#3204)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **conversation:** align workspace path availability handling (#3207)
|
||||
- **preview:** dedupe @codemirror/language so markdown source highlight survives (#3206)
|
||||
|
||||
### Core ([v0.1.22](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.22))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** stabilize mode and model source of truth ([#409](https://github.com/iOfficeAI/AionCore/issues/409))
|
||||
- **conversation:** align workspace path availability handling ([#410](https://github.com/iOfficeAI/AionCore/issues/410))
|
||||
- **file:** lazy load browse roots ([#406](https://github.com/iOfficeAI/AionCore/issues/406))
|
||||
- prepare managed acp tools locally without cdn ([#408](https://github.com/iOfficeAI/AionCore/issues/408))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **error:** finish ApiError phase3 ([#398](https://github.com/iOfficeAI/AionCore/issues/398))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.11](https://github.com/iOfficeAI/AionUi/compare/v2.1.10...v2.1.11) (2026-06-04)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **preview:** unify code viewing & editing on CodeMirror 6 (#3194)
|
||||
- **preview:** unify code view font and fix view-mode/line-height regressions (#3185)
|
||||
- **workspace:** VSCode-style file tree icons + smoother preview browsing (#3181)
|
||||
- add managed acp artifact mirror workflow (#3182)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **web-host:** use aioncore reported backend port (#3193)
|
||||
- **settings:** apply UI scale only on slider release (#3190)
|
||||
|
||||
### Core ([v0.1.20](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.20))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **app:** bind backend before startup services ([#397](https://github.com/iOfficeAI/AionCore/issues/397))
|
||||
- stabilize agent runtime terminal lifecycle ([#396](https://github.com/iOfficeAI/AionCore/pull/396))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **error:** ACP error classification ([#393](https://github.com/iOfficeAI/AionCore/issues/393))
|
||||
- **error:** migrate phase2 service errors ([#395](https://github.com/iOfficeAI/AionCore/issues/395))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.10](https://github.com/iOfficeAI/AionUi/compare/v2.1.9...v2.1.10) (2026-06-02)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **runtime:** show runtime-specific MCP missing command hints (#3167)
|
||||
- **startup:** add health polling diagnostics (#3168)
|
||||
- **acp:** show model switch feedback
|
||||
- **acp:** avoid duplicate runtime sync requests
|
||||
- **acp:** wait for warmup before runtime sync
|
||||
- **sentry:** split incomplete install diagnostics (#3164)
|
||||
- normalize workspace path error handling (#3158)
|
||||
- **acp:** fix model state sync after session recovery (#3162)
|
||||
- **desktop:** persist close-to-tray setting (#3150)
|
||||
|
||||
### Core ([v0.1.19](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.19))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **aionui-ai-agent:** classify aionrs API connection errors ([#389](https://github.com/iOfficeAI/AionCore/issues/389))
|
||||
- classify missing MCP launcher runtimes ([#387](https://github.com/iOfficeAI/AionCore/issues/387))
|
||||
- enforce workspace path whitespace errors across create and runtime ([#381](https://github.com/iOfficeAI/AionCore/issues/381))
|
||||
- **startup:** add startup phase diagnostics ([#388](https://github.com/iOfficeAI/AionCore/issues/388))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.9](https://github.com/iOfficeAI/AionUi/compare/v2.1.8...v2.1.9) (2026-06-01)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **web-host:** skip fetch-blocked backend ports (#3146)
|
||||
- **i18n:** clarify incomplete installation recovery (#3145)
|
||||
- **conversation:** map 409 already-processing to CONVERSATION_BUSY (#3142)
|
||||
- **i18n:** localize MCP check strings (#3141)
|
||||
|
||||
#### Features
|
||||
|
||||
- Allow importing skill folders and zip archives (#3144)
|
||||
|
||||
### Core ([v0.1.18](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.18))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** classify Bedrock 'model identifier is invalid' as model-not-found (AIO-12) ([#377](https://github.com/iOfficeAI/AionCore/issues/377))
|
||||
- **agent:** preserve process-group cleanup after leader exit ([#369](https://github.com/iOfficeAI/AionCore/issues/369))
|
||||
- **agent:** tighten send_error classifier (AIO-87, AIO-89, AIO-90) ([#375](https://github.com/iOfficeAI/AionCore/issues/375))
|
||||
- **aionui-ai-agent:** strip HTML body from sanitized error detail (AIO-13) ([#380](https://github.com/iOfficeAI/AionCore/issues/380))
|
||||
- recover deleted conversation workspaces ([#379](https://github.com/iOfficeAI/AionCore/issues/379))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.8](https://github.com/iOfficeAI/AionUi/compare/v2.1.7...v2.1.8) (2026-05-30)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **desktop:** improve incomplete backend install diagnostics (#3121)
|
||||
- **web-host:** enrich backend health timeout diagnostics (#3120)
|
||||
- **feedback:** preserve structured live error tips (#3116)
|
||||
|
||||
### Core ([v0.1.17](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.17))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** make codex sandbox sync non-fatal ([#370](https://github.com/iOfficeAI/AionCore/issues/370))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.7](https://github.com/iOfficeAI/AionUi/compare/v2.1.6...v2.1.7) (2026-05-29)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **mcp:** move MCP management to conversation scope (#3109)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **feedback:** tag agent error reports (#3113)
|
||||
- **conversation:** render structured agent errors (#3093)
|
||||
- **web-host:** reuse backend port after crash restart (#3111)
|
||||
- **webui:** auto-open local url on startup (#3110)
|
||||
- **startup:** ignore cancelled backend startup (#3108)
|
||||
- **mcp:** validate json imports (#3106)
|
||||
- **team:** avoid sidebar confirmation fan-out (#3105)
|
||||
- **web-host:** add health timeout diagnostics (#3102)
|
||||
- **settings:** avoid blue switch during image generation loading (#3091)
|
||||
|
||||
### Core ([v0.1.16](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.16))
|
||||
|
||||
#### Features
|
||||
|
||||
- **agent:** classify structured agent send errors ([#356](https://github.com/iOfficeAI/AionCore/issues/356))
|
||||
- **mcp:** support session scoped MCP injection ([#363](https://github.com/iOfficeAI/AionCore/issues/363))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- channel reply stream cold start ([#366](https://github.com/iOfficeAI/AionCore/issues/366))
|
||||
- **mcp:** clean up stdio test process trees ([#368](https://github.com/iOfficeAI/AionCore/issues/368))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.6](https://github.com/iOfficeAI/AionUi/compare/v2.1.5...v2.1.6) (2026-05-28)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **model-selector:** trust backend current model and persist preferences (#3084)
|
||||
- **build:** align bundled aioncore target arch (#3092)
|
||||
- **settings:** use provider health check probe (#3090)
|
||||
- **settings:** use health check error message (#3080)
|
||||
- **backend:** handle incomplete bundled aioncore installs (#3078)
|
||||
|
||||
#### Performance
|
||||
|
||||
- lazy-load full tool message content (#3086)
|
||||
- improve message startup latency (#3082)
|
||||
|
||||
### Core ([v0.1.15](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.15))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **agent:** add provider health check probe ([#358](https://github.com/iOfficeAI/AionCore/issues/358))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.5](https://github.com/iOfficeAI/AionUi/compare/v2.1.4...v2.1.5) (2026-05-27)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Features
|
||||
|
||||
- **settings:** use backend MCP settings source (#3069)
|
||||
- **settings:** rename capabilities tab + collapse speech/image-gen when disabled
|
||||
- **settings:** clarify builtin assistant readonly state in editor
|
||||
- **update:** add install warning on downloaded state in UpdateModal
|
||||
- **tools:** allowlist image-gen models and document supported set
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **acp:** surface raw send errors (#3067)
|
||||
- **guid:** use startsWith('custom:') to detect preset agent on New Chat reset
|
||||
- **guid:** preserve CLI agent selection on New Chat, only reset preset agents
|
||||
- **guid:** restore last selected agent on initial render without flash
|
||||
- **guid:** include user skills in action-row Skills count
|
||||
- **update:** polish downloaded state — remove desc text, drop icon from warning
|
||||
- **startup:** show incompatible backend runtime (#3062)
|
||||
- **image-gen:** strip response_format from gpt-image requests + remove double-save
|
||||
- **tools:** use Form.Item tooltip prop for image model help icon
|
||||
- **tools:** align help icon vertically with image model label
|
||||
- **sendbox:** map workspace file paths for mentions (#3060)
|
||||
- **settings:** route provider health check via aionrs (#3058)
|
||||
- **settings:** localize sentence terminator on builtin readonly banner
|
||||
- **electron:** tolerate pending backend startup (#3057)
|
||||
- recover pending permission prompts (#3059)
|
||||
- preserve timezone for scheduled tasks (#3056)
|
||||
|
||||
### Core ([v0.1.14](https://github.com/iOfficeAI/AionCore/releases/tag/v0.1.14))
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- preserve cron timezone on legacy schedule updates ([#344](https://github.com/iOfficeAI/AionCore/issues/344))
|
||||
- **startup:** add backend readiness diagnostics ([#346](https://github.com/iOfficeAI/AionCore/issues/346))
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- four-layer architecture (connect / conv / biz) ([#349](https://github.com/iOfficeAI/AionCore/issues/349))
|
||||
|
||||
---
|
||||
|
||||
## [2.1.4](https://github.com/iOfficeAI/AionUi/compare/v2.1.3...v2.1.4) (2026-05-27)
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- **messages:** ignore non-renderable stream events (#3053)
|
||||
- **messages:** stabilize stream scrolling and initial loading (#3042)
|
||||
|
||||
---
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# Contributing Guide
|
||||
|
||||
> **Chinese version**: [CONTRIBUTING.zh.md](CONTRIBUTING.zh.md)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
See [docs/contributing/development.md](docs/contributing/development.md) for environment setup. You will need:
|
||||
|
||||
- Node.js 22+
|
||||
- [bun](https://bun.sh)
|
||||
- [Rust stable + Cargo](https://rustup.rs) for the local AionCore backend
|
||||
- [prek](https://github.com/j178/prek) (`npm install -g @j178/prek`)
|
||||
|
||||
## Rule 1: Atomic PRs
|
||||
|
||||
Each pull request must contain **exactly one feature or one bug fix** that cannot be further decomposed.
|
||||
|
||||
**How to check:** Ask yourself (or an AI): _"Can this diff be split into multiple independently mergeable PRs?"_ If yes, split it before submitting.
|
||||
|
||||
### Examples
|
||||
|
||||
**Acceptable (single PR):**
|
||||
|
||||
- A bug fix with one root cause, even if it touches multiple files (e.g., fixing toast z-index across modal and chat layers)
|
||||
- A single coherent feature (e.g., team creation modal with form validation)
|
||||
|
||||
**Must be split into separate PRs:**
|
||||
|
||||
- Team chat scroll fix + Sentry user tracking + office preview performance optimization = 3 PRs
|
||||
- Unrelated bug fixes bundled together (e.g., titlebar navigation fix + i18n missing key + speech input UI fix)
|
||||
- Independent technical layers (e.g., IPC bridge refactor + renderer component + worker process change for unrelated features)
|
||||
|
||||
## Rule 2: Commit and PR Title Format
|
||||
|
||||
Commit messages and PR titles must use Conventional Commit format in English:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
Use one of these types:
|
||||
|
||||
| Type | Meaning | Changelog visibility |
|
||||
| ---------- | ------------------------ | -------------------- |
|
||||
| `feat` | New user-facing behavior | Visible |
|
||||
| `fix` | Bug fix | Visible |
|
||||
| `perf` | Performance improvement | Visible |
|
||||
| `refactor` | Code restructuring | Visible |
|
||||
| `docs` | Documentation | Visible |
|
||||
| `style` | Formatting or styles | Hidden |
|
||||
| `chore` | Maintenance work | Hidden |
|
||||
| `test` | Tests | Hidden |
|
||||
| `ci` | CI configuration | Hidden |
|
||||
| `build` | Build system | Hidden |
|
||||
|
||||
Examples:
|
||||
|
||||
- `fix(preview): restore local html loading`
|
||||
- `feat(workspace): add file preview shortcuts`
|
||||
- `docs(contributing): document pr title format`
|
||||
|
||||
## Rule 3: Pass Local Checks Before Push
|
||||
|
||||
CI will reject your PR if these checks fail. Run them locally **before pushing** to save time.
|
||||
|
||||
### Step-by-step
|
||||
|
||||
```bash
|
||||
# 1. Format (always run — covers .ts, .tsx, .css, .json, .md)
|
||||
bun run format
|
||||
|
||||
# 2. Lint (skip if no .ts/.tsx files changed)
|
||||
bun run lint
|
||||
|
||||
# 3. Type check (skip if no .ts/.tsx files changed)
|
||||
bunx tsc --noEmit
|
||||
|
||||
# 4. i18n validation (only if you changed files in src/renderer/, locales/, or src/common/config/i18n/)
|
||||
bun run i18n:types
|
||||
node scripts/check-i18n.js
|
||||
|
||||
# 5. Tests
|
||||
bunx vitest run
|
||||
```
|
||||
|
||||
### One-command alternative
|
||||
|
||||
This replicates the exact CI quality check, then runs tests:
|
||||
|
||||
```bash
|
||||
prek run --from-ref origin/main --to-ref HEAD
|
||||
bunx vitest run
|
||||
```
|
||||
|
||||
> `prek` runs format-check + lint + tsc in read-only mode. If it reports issues, run the auto-fix commands above first, then re-run prek.
|
||||
|
||||
### Common failures and fixes
|
||||
|
||||
| Failure | Fix |
|
||||
| ------------- | -------------------------------------------------------------------- |
|
||||
| Format errors | `bun run format` (auto-fixes) |
|
||||
| Lint errors | `bun run lint:fix` for auto-fixable issues; fix the rest manually |
|
||||
| Type errors | Fix the TypeScript issue, then re-run `bunx tsc --noEmit` |
|
||||
| i18n errors | Check for missing keys; run `bun run i18n:types` to regenerate types |
|
||||
| Test failures | Fix the failing test or implementation; re-run `bunx vitest run` |
|
||||
|
||||
## Enforcement
|
||||
|
||||
When these rules are not followed, maintainers may:
|
||||
|
||||
1. **Close and request resubmission** (preferred) — you retain full credit upon proper resubmission.
|
||||
2. **Cherry-pick valuable portions** — your authorship is preserved in git history, but the original PR shows as "Closed" rather than "Merged".
|
||||
|
||||
Code style, dependency choices, and documentation polish are handled by maintainers post-merge. Focus your PR on the functional change.
|
||||
@@ -0,0 +1,114 @@
|
||||
# 贡献指南
|
||||
|
||||
> **English version**: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## 前置条件
|
||||
|
||||
环境搭建请参考 [docs/contributing/development.md](docs/contributing/development.md),你需要:
|
||||
|
||||
- Node.js 22+
|
||||
- [bun](https://bun.sh)
|
||||
- [Rust stable + Cargo](https://rustup.rs),用于构建本地 AionCore 后端
|
||||
- [prek](https://github.com/j178/prek)(`npm install -g @j178/prek`)
|
||||
|
||||
## 规则一:原子化 PR
|
||||
|
||||
每个 PR 只能包含**一个不可再拆的 feature 或一个 bug fix**。
|
||||
|
||||
**判断方法:** 问自己(或 AI):_"这个 diff 能否拆成多个独立可合并的 PR?"_ 如果能,提交前必须拆分。
|
||||
|
||||
### 示例
|
||||
|
||||
**可接受(单个 PR):**
|
||||
|
||||
- 一个根因的 bug 修复,即使涉及多个文件(例如修复 toast 在 modal 和聊天层的 z-index 问题)
|
||||
- 一个完整的功能(例如团队创建弹窗及其表单校验)
|
||||
|
||||
**必须拆分成多个 PR:**
|
||||
|
||||
- 团队聊天滚动修复 + Sentry 用户追踪 + Office 预览性能优化 = 3 个 PR
|
||||
- 多个不相关的 bug 修复打包在一起(例如标题栏导航修复 + i18n 缺失 key + 语音输入 UI 修复)
|
||||
- 独立的技术层(例如 IPC 桥接重构 + 渲染进程组件 + Worker 进程变更,分属不相关的功能)
|
||||
|
||||
## 规则二:Commit 和 PR 标题格式
|
||||
|
||||
Commit message 和 PR 标题必须使用英文 Conventional Commit 格式:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
`type` 必须使用以下取值之一:
|
||||
|
||||
| Type | 含义 | Changelog 可见性 |
|
||||
| ---------- | ---------- | ---------------- |
|
||||
| `feat` | 新用户功能 | 可见 |
|
||||
| `fix` | Bug 修复 | 可见 |
|
||||
| `perf` | 性能优化 | 可见 |
|
||||
| `refactor` | 代码重构 | 可见 |
|
||||
| `docs` | 文档 | 可见 |
|
||||
| `style` | 格式或样式 | 隐藏 |
|
||||
| `chore` | 维护工作 | 隐藏 |
|
||||
| `test` | 测试 | 隐藏 |
|
||||
| `ci` | CI 配置 | 隐藏 |
|
||||
| `build` | 构建系统 | 隐藏 |
|
||||
|
||||
示例:
|
||||
|
||||
- `fix(preview): restore local html loading`
|
||||
- `feat(workspace): add file preview shortcuts`
|
||||
- `docs(contributing): document pr title format`
|
||||
|
||||
## 规则三:Push 前必须通过本地检查
|
||||
|
||||
CI 会在这些检查失败时拒绝你的 PR。**推送前**在本地运行,节省时间。
|
||||
|
||||
### 逐步执行
|
||||
|
||||
```bash
|
||||
# 1. 格式化(必须运行 — 覆盖 .ts, .tsx, .css, .json, .md)
|
||||
bun run format
|
||||
|
||||
# 2. Lint 检查(如果没改 .ts/.tsx 文件可跳过)
|
||||
bun run lint
|
||||
|
||||
# 3. 类型检查(如果没改 .ts/.tsx 文件可跳过)
|
||||
bunx tsc --noEmit
|
||||
|
||||
# 4. i18n 校验(仅当修改了 src/renderer/、locales/ 或 src/common/config/i18n/ 下的文件时)
|
||||
bun run i18n:types
|
||||
node scripts/check-i18n.js
|
||||
|
||||
# 5. 测试
|
||||
bunx vitest run
|
||||
```
|
||||
|
||||
### 一条命令替代
|
||||
|
||||
完全复刻 CI 质量检查,再跑测试:
|
||||
|
||||
```bash
|
||||
prek run --from-ref origin/main --to-ref HEAD
|
||||
bunx vitest run
|
||||
```
|
||||
|
||||
> `prek` 以只读模式运行 format-check + lint + tsc。如果报错,先运行上面的自动修复命令,再重新运行 prek。
|
||||
|
||||
### 常见失败及修复
|
||||
|
||||
| 失败类型 | 修复方法 |
|
||||
| --------- | ------------------------------------------------------ |
|
||||
| 格式错误 | `bun run format`(自动修复) |
|
||||
| Lint 错误 | `bun run lint:fix` 修复可自动修复的部分,其余手动修复 |
|
||||
| 类型错误 | 修复 TypeScript 问题,重新运行 `bunx tsc --noEmit` |
|
||||
| i18n 错误 | 检查缺失的 key,运行 `bun run i18n:types` 重新生成类型 |
|
||||
| 测试失败 | 修复失败的测试或实现,重新运行 `bunx vitest run` |
|
||||
|
||||
## 执行方式
|
||||
|
||||
不符合规则时,维护者可能:
|
||||
|
||||
1. **关闭并要求重新提交**(首选)—— 正确重提后你保留全部署名。
|
||||
2. **Cherry-pick 有价值的部分** —— 你的作者信息保留在 git 历史中,但原 PR 显示为 "Closed" 而非 "Merged"。
|
||||
|
||||
代码风格、依赖选择、文档润色由维护者在合并后处理。你的 PR 只需聚焦功能变更本身。
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
FROM node:20-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install bun
|
||||
RUN npm install -g bun
|
||||
|
||||
# Install all dependencies (including devDeps for build)
|
||||
COPY package.json bun.lock ./
|
||||
COPY patches/ ./patches/
|
||||
RUN bun install --ignore-scripts
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build renderer (no Electron needed) and server bundle
|
||||
RUN bun run build:renderer:web
|
||||
RUN node scripts/build-server.mjs
|
||||
|
||||
# ---- Runtime image ----
|
||||
FROM oven/bun:latest AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# officecli (the Office preview component, auto-installed at runtime by the
|
||||
# backend) is a .NET binary that aborts on startup without ICU, and Debian
|
||||
# base images don't ship it. libicu-dev is version-agnostic so it keeps
|
||||
# resolving the right libicuNN when the base image bumps Debian releases.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libicu-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only build artifacts and production deps
|
||||
COPY --from=builder /app/dist-server ./dist-server
|
||||
COPY --from=builder /app/out/renderer ./out/renderer
|
||||
COPY package.json bun.lock ./
|
||||
COPY patches/ ./patches/
|
||||
RUN bun install --production --ignore-scripts
|
||||
|
||||
ENV PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
ENV ALLOW_REMOTE=true
|
||||
ENV DATA_DIR=/data
|
||||
|
||||
# SQLite data volume — mount with: -v $(pwd)/data:/data
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["bun", "dist-server/server.mjs"]
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 AionUi (aionui.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
cat-config:
|
||||
@base64 -D -i ~/.aionui-config-dev/aionui-config.txt | python3 -c 'import sys, urllib.parse; print(urllib.parse.unquote(sys.stdin.read()))' | pbcopy
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`iOfficeAI/AionUi`
|
||||
- 原始仓库:https://github.com/iOfficeAI/AionUi
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
max_report_age: off
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: '0...100'
|
||||
|
||||
comment:
|
||||
layout: 'reach,diff,flags,files'
|
||||
behavior: default
|
||||
require_changes: false
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
target: 50%
|
||||
informational: true
|
||||
|
||||
ignore:
|
||||
- 'tests/**'
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
- '**/*.d.ts'
|
||||
- 'packages/desktop/src/index.ts'
|
||||
- 'packages/desktop/src/preload.ts'
|
||||
- 'packages/desktop/src/common/utils/shims/**'
|
||||
- 'packages/desktop/src/common/types/**'
|
||||
- 'packages/desktop/src/common/config/i18n-config.json'
|
||||
- 'packages/desktop/src/renderer/**/*.json'
|
||||
- 'packages/desktop/src/renderer/**/*.svg'
|
||||
- 'packages/desktop/src/renderer/**/*.css'
|
||||
- 'scripts/**'
|
||||
- 'examples/**'
|
||||
- 'resources/**'
|
||||
- 'skills/**'
|
||||
- 'assistant/**'
|
||||
- 'homebrew/**'
|
||||
- 'patches/**'
|
||||
@@ -0,0 +1,31 @@
|
||||
# AionUi Docs
|
||||
|
||||
Documentation is organized by reader intent, not by document type.
|
||||
|
||||
| Directory | For whom | What lives here |
|
||||
| ------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`guides/`](guides) | Users & operators | How to deploy, test, and run the product. Server deployment, WebUI, Hub testing, CDP debugging. |
|
||||
| [`contributing/`](contributing) | Contributors | Dev environment setup, file-structure conventions, PR automation workflow. |
|
||||
| [`architecture/`](architecture) | Engineers & architects | System architecture overview, subsystem deep-dives (ACP, queue, team mode), and supporting research notes. |
|
||||
| [`specs/`](specs) | Engineering-driven specs | Feature design docs, requirements, implementation plans (ACP rewrite, extension market, remote agent, wake prompt, PR notes). |
|
||||
| [`prds/`](prds) | Product team | Formal Product Requirement Documents maintained by the product team. **Do not reorganize without their consent.** |
|
||||
| [`readme/`](readme) | Global users | Translated copies of the root `readme.md` (Chinese, Japanese, Korean, Spanish, etc.). |
|
||||
|
||||
## Quick pointers
|
||||
|
||||
- New to the project? Start with [`architecture/overview.md`](architecture/overview.md).
|
||||
- Setting up a dev environment? See [`contributing/development.md`](contributing/development.md).
|
||||
- Writing code? The entry point for code-style, linting, formatting, and commit rules is [`AGENTS.md`](../AGENTS.md) at the repo root.
|
||||
- Deploying a server? [`guides/deploy-server.md`](guides/deploy-server.md).
|
||||
|
||||
## Where to put new docs
|
||||
|
||||
| Content type | Destination |
|
||||
| ---------------------------------------------------------- | --------------------------- |
|
||||
| User/ops-facing how-to | `guides/` |
|
||||
| Contributor convention, workflow, or tooling rule | `contributing/` |
|
||||
| System or subsystem design, technical analysis | `architecture/` |
|
||||
| Exploratory research, analysis reports | `architecture/research/` |
|
||||
| Feature requirements / design drafts driven by engineering | `specs/<feature-name>/` |
|
||||
| Formal PRD owned by product team | `prds/` (coordinate first) |
|
||||
| README translation | `readme/readme_<locale>.md` |
|
||||
@@ -0,0 +1,278 @@
|
||||
# Development Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** 22 or higher
|
||||
- **bun** — Package manager & runtime ([install](https://bun.sh))
|
||||
- **Rust stable + Cargo** — Required to build the local AionCore backend ([install](https://rustup.rs))
|
||||
- **Python** 3.11+ (for native module compilation)
|
||||
- **prek** — PR code checker (`npm install -g @j178/prek`)
|
||||
|
||||
On Windows, install the Rust MSVC toolchain. If Rust compilation fails because native build tools are missing, install **Microsoft C++ Build Tools** from the Visual Studio installer, then reopen your terminal.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
AionUi development uses two repositories:
|
||||
|
||||
- **AionCore** (`https://github.com/iOfficeAI/AionCore.git`) builds the local backend binary: `aioncore` on macOS/Linux and `aioncore.exe` on Windows.
|
||||
- **AionUi** (`https://github.com/iOfficeAI/AionUi.git`) starts the Electron desktop app and launches the backend binary automatically.
|
||||
|
||||
Keep the repositories side by side when possible:
|
||||
|
||||
```text
|
||||
workspace/
|
||||
|-- AionCore/
|
||||
`-- AionUi/
|
||||
```
|
||||
|
||||
The desktop development server resolves the backend from the `PATH` inherited by `bun run start`. Install AionCore first, verify the binary is discoverable in the same terminal, then start AionUi.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone Both Repositories
|
||||
|
||||
```bash
|
||||
git clone https://github.com/iOfficeAI/AionCore.git
|
||||
git clone https://github.com/iOfficeAI/AionUi.git
|
||||
```
|
||||
|
||||
Use the `main` branch for both repositories unless a maintainer asks you to test another branch.
|
||||
|
||||
### 2. Build and Install AionCore
|
||||
|
||||
Run these commands from the `AionCore` repository.
|
||||
|
||||
#### macOS / Linux
|
||||
|
||||
```bash
|
||||
cd AionCore
|
||||
cargo clean
|
||||
cargo install --path crates/aionui-app --locked
|
||||
|
||||
# Make Cargo-installed binaries visible to this shell if needed.
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# Verify that AionUi will be able to find the backend.
|
||||
which aioncore
|
||||
aioncore --help
|
||||
```
|
||||
|
||||
If `which aioncore` prints nothing, add `export PATH="$HOME/.cargo/bin:$PATH"` to your shell profile (`~/.zshrc`, `~/.bashrc`, or your shell's equivalent), open a new terminal, and verify again.
|
||||
|
||||
#### Windows PowerShell
|
||||
|
||||
```powershell
|
||||
cd AionCore
|
||||
cargo clean
|
||||
cargo install --path crates/aionui-app --locked
|
||||
|
||||
# Make Cargo-installed binaries visible to this PowerShell session if needed.
|
||||
$env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"
|
||||
|
||||
# Verify that AionUi will be able to find the backend.
|
||||
where.exe aioncore
|
||||
aioncore --help
|
||||
```
|
||||
|
||||
If `where.exe aioncore` prints nothing, make sure `%USERPROFILE%\.cargo\bin` is in your user `Path`, open a new PowerShell window, and verify again.
|
||||
|
||||
### 3. Start AionUi
|
||||
|
||||
Run these commands from the `AionUi` repository in a terminal where `aioncore` is discoverable.
|
||||
|
||||
```bash
|
||||
cd AionUi
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Start the Electron desktop app in development mode
|
||||
bun run start
|
||||
```
|
||||
|
||||
During startup, AionUi launches `aioncore` automatically and passes the backend port to the renderer. You do not need to start AionCore in a separate terminal.
|
||||
|
||||
## Updating the Local Backend
|
||||
|
||||
When you pull or change AionCore, reinstall the backend binary and restart AionUi:
|
||||
|
||||
```bash
|
||||
cd ../AionCore
|
||||
cargo install --path crates/aionui-app --locked --force
|
||||
|
||||
cd ../AionUi
|
||||
bun run start
|
||||
```
|
||||
|
||||
Use `--force` when rebuilding local changes with the same AionCore package version; otherwise Cargo may keep the already installed binary.
|
||||
|
||||
## Backend Startup Troubleshooting
|
||||
|
||||
### `Cannot find "aioncore" binary`
|
||||
|
||||
AionUi cannot find the backend from the `PATH` inherited by `bun run start`.
|
||||
|
||||
Check from the same terminal where you start AionUi:
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
which aioncore
|
||||
|
||||
# Windows PowerShell
|
||||
where.exe aioncore
|
||||
```
|
||||
|
||||
If the command fails, add Cargo's binary directory to `PATH` and start AionUi from a new terminal.
|
||||
|
||||
### `aioncore` Works in a Terminal but AionUi Still Cannot Find It
|
||||
|
||||
Make sure you start `bun run start` from the same terminal environment that can run `aioncore --help`. IDE terminals and GUI-launched shells can inherit a different `PATH`; restart the IDE or launch it from a terminal after updating `PATH`.
|
||||
|
||||
### Backend Changes Do Not Show Up
|
||||
|
||||
Quit AionUi, reinstall AionCore with `cargo install --path crates/aionui-app --locked --force`, then start AionUi again. The Electron app owns the backend subprocess during development, so a running AionUi instance will not pick up a newly installed binary until it restarts.
|
||||
|
||||
### Windows Rust Build Errors
|
||||
|
||||
Use the Rust MSVC toolchain and install Microsoft C++ Build Tools. After installing or changing toolchains, open a new PowerShell window and rerun the AionCore install command.
|
||||
|
||||
## Scripts Reference
|
||||
|
||||
### Development
|
||||
|
||||
| Command | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `bun start` | Start Electron app in development mode (desktop) |
|
||||
| `bun run start:multi` | Start a second Electron instance alongside an existing one (see [Multi-Instance](#multi-instance-development)) |
|
||||
| `bun run cli` | Alias for `bun start` |
|
||||
| `bun run webui` | Start in WebUI mode (browser-based, no Electron window) |
|
||||
| `bun run webui:remote` | Start in WebUI mode with remote access enabled |
|
||||
| `bun run webui:prod` | Start WebUI in production mode |
|
||||
| `bun run webui:prod:remote` | Start WebUI in production mode with remote access |
|
||||
| `bun run resetpass` | Reset user password via CLI |
|
||||
|
||||
### Build & Distribution
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `bun run package` | Build all processes (main, preload, renderer) to `out/` |
|
||||
| `bun run make` | Alias for `bun run package` |
|
||||
| `bun run dist` | Build and package distributable for current platform |
|
||||
| `bun run dist:mac` | Build distributable for macOS |
|
||||
| `bun run dist:win` | Build distributable for Windows |
|
||||
| `bun run dist:linux` | Build distributable for Linux |
|
||||
| `bun run build-mac` | Build macOS distributable for both arm64 and x64 |
|
||||
| `bun run build-mac:arm64` | Build macOS distributable for Apple Silicon only |
|
||||
| `bun run build-mac:x64` | Build macOS distributable for Intel only |
|
||||
| `bun run build-win` | Build Windows distributable |
|
||||
| `bun run build-win:arm64` | Build Windows distributable for ARM64 |
|
||||
| `bun run build-win:x64` | Build Windows distributable for x64 |
|
||||
| `bun run build-deb` | Build Linux (.deb) distributable |
|
||||
| `bun run build` | Alias for `bun run build-mac` |
|
||||
|
||||
### Standalone Server (non-Electron)
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------------- | ----------------------------------------------------------- |
|
||||
| `bun run build:renderer:web` | Build renderer for standalone web deployment |
|
||||
| `bun run build:server` | Build standalone server bundle to `dist-server/` |
|
||||
| `bun run server:start` | Run standalone server in development mode |
|
||||
| `bun run server:start:remote` | Run standalone server with remote access |
|
||||
| `bun run server:start:prod` | Run standalone server in production mode |
|
||||
| `bun run server:start:prod:remote` | Run standalone server in production mode with remote access |
|
||||
| `bun run server:resetpass` | Reset password via standalone server CLI |
|
||||
| `bun run server:resetpass:prod` | Reset password via standalone server CLI (production) |
|
||||
|
||||
### Code Quality
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ----------------------------------------- |
|
||||
| `bun run lint` | Check for lint issues (oxlint, read-only) |
|
||||
| `bun run lint:fix` | Auto-fix lint issues |
|
||||
| `bun run format` | Auto-format code (oxfmt) |
|
||||
| `bun run format:check` | Check formatting without modifying files |
|
||||
| `bun run i18n:types` | Generate TypeScript types for i18n keys |
|
||||
|
||||
### Testing
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ------------------------------------------------- |
|
||||
| `bun run test` | Run all unit tests (vitest) |
|
||||
| `bun run test:watch` | Run tests in watch mode |
|
||||
| `bun run test:coverage` | Run tests with coverage report |
|
||||
| `bun run test:contract` | Run contract tests |
|
||||
| `bun run test:integration` | Run integration tests |
|
||||
| `bun run test:bun` | Run Bun-specific database driver tests |
|
||||
| `bun run test:e2e` | Run end-to-end tests (Playwright) |
|
||||
| `bun run test:packaged:i18n` | Run i18n integration tests against packaged build |
|
||||
| `bun run test:packaged:bun` | Run Bun packaged integration tests |
|
||||
|
||||
### Debug
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ----------------------------------------------- |
|
||||
| `bun run debug:perf` | Start app with performance monitoring enabled |
|
||||
| `bun run debug:perf:report` | Generate performance report from collected data |
|
||||
| `bun run debug:mcp` | Debug MCP server connections |
|
||||
| `bun run debug:mcp:list` | List configured MCP servers |
|
||||
| `bun run debug:mcp:validate` | Validate MCP server configurations |
|
||||
| `bun run debug:custom-agent` | Debug custom agent connections |
|
||||
|
||||
## Multi-Instance Development
|
||||
|
||||
When you have two clones of the repository (e.g. `AionUi` and `AionUi-refactor`) and need to run both simultaneously, the second instance can be started with:
|
||||
|
||||
```bash
|
||||
bun run start:multi
|
||||
```
|
||||
|
||||
This sets `AIONUI_MULTI_INSTANCE=1`, which:
|
||||
|
||||
- Skips the Electron single-instance lock
|
||||
- Uses a separate userData directory (`AionUi-Dev-2`) to avoid database and config conflicts
|
||||
- Isolates data/config symlink paths (`~/.aionui-dev-2`, `~/.aionui-config-dev-2`)
|
||||
- Vite renderer, CDP, and WebUI proxy ports auto-increment to avoid collisions
|
||||
|
||||
> **Note:** The multi-instance WebUI defaults to port 25810 (instead of 25809). When accessing WebUI in a browser, use an **incognito/private window** for the second instance — both instances share the `localhost` cookie jar, and their JWT secrets differ, causing authentication failures if the same browser session is reused.
|
||||
|
||||
## Code Checks (prek)
|
||||
|
||||
The project uses [prek](https://github.com/j178/prek) (a Rust implementation of pre-commit) for code checks, configured in `.pre-commit-config.yaml`:
|
||||
|
||||
```bash
|
||||
# Install prek
|
||||
npm install -g @j178/prek
|
||||
|
||||
# Install git hooks (optional, auto-check before commit)
|
||||
prek install
|
||||
|
||||
# Run checks on staged files
|
||||
prek run
|
||||
|
||||
# Run checks on changes vs main (same as CI)
|
||||
prek run --from-ref origin/main --to-ref HEAD
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
AionUi uses **electron-vite** for fast bundling:
|
||||
|
||||
- **Main process**: bundled with Vite (ESM)
|
||||
- **Renderer process**: bundled with Vite (React + TypeScript)
|
||||
- **Preload scripts**: bundled with Vite
|
||||
|
||||
The build output goes to `out/` directory:
|
||||
|
||||
- `out/main/` - Main process code
|
||||
- `out/renderer/` - Renderer process code
|
||||
- `out/preload/` - Preload scripts
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Electron** - Cross-platform desktop framework
|
||||
- **React 19** - UI framework
|
||||
- **TypeScript** - Type safety
|
||||
- **Vite** - Fast bundler (via electron-vite)
|
||||
- **UnoCSS** - Atomic CSS engine
|
||||
- **better-sqlite3** - Local database
|
||||
- **vitest** - Testing framework
|
||||
@@ -0,0 +1,315 @@
|
||||
# File & Directory Structure
|
||||
|
||||
Rules for organizing files and directories across the entire Electron project.
|
||||
|
||||
## Repository Root
|
||||
|
||||
### Root Directory Rules
|
||||
|
||||
- **README translations** belong in `docs/readme/`, not at root. Only the main `readme.md` stays at root (GitHub convention)
|
||||
- **Guide documents** (deployment, testing, WebUI, CDP, etc.) belong in `docs/guides/`
|
||||
- **Contributor documentation** (dev setup, code style, file structure, PR workflow) belongs in `docs/contributing/`
|
||||
- **Architecture documentation** belongs in `docs/architecture/` (research notes under `docs/architecture/research/`)
|
||||
- **Feature specs / PRDs / design drafts** belong in `docs/specs/` (or `docs/prds/` for formal PRDs maintained by the product team)
|
||||
- **Config files** (`tsconfig.json`, `package.json`, etc.) stay at root — Node.js/Electron ecosystem convention
|
||||
- **New documentation** should be placed under the appropriate `docs/` subdirectory, not at project root
|
||||
|
||||
### Current Root Cleanup Targets
|
||||
|
||||
| Action | Files |
|
||||
| ------------------------------------------ | ---------------------------------- |
|
||||
| Move readme translations to `docs/readme/` | `readme_{ch,es,jp,ko,pt,tr,tw}.md` |
|
||||
|
||||
## Project Layout (`src/`)
|
||||
|
||||
AionUi is a multi-process Electron app with three core layers: **renderer**, **main process**, and **preload/shared**.
|
||||
|
||||
### Target Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── renderer/ # Renderer layer — React UI, no Node.js APIs
|
||||
├── process/ # Main process layer — all Node.js / Electron business
|
||||
│ ├── bridge/ # IPC handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── database/ # SQLite
|
||||
│ ├── task/ # Agent/task management
|
||||
│ ├── agent/ # AI platform connections
|
||||
│ ├── channels/ # Multi-channel messaging
|
||||
│ ├── extensions/ # Plugin system
|
||||
│ ├── webserver/ # WebUI server
|
||||
│ ├── worker/ # Background workers (fork)
|
||||
│ └── i18n/ # Main-process i18n
|
||||
├── common/ # Shared layer — cross-process types, adapters, utilities
|
||||
├── preload.ts # IPC bridge — contextBridge between main ↔ renderer
|
||||
└── index.ts # Main process entry point
|
||||
```
|
||||
|
||||
### Current Structure
|
||||
|
||||
All main-process modules now live under `src/process/`. The `src/` root contains only the three core layers (`renderer/`, `process/`, `common/`), the entry files (`index.ts`, `preload.ts`), and the ambient type declaration (`types.d.ts`).
|
||||
|
||||
## Directory Naming — Two Conventions by Process
|
||||
|
||||
This project straddles two ecosystems. Each follows its own convention:
|
||||
|
||||
| Scope | Directory naming | Reason |
|
||||
| ---------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| **Renderer** (`src/renderer/`) | **PascalCase** for component/module dirs | React ecosystem — directory name = component name |
|
||||
| **Everything else** | **lowercase** | Node.js ecosystem |
|
||||
| **Categorical dirs** (everywhere) | **lowercase** | `components/`, `hooks/`, `utils/`, `services/` are categories, not entities |
|
||||
| **Platform dirs** (renderer pages) | **lowercase** | Mirror `src/process/agent/<platform>/` naming for cross-process consistency |
|
||||
|
||||
### Quick test
|
||||
|
||||
> "Is this directory inside `src/renderer/` AND does it represent a specific component or feature module (not a category)?"
|
||||
>
|
||||
> **YES** → PascalCase. **NO** → lowercase.
|
||||
>
|
||||
> **Exception**: Platform directories (`acp/`, `codex/`, `gemini/`, `nanobot/`, `openclaw/`) always use lowercase, even inside renderer, to match `src/process/agent/`.
|
||||
|
||||
### Renderer examples
|
||||
|
||||
```
|
||||
src/renderer/
|
||||
├── components/ # categorical → lowercase
|
||||
│ ├── SettingsModal/ # component → PascalCase
|
||||
│ └── EmojiPicker/ # component → PascalCase
|
||||
├── pages/ # categorical → lowercase
|
||||
│ ├── settings/ # top-level page → lowercase (route segment)
|
||||
│ │ ├── CssThemeSettings/ # feature module → PascalCase
|
||||
│ │ └── McpManagement/ # feature module → PascalCase
|
||||
│ └── conversation/ # top-level page → lowercase
|
||||
│ ├── GroupedHistory/ # feature module → PascalCase
|
||||
│ ├── Workspace/ # feature module → PascalCase
|
||||
│ ├── acp/ # platform dir → lowercase (mirrors src/agent/acp/)
|
||||
│ └── components/ # categorical → lowercase
|
||||
└── hooks/ # categorical → lowercase
|
||||
```
|
||||
|
||||
### Non-renderer examples
|
||||
|
||||
```
|
||||
src/process/services/cron/ # lowercase
|
||||
src/process/agent/acp/ # lowercase
|
||||
src/process/channels/plugins/dingtalk/ # lowercase
|
||||
```
|
||||
|
||||
## File Naming — Same Everywhere
|
||||
|
||||
| Content | Convention | Examples |
|
||||
| ------------------------- | ------------------------------- | ------------------------------------- |
|
||||
| React components, classes | PascalCase | `SettingsModal.tsx`, `CronService.ts` |
|
||||
| Hooks | camelCase with `use` prefix | `useTheme.ts`, `useCronJobs.ts` |
|
||||
| Utilities, helpers | camelCase | `formatDate.ts`, `cronUtils.ts` |
|
||||
| Entry points | `index.ts` / `index.tsx` | Required for directory-based modules |
|
||||
| Config, types, constants | camelCase | `types.ts`, `constants.ts` |
|
||||
| Styles | kebab-case or `Name.module.css` | `chat-layout.css` |
|
||||
|
||||
## Process Boundary Rules
|
||||
|
||||
**Violating these causes runtime crashes.**
|
||||
|
||||
| Process | Can use | Cannot use |
|
||||
| ---------------------------------- | ----------------------------- | -------------------------------- |
|
||||
| **Main** (`src/process/`) | Node.js, Electron main APIs | DOM APIs, React |
|
||||
| **Renderer** (`src/renderer/`) | DOM APIs, React, browser APIs | Node.js APIs, Electron main APIs |
|
||||
| **Worker** (`src/process/worker/`) | Node.js APIs | DOM APIs, Electron APIs |
|
||||
|
||||
Cross-process communication MUST go through:
|
||||
|
||||
- Main ↔ Renderer: IPC via `src/preload.ts` + `src/process/bridge/*.ts`
|
||||
- Main ↔ Worker: fork protocol via `src/process/worker/WorkerProtocol.ts`
|
||||
|
||||
## Main Process Naming
|
||||
|
||||
| Type | Pattern | Examples |
|
||||
| ---------- | --------------------- | --------------------------------- |
|
||||
| Bridge | `<domain>Bridge.ts` | `cronBridge.ts`, `webuiBridge.ts` |
|
||||
| Service | `<Name>Service.ts` | `CronService.ts`, `McpService.ts` |
|
||||
| Interface | `I<Name>Service.ts` | `IConversationService.ts` |
|
||||
| Repository | `<Name>Repository.ts` | `SqliteConversationRepository.ts` |
|
||||
|
||||
## Service Testability Rules
|
||||
|
||||
### Pure Logic vs IO Separation
|
||||
|
||||
Services must separate **pure logic** from **IO operations**:
|
||||
|
||||
- **Pure logic** (data transformation, validation, formatting) → standalone functions, no `fs`/`db`/`net` imports
|
||||
- **IO operations** (file read, DB query, HTTP call) → thin wrappers in service class or repository
|
||||
- Service methods should receive IO results as parameters rather than calling IO internally
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Services and bridges that depend on external resources (DB, file system, other services) should accept dependencies as constructor/function parameters:
|
||||
|
||||
```typescript
|
||||
// ❌ Hard to test — must mock the entire module
|
||||
import { db } from '@process/database';
|
||||
function getConversation(id: string) {
|
||||
return db.query('SELECT * FROM conversations WHERE id = ?', id);
|
||||
}
|
||||
|
||||
// ✅ Easy to test — inject the dependency
|
||||
function getConversation(repo: IConversationRepository, id: string) {
|
||||
return repo.findById(id);
|
||||
}
|
||||
```
|
||||
|
||||
For existing code using direct imports, `vi.mock()` is acceptable. For new code, prefer parameter injection.
|
||||
|
||||
## Test File Mapping
|
||||
|
||||
Test files must mirror the source file they test:
|
||||
|
||||
| Source | Test |
|
||||
| -------------------------------------------- | ----------------------------------------------- |
|
||||
| `src/process/services/CronService.ts` | `tests/unit/cronService.test.ts` |
|
||||
| `src/process/bridge/fsBridge.ts` | `tests/unit/fsBridge.test.ts` |
|
||||
| `src/renderer/utils/chat/latexDelimiters.ts` | `tests/unit/latexDelimiters.test.ts` |
|
||||
| `src/renderer/hooks/ui/useAutoScroll.ts` | `tests/unit/useAutoScroll.dom.test.ts` |
|
||||
| `src/process/extensions/ExtensionLoader.ts` | `tests/unit/extensions/extensionLoader.test.ts` |
|
||||
|
||||
When `tests/unit/` exceeds 10 direct children, group into subdirectories matching the source structure (e.g., `tests/unit/extensions/`). New source files with logic should be added to `vitest.config.ts` → `coverage.include`.
|
||||
|
||||
## Directory Size Limit
|
||||
|
||||
A single directory must not contain more than **10** direct children (files + subdirectories). When a directory approaches this limit, split its contents into subdirectories grouped by responsibility.
|
||||
|
||||
## UI Library & Icon Standards
|
||||
|
||||
- **Component library**: `@arco-design/web-react`. All new UI must use Arco components first.
|
||||
- **Icon library**: `@icon-park/react`. All icons must come from this library.
|
||||
- **No raw HTML for interactive elements**: Do not use native `<button>`, `<input>`, `<select>`, `<textarea>`, `<modal>`, etc. Use the corresponding Arco component (`Button`, `Input`, `Select`, `Modal`, etc.).
|
||||
- **Layout tags are fine**: `<div>`, `<span>`, `<section>`, `<nav>`, `<main>`, and other pure layout/semantic tags may be used freely.
|
||||
|
||||
## CSS Conventions
|
||||
|
||||
- **Prefer UnoCSS utility classes**: Use atomic classes for simple styles (`flex items-center gap-8px`).
|
||||
- **Complex/reusable styles**: Must use **CSS Modules** (`ComponentName.module.css`). Plain `.css` files are not allowed for component styles.
|
||||
- **Semantic color tokens only**: Use colors from `uno.config.ts` (e.g., `text-t-primary`, `bg-base`, `border-b-base`) or CSS variables. **Hardcoded color values are forbidden** (e.g., `#86909C`, `rgb(0,0,0)`). Exception: theme preset files under `src/renderer/pages/settings/CssThemeSettings/presets/` may use hardcoded values since they define the theme tokens themselves.
|
||||
- **No inline styles**: Do not use `style={{}}` except for dynamically computed values (e.g., calculated widths, positions).
|
||||
- **Arco style overrides**: Co-locate in the component's CSS Module using `:global(.arco-xxx)`. Do not use a global override file.
|
||||
- **Global styles**: Only allowed in `src/renderer/styles/` (themes, reset, layout base). No CSS files directly in `src/renderer/` root.
|
||||
|
||||
## Renderer Root Directory — Standard Layout
|
||||
|
||||
The renderer root must contain **at most 3 entry files + 7 directories = 10 items**.
|
||||
|
||||
```
|
||||
src/renderer/
|
||||
├── index.html # Vite HTML entry
|
||||
├── main.tsx # React mount + app bootstrap
|
||||
├── types.d.ts # Ambient type declarations
|
||||
├── pages/ # Page-level modules (business code goes here)
|
||||
├── components/ # Shared UI components (used across multiple pages)
|
||||
├── hooks/ # Shared React hooks (supports business domain subdirs)
|
||||
├── context/ # Global React contexts
|
||||
├── services/ # Client-side services + i18n
|
||||
├── utils/ # Utility functions + types + constants
|
||||
├── styles/ # Global styles + theme configuration
|
||||
└── assets/ # Static assets — Vite resolves to hashed URLs
|
||||
```
|
||||
|
||||
**What does NOT belong at the renderer root:**
|
||||
|
||||
- CSS files → move to `styles/`
|
||||
- Component files (`.tsx`) → move to `components/` or `pages/`
|
||||
- Single-file directories (only 1 file inside) → merge into a related directory
|
||||
|
||||
## Renderer Component Rules
|
||||
|
||||
- **Single file** when self-contained; **directory** when it has sub-components/hooks
|
||||
- Directory-based components must have `index.tsx` entry point
|
||||
- **Single-file directory rule**: A directory containing only 1 file should be merged into its parent or a related directory
|
||||
- Page-private code stays under `pages/<PageName>/`; move to shared only when a second consumer appears
|
||||
|
||||
### `src/renderer/components/` Structure
|
||||
|
||||
`components/` is for shared components used across multiple pages. It has two layers:
|
||||
|
||||
**Fixed layer:**
|
||||
|
||||
- `base/` — Generic UI primitives with no business logic. The only fixed subdirectory. Components here must not depend on app-specific context or domain logic.
|
||||
|
||||
**Business layer:**
|
||||
|
||||
- Create subdirectories by **business domain**, using lowercase naming (categorical directory rule)
|
||||
- Create a domain subdirectory when **≥ 2** shared components belong to the same domain
|
||||
- A single component may stay at the `components/` root temporarily until a second component in the same domain appears
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- The `components/` root must not exceed **10** direct children (files + directories)
|
||||
- Components used by only **one** page must live in `pages/<PageName>/components/`, not here
|
||||
|
||||
```
|
||||
src/renderer/components/
|
||||
├── base/ # UI primitives — AionModal, AionSelect, FlexFullContainer, etc.
|
||||
├── chat/ # Conversation/message domain (example, not exhaustive)
|
||||
├── agent/ # Agent selection/configuration domain
|
||||
├── settings/ # Settings domain
|
||||
├── layout/ # Window frame and layout
|
||||
├── media/ # File preview, image viewer
|
||||
└── index.ts # Public re-exports (optional)
|
||||
```
|
||||
|
||||
> The business subdirectory list above is illustrative. New domains are created as needed following the same rules.
|
||||
|
||||
### `src/renderer/hooks/` — Grouping by Business Domain
|
||||
|
||||
When `hooks/` exceeds 10 direct children, group hooks into business domain subdirectories. Generic hooks with no clear domain stay at the root. The root must stay ≤ 10 direct children.
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── agent/ # Agent/model related
|
||||
├── chat/ # Chat/message input
|
||||
├── file/ # File/workspace
|
||||
├── mcp/ # MCP related
|
||||
├── ui/ # Generic UI interaction
|
||||
├── system/ # System-level (deep link, notification, theme, etc.)
|
||||
└── index.ts # Public re-exports (optional)
|
||||
```
|
||||
|
||||
> Domain names are recommendations. Create new domains as needed following the same pattern.
|
||||
|
||||
### `src/renderer/utils/` — Grouping by Business Domain
|
||||
|
||||
Same principle as `hooks/`. When `utils/` exceeds 10 direct children, group into domain subdirectories. The root must stay ≤ 10 direct children.
|
||||
|
||||
```
|
||||
utils/
|
||||
├── file/ # File handling
|
||||
├── workspace/ # Workspace utilities
|
||||
├── chat/ # Chat/message utilities
|
||||
├── model/ # Model/agent utilities
|
||||
├── theme/ # Theme/style utilities
|
||||
├── ui/ # Generic UI utilities
|
||||
└── ... # Ungrouped utilities at root
|
||||
```
|
||||
|
||||
### Page Module Structure
|
||||
|
||||
```
|
||||
PageName/ # PascalCase
|
||||
├── index.tsx # Entry point (required)
|
||||
├── components/ # lowercase (categorical)
|
||||
├── hooks/ # lowercase (categorical)
|
||||
├── contexts/ # lowercase (categorical)
|
||||
├── utils/ # lowercase (categorical)
|
||||
├── types.ts
|
||||
└── constants.ts
|
||||
```
|
||||
|
||||
### Page-Level Directory Naming
|
||||
|
||||
Inside a page module (e.g., `pages/conversation/`), three types of subdirectories exist:
|
||||
|
||||
| Type | Convention | Examples |
|
||||
| ----------------------------------------------------- | ---------- | ---------------------------------------------------- |
|
||||
| **Categorical** (standard role) | lowercase | `components/`, `hooks/`, `context/`, `utils/` |
|
||||
| **Feature module** (business feature) | PascalCase | `GroupedHistory/`, `Workspace/`, `Preview/` |
|
||||
| **Platform directory** (mirrors `src/process/agent/`) | lowercase | `acp/`, `codex/`, `gemini/`, `nanobot/`, `openclaw/` |
|
||||
|
||||
Platform directories are an exception to PascalCase. They use lowercase for cross-process naming consistency with `src/process/agent/<platform>/`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ACP 图片输出展示
|
||||
|
||||
AionUi 对 ACP 工具调用中的图片结果使用文件路径展示,不依赖 inline base64。
|
||||
|
||||
## 数据约定
|
||||
|
||||
后端会把 Codex 图片生成工具的大型 `raw_output.result` base64 清洗掉,并保留小型字段:
|
||||
|
||||
- `rawOutput.image.path` 或 `raw_output.image.path`:生成图片的本地路径
|
||||
- `rawOutput.saved_path` 或 `raw_output.saved_path`:兼容 Codex 当前返回结构的本地路径
|
||||
- `rawOutput.result_omitted`:表示原始图片 base64 已被省略
|
||||
- `rawOutput.result_bytes`:被省略内容的原始字节长度
|
||||
|
||||
## 渲染流程
|
||||
|
||||
`MessageAcpToolCall` 优先读取 `image.path`,没有时回退到 `saved_path`。
|
||||
本地图片通过 `/api/fs/image-base64` 按需读取,再交给 `LocalImageView`
|
||||
渲染。这样可以避免聊天消息列表、WebSocket 事件和 SQLite 消息记录承载
|
||||
MB 级 base64。
|
||||
|
||||
对话列表会把工具调用聚合到 `View Steps` 摘要中。ACP 图片工具调用在进入
|
||||
`MessageToolGroupSummary` 前会通过标准化层保留 `image.path`,因此聚合视图
|
||||
展开后也能展示同一张本地图片,并提供下载按钮把图片保存到本机。
|
||||
|
||||
## 前端兜底
|
||||
|
||||
后端是主要清洗边界。前端在 `mergeAcpToolCallContent` 和消息列表快速合并路径中
|
||||
保留兜底清洗:如果收到包含 `saved_path` 且超出阈值的 `result` 字符串,会移除
|
||||
`result` 并补齐 `image.path`、`result_omitted` 和 `result_bytes`。
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
相关单测位于 `tests/unit/chat/`:
|
||||
|
||||
- `acpToolCallOutput.test.ts` 覆盖图片路径提取、文件名生成和 base64 清洗。
|
||||
- `messageHooks.dom.test.tsx` 覆盖空列表、非空列表和 `add=true` 路径下的 ACP 图片消息清洗。
|
||||
- `MessageAcpToolCall.dom.test.tsx` 与 `MessageToolGroupSummary.dom.test.tsx` 覆盖图片预览、下载成功调用和下载失败提示。
|
||||
@@ -0,0 +1,121 @@
|
||||
# CDP (Chrome DevTools Protocol) for MCP Development
|
||||
|
||||
AionUi supports CDP for external debugging tools integration. In development mode (`just dev`), CDP is enabled by default on port 9230.
|
||||
|
||||
## Enable CDP in Production
|
||||
|
||||
1. Open AionUi Settings → System → Developer Debug
|
||||
2. Enable "Enable Remote Debugging (CDP)"
|
||||
3. Restart the app
|
||||
|
||||
## Configure MCP chrome-devtools
|
||||
|
||||
Add this to your IDE's MCP configuration. The configuration file location depends on your IDE:
|
||||
|
||||
| IDE | Config Path |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Cursor** | `~/.cursor/mcp.json` |
|
||||
| **VS Code** | `~/.vscode/mcp.json` |
|
||||
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
|
||||
| **Codebuddy** | `~/.codebuddy/mcp.json` |
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "chrome-devtools-mcp@0.16.0", "--browser-url=http://127.0.0.1:9230"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Other AI-Friendly Development Tools
|
||||
|
||||
AionUi can integrate with other MCP tools for enhanced development experience:
|
||||
|
||||
| Tool | Purpose | Config |
|
||||
| ------------------ | --------------------------------------------------- | ----------------------------------------- |
|
||||
| **Playwright MCP** | Browser automation (alternative to chrome-devtools) | `"@playwright/mcp@latest"` |
|
||||
| **Puppeteer MCP** | Browser automation | `"@puppeteer/mcp@latest"` |
|
||||
| **Filesystem MCP** | File operations | `@modelcontextprotocol/server-filesystem` |
|
||||
| **Git MCP** | Git repository operations | `@modelcontextprotocol/server-git` |
|
||||
|
||||
See [MCP Servers](https://github.com/modelcontextprotocol/servers) for more tools.
|
||||
|
||||
## Usage with MCP
|
||||
|
||||
Once configured, you can use MCP tools to interact with AionUi:
|
||||
|
||||
- `list_pages` — List all open pages in AionUi
|
||||
- `take_snapshot` — Get accessibility tree snapshot of current page
|
||||
- `click`, `fill`, `hover` — Interact with UI elements
|
||||
- `navigate_page` — Navigate to URLs
|
||||
|
||||
## Inspect with Chrome DevTools
|
||||
|
||||
1. Open `http://127.0.0.1:9230/json` in Chrome
|
||||
2. Click on a page to inspect it with DevTools
|
||||
3. Or use Chrome's `chrome://inspect` → Configure → add `127.0.0.1:9230`
|
||||
|
||||
---
|
||||
|
||||
# CDP (Chrome DevTools Protocol) MCP 开发
|
||||
|
||||
AionUi 支持 CDP 用于外部调试工具集成。在开发模式 (`just dev`) 下,CDP 默认在端口 9230 启用。
|
||||
|
||||
## 在生产环境启用 CDP
|
||||
|
||||
1. 打开 AionUi 设置 → 系统 → 开发者调试
|
||||
2. 启用"启用远程调试 (CDP)"
|
||||
3. 重启应用
|
||||
|
||||
## 配置 MCP chrome-devtools
|
||||
|
||||
将以下配置添加到你的 IDE 的 MCP 配置文件中。配置文件位置取决于你使用的 IDE:
|
||||
|
||||
| IDE | 配置路径 |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Cursor** | `~/.cursor/mcp.json` |
|
||||
| **VS Code** | `~/.vscode/mcp.json` |
|
||||
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) 或 `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
|
||||
| **Codebuddy** | `~/.codebuddy/mcp.json` |
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "chrome-devtools-mcp@0.16.0", "--browser-url=http://127.0.0.1:9230"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 其他 AI 友好的开发工具
|
||||
|
||||
AionUi 可以集成其他 MCP 工具来增强开发体验:
|
||||
|
||||
| 工具 | 用途 | 配置 |
|
||||
| ------------------ | ---------------------------------------- | ----------------------------------------- |
|
||||
| **Playwright MCP** | 浏览器自动化(chrome-devtools 替代方案) | `"@playwright/mcp@latest"` |
|
||||
| **Puppeteer MCP** | 浏览器自动化 | `"@puppeteer/mcp@latest"` |
|
||||
| **Filesystem MCP** | 文件操作 | `@modelcontextprotocol/server-filesystem` |
|
||||
| **Git MCP** | Git 仓库操作 | `@modelcontextprotocol/server-git` |
|
||||
|
||||
更多工具请查看 [MCP Servers](https://github.com/modelcontextprotocol/servers)。
|
||||
|
||||
## MCP 使用方式
|
||||
|
||||
配置完成后,可以使用 MCP 工具与 AionUi 交互:
|
||||
|
||||
- `list_pages` — 列出 AionUi 中所有打开的页面
|
||||
- `take_snapshot` — 获取当前页面的可访问性树快照
|
||||
- `click`, `fill`, `hover` — 与 UI 元素交互
|
||||
- `navigate_page` — 导航到 URL
|
||||
|
||||
## 使用 Chrome DevTools 检查
|
||||
|
||||
1. 在 Chrome 中打开 `http://127.0.0.1:9230/json`
|
||||
2. 点击页面链接使用 DevTools 检查
|
||||
3. 或使用 Chrome 的 `chrome://inspect` → 配置 → 添加 `127.0.0.1:9230`
|
||||
@@ -0,0 +1,504 @@
|
||||
# AionUi Headless Server Deployment Guide
|
||||
|
||||
Deploy AionUi WebUI on headless Linux servers — cloud VMs, Kubernetes Pods, and containers — with proxy auto-fallback support.
|
||||
|
||||
**Translations**: [中文版](#中文版--chinese-version) below.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Virtual Display (Xvfb)](#virtual-display-xvfb)
|
||||
- [Service Management Script](#service-management-script)
|
||||
- [Remote Access](#remote-access)
|
||||
- [Proxy with Auto-Fallback](#proxy-with-auto-fallback)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux x86_64 (Ubuntu 20.04+ / Debian 11+ recommended)
|
||||
- At least 2GB RAM
|
||||
- AionUi `.deb` package from [Releases](https://github.com/iOfficeAI/AionUi/releases)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Download the latest .deb package
|
||||
wget https://github.com/iOfficeAI/AionUi/releases/latest/download/AionUi-linux-amd64.deb
|
||||
|
||||
# Install
|
||||
sudo dpkg -i AionUi-linux-amd64.deb
|
||||
sudo apt-get install -f # Fix missing dependencies
|
||||
```
|
||||
|
||||
> **Container note**: If you encounter dependency errors for `libegl1` / `libgles2` (common with NVIDIA runtime in containers), use `dpkg --force-all -i` to force install.
|
||||
|
||||
---
|
||||
|
||||
## Virtual Display (Xvfb)
|
||||
|
||||
AionUi is an Electron app and requires a display server. On headless servers (no monitor), use Xvfb to create a virtual display:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y xvfb
|
||||
```
|
||||
|
||||
Xvfb is used automatically by the startup script below via `xvfb-run`.
|
||||
|
||||
---
|
||||
|
||||
## Service Management Script
|
||||
|
||||
Since many cloud/container environments lack systemd, use the following nohup-based script.
|
||||
|
||||
Create `/opt/AionUi/start-aionui.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# AionUi WebUI headless startup script
|
||||
# Usage: ./start-aionui.sh [start|stop|restart|status]
|
||||
|
||||
PIDFILE="/var/run/aionui.pid"
|
||||
LOGFILE="/var/log/aionui.log"
|
||||
WORKDIR="$HOME" # Change to your workspace directory
|
||||
|
||||
start() {
|
||||
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi is already running (PID: $(cat $PIDFILE))"
|
||||
return 1
|
||||
fi
|
||||
echo "Starting AionUi WebUI..."
|
||||
cd "$WORKDIR"
|
||||
|
||||
nohup xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
|
||||
/usr/bin/AionUi --webui --remote --no-sandbox \
|
||||
> "$LOGFILE" 2>&1 &
|
||||
echo $! > "$PIDFILE"
|
||||
sleep 3
|
||||
if kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi started successfully (PID: $(cat $PIDFILE))"
|
||||
echo "WebUI: http://$(hostname -I | awk '{print $1}'):25808"
|
||||
else
|
||||
echo "AionUi failed to start. Check log: $LOGFILE"
|
||||
rm -f "$PIDFILE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [ ! -f "$PIDFILE" ]; then
|
||||
echo "AionUi is not running (no PID file)"
|
||||
return 1
|
||||
fi
|
||||
PID=$(cat "$PIDFILE")
|
||||
echo "Stopping AionUi (PID: $PID)..."
|
||||
kill "$PID" 2>/dev/null
|
||||
sleep 2
|
||||
kill -9 "$PID" 2>/dev/null
|
||||
pkill -f "AionUi --webui" 2>/dev/null
|
||||
rm -f "$PIDFILE"
|
||||
echo "AionUi stopped."
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
status() {
|
||||
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi is running (PID: $(cat $PIDFILE))"
|
||||
ss -tlnp | grep 25808
|
||||
else
|
||||
echo "AionUi is not running."
|
||||
rm -f "$PIDFILE" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart) restart ;;
|
||||
status) status ;;
|
||||
*) echo "Usage: $0 {start|stop|restart|status}" ;;
|
||||
esac
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x /opt/AionUi/start-aionui.sh
|
||||
```
|
||||
|
||||
> **Tip**: `WORKDIR` determines the directory AionUi can access for file operations. Set it to your project workspace.
|
||||
|
||||
---
|
||||
|
||||
## Remote Access
|
||||
|
||||
AionUi WebUI listens on port **25808**. Choose a method based on your network setup:
|
||||
|
||||
### Option A: Direct Access (Public IP)
|
||||
|
||||
Open port 25808 in your cloud provider's security group or firewall, then access via `http://YOUR_SERVER_IP:25808`.
|
||||
|
||||
### Option B: ngrok Tunnel (NAT / K8s / No Public IP)
|
||||
|
||||
```bash
|
||||
pip3 install pyngrok
|
||||
ngrok config add-authtoken YOUR_TOKEN
|
||||
|
||||
# Start tunnel
|
||||
nohup ngrok http 25808 --log=stdout > /var/log/ngrok.log 2>&1 &
|
||||
|
||||
# Get public URL
|
||||
curl -s http://127.0.0.1:4040/api/tunnels | python3 -c "
|
||||
import sys, json
|
||||
[print(t['public_url']) for t in json.load(sys.stdin)['tunnels']]
|
||||
"
|
||||
```
|
||||
|
||||
> Note: ngrok free tier generates a new URL on each restart. You can claim a free static domain at [ngrok dashboard](https://dashboard.ngrok.com/).
|
||||
|
||||
### Option C: SSH Tunnel (From Your Local Machine)
|
||||
|
||||
```bash
|
||||
ssh -L 25808:127.0.0.1:25808 user@YOUR_SERVER_IP
|
||||
# Then access: http://localhost:25808
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Proxy with Auto-Fallback
|
||||
|
||||
If your server needs a proxy for certain APIs (e.g., via an SSH reverse tunnel to a local VPN), use the **PAC auto-fallback** approach: try proxy first, fall back to direct connection when the proxy is unavailable. No restart needed.
|
||||
|
||||
### Step 1: SSH Reverse Tunnel (Run on Your Local Machine)
|
||||
|
||||
Forward your local proxy port to the server:
|
||||
|
||||
```bash
|
||||
ssh -R 7897:127.0.0.1:7897 user@YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
> Replace `7897` with your actual proxy port. The tunnel is active as long as the SSH session is open.
|
||||
|
||||
### Step 2: PAC File for AionUi (Electron / Chromium Layer)
|
||||
|
||||
Using `--proxy-server` is fragile — when the proxy goes down, **all** requests fail including the WebUI itself. Instead, use a **PAC (Proxy Auto-Configuration) file** that provides automatic fallback.
|
||||
|
||||
Create `/opt/AionUi/proxy.pac`:
|
||||
|
||||
```javascript
|
||||
function FindProxyForURL(url, host) {
|
||||
// Localhost and private networks: always direct
|
||||
if (
|
||||
isPlainHostName(host) ||
|
||||
host === '127.0.0.1' ||
|
||||
host === 'localhost' ||
|
||||
shExpMatch(host, '10.*') ||
|
||||
shExpMatch(host, '192.168.*') ||
|
||||
shExpMatch(host, '172.16.*')
|
||||
) {
|
||||
return 'DIRECT';
|
||||
}
|
||||
// All other requests: try proxy first, fallback to direct
|
||||
return 'PROXY 127.0.0.1:7897; DIRECT';
|
||||
}
|
||||
```
|
||||
|
||||
Then update the `nohup xvfb-run ...` line in your startup script:
|
||||
|
||||
```bash
|
||||
nohup xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
|
||||
/usr/bin/AionUi --webui --remote --no-sandbox \
|
||||
--proxy-pac-url="file:///opt/AionUi/proxy.pac" \
|
||||
> "$LOGFILE" 2>&1 &
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
|
||||
- Chromium natively supports PAC proxy rules
|
||||
- `"PROXY 127.0.0.1:7897; DIRECT"` means: try the proxy, and if it fails (connection refused / timeout), automatically fall back to a direct connection
|
||||
- Failover is per-request and real-time — no restart needed when the SSH tunnel connects or disconnects
|
||||
|
||||
### Step 3: Auto-Detect Proxy for Shell Commands
|
||||
|
||||
Shell tools like `curl` and `wget` use `http_proxy` environment variables. Add automatic detection to `~/.bashrc` so the proxy env vars are set/unset dynamically before every command:
|
||||
|
||||
```bash
|
||||
# === Proxy Auto-Detect ===
|
||||
_auto_proxy() {
|
||||
if (echo > /dev/tcp/127.0.0.1/7897) 2>/dev/null; then
|
||||
export http_proxy=http://127.0.0.1:7897
|
||||
export https_proxy=http://127.0.0.1:7897
|
||||
export ALL_PROXY=socks5://127.0.0.1:7897
|
||||
else
|
||||
unset http_proxy https_proxy ALL_PROXY 2>/dev/null
|
||||
fi
|
||||
}
|
||||
_auto_proxy
|
||||
PROMPT_COMMAND="_auto_proxy;${PROMPT_COMMAND}"
|
||||
# === Proxy Auto-Detect End ===
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
|
||||
- `PROMPT_COMMAND` runs before every shell prompt, re-checking proxy availability
|
||||
- SSH tunnel connected → proxy env vars set automatically
|
||||
- SSH tunnel disconnected → proxy env vars cleared, commands use direct connection
|
||||
- No manual intervention or terminal restart needed
|
||||
|
||||
### Step 4: AionUi Internal Proxy (Gemini API)
|
||||
|
||||
For Gemini API calls, configure the proxy inside AionUi WebUI:
|
||||
|
||||
**Settings → Gemini Settings → Proxy** → `http://127.0.0.1:7897`
|
||||
|
||||
> This proxy is handled by AionUi's Node.js layer (separate from the Chromium layer). When the SSH tunnel is down, Gemini API calls will fail, but the WebUI and other APIs remain functional.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
| ----------------------------------------- | ------------------------------------------------------------ |
|
||||
| `dpkg` dependency errors in containers | `dpkg --force-all -i AionUi-linux-amd64.deb` |
|
||||
| AionUi can only access `/tmp` | Set `WORKDIR` in the startup script to your workspace path |
|
||||
| WebUI not accessible remotely | Check firewall rules, or use ngrok / SSH tunnel |
|
||||
| All requests fail when proxy is down | Use PAC file (`--proxy-pac-url`) instead of `--proxy-server` |
|
||||
| `curl` fails after SSH tunnel disconnects | Add `PROMPT_COMMAND` auto-detect to `~/.bashrc` (see Step 3) |
|
||||
| Port 25808 already in use | `kill $(lsof -t -i:25808)` then restart |
|
||||
| Xvfb errors | `apt-get install -y xvfb libxkbcommon-x11-0` |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Headless Linux Server / Container │
|
||||
│ │
|
||||
│ start-aionui.sh │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ xvfb-run (virtual display) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ AionUi (Electron) │ │
|
||||
│ │ ├─ Chromium (port 25808) │ │
|
||||
│ │ │ └─ proxy.pac │──► PAC decides: │
|
||||
│ │ │ per-request │ PROXY or DIRECT │
|
||||
│ │ └─ Node.js (API calls) │ │
|
||||
│ └────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ SSH Reverse Tunnel │ │
|
||||
│ │ 127.0.0.1:7897 │ │
|
||||
│ │ (when available) │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────┴───────┐ │
|
||||
│ │ ngrok tunnel │ (optional, for public URL) │
|
||||
│ └────────────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# 中文版 / Chinese Version
|
||||
|
||||
# AionUi 无头服务器部署指南
|
||||
|
||||
在无图形界面的 Linux 服务器(云主机、K8s Pod、容器)上部署 AionUi WebUI,支持代理自动回退。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- Linux x86_64(推荐 Ubuntu 20.04+ / Debian 11+)
|
||||
- 至少 2GB 内存
|
||||
- AionUi `.deb` 安装包([下载地址](https://github.com/iOfficeAI/AionUi/releases))
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 下载最新 .deb 包
|
||||
wget https://github.com/iOfficeAI/AionUi/releases/latest/download/AionUi-linux-amd64.deb
|
||||
|
||||
# 安装
|
||||
sudo dpkg -i AionUi-linux-amd64.deb
|
||||
sudo apt-get install -f # 修复依赖
|
||||
```
|
||||
|
||||
> **容器环境**:若遇到 `libegl1` / `libgles2` 依赖错误(常见于 NVIDIA 运行时),可用 `dpkg --force-all -i` 强制安装。
|
||||
|
||||
## 虚拟显示 (Xvfb)
|
||||
|
||||
AionUi 是 Electron 应用,需要显示服务。无头服务器需安装 Xvfb:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y xvfb
|
||||
```
|
||||
|
||||
## 服务管理脚本
|
||||
|
||||
许多云/容器环境没有 systemd,使用以下基于 nohup 的管理脚本。
|
||||
|
||||
创建 `/opt/AionUi/start-aionui.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# AionUi WebUI 无头启动脚本
|
||||
# 用法: ./start-aionui.sh [start|stop|restart|status]
|
||||
|
||||
PIDFILE="/var/run/aionui.pid"
|
||||
LOGFILE="/var/log/aionui.log"
|
||||
WORKDIR="$HOME" # 改为你的工作目录
|
||||
|
||||
start() {
|
||||
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi 已在运行 (PID: $(cat $PIDFILE))"
|
||||
return 1
|
||||
fi
|
||||
echo "正在启动 AionUi WebUI..."
|
||||
cd "$WORKDIR"
|
||||
|
||||
nohup xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
|
||||
/usr/bin/AionUi --webui --remote --no-sandbox \
|
||||
> "$LOGFILE" 2>&1 &
|
||||
echo $! > "$PIDFILE"
|
||||
sleep 3
|
||||
if kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi 启动成功 (PID: $(cat $PIDFILE))"
|
||||
echo "WebUI: http://$(hostname -I | awk '{print $1}'):25808"
|
||||
else
|
||||
echo "AionUi 启动失败,请查看日志: $LOGFILE"
|
||||
rm -f "$PIDFILE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [ ! -f "$PIDFILE" ]; then
|
||||
echo "AionUi 未在运行"
|
||||
return 1
|
||||
fi
|
||||
PID=$(cat "$PIDFILE")
|
||||
echo "正在停止 AionUi (PID: $PID)..."
|
||||
kill "$PID" 2>/dev/null
|
||||
sleep 2
|
||||
kill -9 "$PID" 2>/dev/null
|
||||
pkill -f "AionUi --webui" 2>/dev/null
|
||||
rm -f "$PIDFILE"
|
||||
echo "AionUi 已停止。"
|
||||
}
|
||||
|
||||
restart() { stop; sleep 1; start; }
|
||||
|
||||
status() {
|
||||
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
|
||||
echo "AionUi 运行中 (PID: $(cat $PIDFILE))"
|
||||
ss -tlnp | grep 25808
|
||||
else
|
||||
echo "AionUi 未在运行。"
|
||||
rm -f "$PIDFILE" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;; stop) stop ;; restart) restart ;; status) status ;;
|
||||
*) echo "用法: $0 {start|stop|restart|status}" ;;
|
||||
esac
|
||||
```
|
||||
|
||||
## 远程访问
|
||||
|
||||
AionUi WebUI 监听端口 **25808**,根据网络环境选择访问方式:
|
||||
|
||||
| 方式 | 适用场景 | 命令 |
|
||||
| ---------- | --------------------- | ------------------------------------------ |
|
||||
| 直接访问 | 有公网 IP | 安全组开放 25808 端口 |
|
||||
| ngrok 穿透 | NAT / K8s / 无公网 IP | `ngrok http 25808` |
|
||||
| SSH 隧道 | 仅个人使用 | `ssh -L 25808:127.0.0.1:25808 user@server` |
|
||||
|
||||
## 代理自动回退
|
||||
|
||||
当服务器需要通过代理访问某些 API(如通过 SSH 反向隧道连接本地 VPN)时,使用 **PAC 自动回退**:代理可用时走代理,不可用时自动直连,无需重启。
|
||||
|
||||
### 第一步:SSH 反向隧道(本地电脑执行)
|
||||
|
||||
```bash
|
||||
ssh -R 7897:127.0.0.1:7897 user@YOUR_SERVER
|
||||
```
|
||||
|
||||
### 第二步:PAC 代理文件(AionUi Electron 层)
|
||||
|
||||
`--proxy-server` 的问题:代理一断,**所有请求**全挂。改用 PAC 文件实现自动回退。
|
||||
|
||||
创建 `/opt/AionUi/proxy.pac`:
|
||||
|
||||
```javascript
|
||||
function FindProxyForURL(url, host) {
|
||||
if (
|
||||
isPlainHostName(host) ||
|
||||
host === '127.0.0.1' ||
|
||||
host === 'localhost' ||
|
||||
shExpMatch(host, '10.*') ||
|
||||
shExpMatch(host, '192.168.*') ||
|
||||
shExpMatch(host, '172.16.*')
|
||||
) {
|
||||
return 'DIRECT';
|
||||
}
|
||||
return 'PROXY 127.0.0.1:7897; DIRECT';
|
||||
}
|
||||
```
|
||||
|
||||
启动脚本中添加参数:`--proxy-pac-url="file:///opt/AionUi/proxy.pac"`
|
||||
|
||||
**原理**:Chromium 原生支持 PAC,`PROXY ...; DIRECT` 表示先尝试代理,失败自动直连,每个请求实时判断。
|
||||
|
||||
### 第三步:Shell 命令代理自动检测
|
||||
|
||||
在 `~/.bashrc` 中添加,让 `curl` 等命令也能自动检测代理:
|
||||
|
||||
```bash
|
||||
# === Proxy Auto-Detect ===
|
||||
_auto_proxy() {
|
||||
if (echo > /dev/tcp/127.0.0.1/7897) 2>/dev/null; then
|
||||
export http_proxy=http://127.0.0.1:7897
|
||||
export https_proxy=http://127.0.0.1:7897
|
||||
export ALL_PROXY=socks5://127.0.0.1:7897
|
||||
else
|
||||
unset http_proxy https_proxy ALL_PROXY 2>/dev/null
|
||||
fi
|
||||
}
|
||||
_auto_proxy
|
||||
PROMPT_COMMAND="_auto_proxy;${PROMPT_COMMAND}"
|
||||
# === Proxy Auto-Detect End ===
|
||||
```
|
||||
|
||||
**原理**:`PROMPT_COMMAND` 在每次命令提示符前执行,自动检测代理端口是否可达,实时切换。
|
||||
|
||||
### 第四步:AionUi 内置代理(Gemini API)
|
||||
|
||||
在 WebUI 中设置:**Settings → Gemini Settings → Proxy** → `http://127.0.0.1:7897`
|
||||
|
||||
> 此代理由 Node.js 层处理,独立于 Chromium。隧道断开时仅 Gemini API 受影响。
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ---------------------- | ------------------------------------- |
|
||||
| 容器内 dpkg 依赖报错 | `dpkg --force-all -i` 强制安装 |
|
||||
| AionUi 只能访问 /tmp | 修改启动脚本中的 `WORKDIR` |
|
||||
| 远程无法访问 WebUI | 检查防火墙/安全组,或使用 ngrok |
|
||||
| 代理断开后所有请求失败 | 用 PAC 文件替代 `--proxy-server` |
|
||||
| SSH 断开后 curl 失败 | bashrc 添加 `PROMPT_COMMAND` 自动检测 |
|
||||
| 端口 25808 被占用 | `kill $(lsof -t -i:25808)` 后重启 |
|
||||
@@ -0,0 +1,207 @@
|
||||
# Hub Backend 测试指南
|
||||
|
||||
## 测试分层
|
||||
|
||||
| 层级 | 名称 | 测什么 | 关注点 | 运行环境 |
|
||||
| ---- | ---------------- | ------------------- | -------------- | ------------------- |
|
||||
| L1 | Integration Test | 主进程安装链路 | 数据流转正确性 | 本地 + CI |
|
||||
| L2 | E2E Test | UI 交互流程 | 用户体验正确性 | 本地(需 Electron) |
|
||||
| L3 | Smoke Test | 真实 Backend 连通性 | ACP 协议可用性 | 仅本地 |
|
||||
|
||||
---
|
||||
|
||||
## L1 Integration Test
|
||||
|
||||
**文件**: `tests/integration/hub-install-flow.test.ts`
|
||||
|
||||
**测试的完整链路**:
|
||||
|
||||
```
|
||||
HubIndexManager 加载 index
|
||||
→ HubInstaller.install() 下载解压
|
||||
→ lifecycle onInstall 执行(安装 CLI)
|
||||
→ ExtensionRegistry.hotReload()
|
||||
→ AcpDetector.refreshAll() 检测新 Backend
|
||||
→ AcpConnection.connect() 完成 handshake
|
||||
→ sendPrompt() 收到 response
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
|
||||
- Extension manifest 解析是否正确
|
||||
- onInstall 钩子是否成功安装 CLI
|
||||
- hotReload 后新 extension 是否被识别
|
||||
- AcpDetector 是否检测到新 Backend
|
||||
- ACP JSON-RPC 协议 handshake 是否完整
|
||||
- **集成链路测试**: 上述环节作为一条完整链路执行,而非逐步隔离
|
||||
|
||||
**使用的 fixture**:
|
||||
|
||||
- `tests/fixtures/fake-acp-cli/` — 最小 ACP JSON-RPC CLI,支持 initialize / session/new / session/prompt
|
||||
- `tests/fixtures/fake-extension/` — 测试用 extension,声明 acpAdapters,onInstall 钩子将 fake CLI 放到 PATH
|
||||
|
||||
**运行**:
|
||||
|
||||
```bash
|
||||
bun run test:integration
|
||||
# 或单独运行
|
||||
bunx vitest run tests/integration/hub-install-flow.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## L2 E2E Test
|
||||
|
||||
**文件**: `tests/e2e/specs/hub-backend-install.e2e.ts`
|
||||
|
||||
**测试的 UI 流程**:
|
||||
|
||||
```
|
||||
设置页 → Agent 页 → 本地 Agent Tab
|
||||
→ 点击"从市场安装"
|
||||
→ Hub 弹窗打开,列表加载
|
||||
→ 验证 card 状态(Install / Installed / Retry)
|
||||
→ 点击 Install,状态流转(Installing → Installed)
|
||||
→ 关闭弹窗,验证新 Backend 出现在列表
|
||||
→ 选择新 Backend,验证可发起会话
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
|
||||
- 用户操作路径是否完整覆盖
|
||||
- 状态展示是否正确(每张 card 单独验证)
|
||||
- 安装后列表是否自动刷新
|
||||
- 弹窗打开 / 关闭交互
|
||||
- 边界场景:install_failed 时的 Retry 按钮
|
||||
|
||||
**运行**:
|
||||
|
||||
```bash
|
||||
# 需要 Electron 环境
|
||||
bun run test:e2e
|
||||
# 或单独运行
|
||||
bunx playwright test tests/e2e/specs/hub-backend-install.e2e.ts --config playwright.config.ts
|
||||
```
|
||||
|
||||
> **注意**: L2 需要 Electron 二进制。如果 Electron 未安装,先运行 `node node_modules/electron/install.js`。
|
||||
|
||||
---
|
||||
|
||||
## L3 Smoke Test
|
||||
|
||||
**文件**: `tests/integration/acp-smoke.test.ts`
|
||||
|
||||
**测试流程**:
|
||||
|
||||
```
|
||||
检查 CLI 是否在 PATH 上
|
||||
→ 不存在则 skip(不 fail)
|
||||
→ 存在则 spawn CLI + ACP handshake
|
||||
→ initialize → session/new → session/prompt
|
||||
→ 验证收到 response chunk
|
||||
→ disconnect,验证进程正常退出
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
|
||||
- 真实 Backend CLI 的 ACP 协议兼容性
|
||||
- handshake 是否正常完成
|
||||
- 是否能收到流式 response
|
||||
- 进程是否正常退出,无残留
|
||||
|
||||
**覆盖的 Backend**:
|
||||
|
||||
| Backend | 命令 | ACP 参数 | 备注 |
|
||||
| ------------ | --------------- | -------- | --------------------- |
|
||||
| fake-acp-cli | `node index.js` | — | 始终运行 |
|
||||
| claude | `claude` | `--acp` | 需 `ACP_SMOKE_REAL=1` |
|
||||
| codex | `codex` | `--acp` | 需 `ACP_SMOKE_REAL=1` |
|
||||
| goose | `goose` | `acp` | 需 `ACP_SMOKE_REAL=1` |
|
||||
|
||||
**运行**:
|
||||
|
||||
```bash
|
||||
# 默认只跑 fake CLI(无需真实 Backend)
|
||||
bunx vitest run tests/integration/acp-smoke.test.ts
|
||||
|
||||
# 启用真实 Backend 冒烟(需要本地已安装对应 CLI + API key)
|
||||
ACP_SMOKE_REAL=1 bunx vitest run tests/integration/acp-smoke.test.ts
|
||||
```
|
||||
|
||||
> **注意**: L3 仅在本地运行,不上 CI。真实 Backend 测试需要本地安装 CLI 并配置好 API key。
|
||||
|
||||
---
|
||||
|
||||
## 添加新 Backend 时的测试清单
|
||||
|
||||
当 Hub 新增一个 Backend extension 时,按以下步骤验证:
|
||||
|
||||
### 1. L1 — 验证安装链路
|
||||
|
||||
无需修改测试代码。L1 使用 fixture extension 验证通用安装链路,与具体 Backend 无关。
|
||||
|
||||
### 2. L3 — 添加真实 Backend 冒烟
|
||||
|
||||
在 `tests/integration/acp-smoke.test.ts` 的 `realBackends` 数组中添加新 Backend:
|
||||
|
||||
```typescript
|
||||
const realBackends = [
|
||||
{ name: 'claude', cmd: 'claude', args: ['--acp'] },
|
||||
{ name: 'codex', cmd: 'codex', args: ['--acp'] },
|
||||
{ name: 'goose', cmd: 'goose', args: ['acp'] },
|
||||
// 新增:
|
||||
{ name: 'new-backend', cmd: 'new-backend', args: ['--acp'] },
|
||||
];
|
||||
```
|
||||
|
||||
然后本地运行:
|
||||
|
||||
```bash
|
||||
ACP_SMOKE_REAL=1 bunx vitest run tests/integration/acp-smoke.test.ts
|
||||
```
|
||||
|
||||
### 3. L2 — 验证 UI 流程
|
||||
|
||||
启动 dev 环境,手动走一遍 UI 流程确认无误后,运行 E2E 测试:
|
||||
|
||||
```bash
|
||||
bun run test:e2e
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 基础设施
|
||||
|
||||
### fake-acp-cli
|
||||
|
||||
**位置**: `tests/fixtures/fake-acp-cli/`
|
||||
|
||||
最小 ACP JSON-RPC 2.0 CLI 实现,通过 stdin/stdout 通信:
|
||||
|
||||
- `initialize` → 返回 capabilities + models
|
||||
- `session/new` → 返回 sessionId
|
||||
- `session/prompt` → 返回流式 text chunks + end_turn
|
||||
- `session/cancel` → 取消当前 prompt
|
||||
|
||||
用于 L1 和 L3(fake CLI 部分),避免依赖真实 Backend。
|
||||
|
||||
### fake-extension
|
||||
|
||||
**位置**: `tests/fixtures/fake-extension/`
|
||||
|
||||
测试用 extension:
|
||||
|
||||
- `aion-extension.json` — 声明 `contributes.acpAdapters`,声明 `lifecycle.onInstall`
|
||||
- `scripts/install.js` — onInstall 钩子,将 fake-acp-cli 放到临时 PATH(Unix: symlink, Windows: .cmd wrapper)
|
||||
|
||||
### 跨平台兼容性
|
||||
|
||||
所有测试已处理跨平台差异:
|
||||
|
||||
| 差异点 | 处理方式 |
|
||||
| ---------------------- | -------------------------------------- |
|
||||
| symlink 权限 (Windows) | Windows 用 .cmd wrapper 替代 |
|
||||
| shebang (Windows) | 统一用 `spawn('node', [path])` |
|
||||
| 进程信号 (Windows) | `child.kill()` 跨平台,SIGKILL 仅 Unix |
|
||||
| CLI 检测 | `where` (Windows) / `which` (Unix) |
|
||||
| 路径分隔符 | 统一用 `path.join()` + `os.tmpdir()` |
|
||||
@@ -0,0 +1,699 @@
|
||||
# AionUi WebUI Mode - Startup Guide
|
||||
|
||||
AionUi supports WebUI mode, allowing you to access the application through a web browser. This guide covers how to start WebUI mode on all supported platforms.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [What is WebUI Mode?](#what-is-webui-mode)
|
||||
- [Windows](#windows)
|
||||
- [macOS](#macos)
|
||||
- [Linux](#linux)
|
||||
- [Android (Termux)](#android-termux)
|
||||
- [Remote Access](#remote-access)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## What is WebUI Mode?
|
||||
|
||||
WebUI mode starts AionUi with an embedded web server, allowing you to:
|
||||
|
||||
- Access the application through any modern web browser
|
||||
- Use AionUi from remote devices on the same network (with `--remote` flag)
|
||||
- Run the application headless on servers
|
||||
|
||||
Default access URL: `http://localhost:3000` (port may vary, check the application output)
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
|
||||
### Method 1: Command Line (Recommended)
|
||||
|
||||
Open **Command Prompt** or **PowerShell** and run:
|
||||
|
||||
```cmd
|
||||
# Using full path
|
||||
"C:\Program Files\AionUi\AionUi.exe" --webui
|
||||
|
||||
# Or if AionUi is in your PATH
|
||||
AionUi.exe --webui
|
||||
```
|
||||
|
||||
### Method 2: Create a Desktop Shortcut
|
||||
|
||||
1. Right-click on desktop → **New** → **Shortcut**
|
||||
2. Enter target location:
|
||||
```
|
||||
"C:\Program Files\AionUi\AionUi.exe" --webui
|
||||
```
|
||||
3. Name it **AionUi WebUI**
|
||||
4. Click **Finish**
|
||||
5. Double-click the shortcut to launch
|
||||
|
||||
### Method 3: Create a Batch File
|
||||
|
||||
Create `start-aionui-webui.bat`:
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
"C:\Program Files\AionUi\AionUi.exe" --webui
|
||||
pause
|
||||
```
|
||||
|
||||
Double-click the batch file to start WebUI mode.
|
||||
|
||||
---
|
||||
|
||||
## macOS
|
||||
|
||||
### Method 1: Terminal Command (Recommended)
|
||||
|
||||
Open **Terminal** and run:
|
||||
|
||||
```bash
|
||||
# Using full path
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --webui
|
||||
|
||||
# Or using open command
|
||||
open -a AionUi --args --webui
|
||||
```
|
||||
|
||||
### Method 2: Create Shell Script
|
||||
|
||||
Create `start-aionui-webui.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --webui
|
||||
```
|
||||
|
||||
Make it executable and run:
|
||||
|
||||
```bash
|
||||
chmod +x start-aionui-webui.sh
|
||||
./start-aionui-webui.sh
|
||||
```
|
||||
|
||||
### Method 3: Create Automator Application
|
||||
|
||||
1. Open **Automator**
|
||||
2. Choose **Application**
|
||||
3. Add **Run Shell Script** action
|
||||
4. Enter:
|
||||
```bash
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --webui
|
||||
```
|
||||
5. Save as **AionUi WebUI.app**
|
||||
6. Double-click to launch
|
||||
|
||||
### Method 4: Add to Dock
|
||||
|
||||
1. Create an Automator app (Method 3)
|
||||
2. Drag **AionUi WebUI.app** to your Dock
|
||||
3. Click the Dock icon to start WebUI mode anytime
|
||||
|
||||
---
|
||||
|
||||
## Linux
|
||||
|
||||
### Method 1: Command Line (Recommended)
|
||||
|
||||
#### For .deb Installation
|
||||
|
||||
```bash
|
||||
# Using system path
|
||||
aionui --webui
|
||||
|
||||
# Or using full path
|
||||
/opt/AionUi/aionui --webui
|
||||
```
|
||||
|
||||
#### For AppImage
|
||||
|
||||
```bash
|
||||
# Make AppImage executable (first time only)
|
||||
chmod +x AionUi-*.AppImage
|
||||
|
||||
# Run with --webui flag
|
||||
./AionUi-*.AppImage --webui
|
||||
```
|
||||
|
||||
### Method 2: Create Desktop Entry
|
||||
|
||||
Create `~/.local/share/applications/aionui-webui.desktop`:
|
||||
|
||||
```ini
|
||||
[Desktop Entry]
|
||||
Name=AionUi WebUI
|
||||
Comment=Start AionUi in WebUI mode
|
||||
Exec=/opt/AionUi/aionui --webui
|
||||
Icon=aionui
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Office;
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x ~/.local/share/applications/aionui-webui.desktop
|
||||
```
|
||||
|
||||
The launcher will appear in your application menu.
|
||||
|
||||
### Method 3: Create Shell Script
|
||||
|
||||
Create `~/bin/start-aionui-webui.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
/opt/AionUi/aionui --webui
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x ~/bin/start-aionui-webui.sh
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
start-aionui-webui.sh
|
||||
```
|
||||
|
||||
### Method 4: Systemd Service (Background)
|
||||
|
||||
Create `/etc/systemd/system/aionui-webui.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AionUi WebUI Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=YOUR_USERNAME
|
||||
ExecStart=/opt/AionUi/aionui --webui --remote
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable aionui-webui.service
|
||||
sudo systemctl start aionui-webui.service
|
||||
|
||||
# Check status
|
||||
sudo systemctl status aionui-webui.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Android (Termux)
|
||||
|
||||
**Important Note**: Electron desktop mode is **not supported** on Android. However, you can run AionUi in WebUI mode using Termux with a prooted Linux environment.
|
||||
|
||||
> **Community Contribution**: This guide is contributed by [@Manamama](https://github.com/Manamama). Special thanks for making AionUi accessible on Android devices! 🙏
|
||||
>
|
||||
> **Original Tutorial**: [Running AionUi WebUI on Android via Termux + Proot Ubuntu](https://gist.github.com/Manamama/b4f903c279b5e73bdad4c2c0a58d5ddd)
|
||||
>
|
||||
> **Related Issues**: [#217 - Android Support Discussion](https://github.com/iOfficeAI/AionUi/issues/217)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Termux** from [F-Droid](https://f-droid.org/en/packages/com.termux/) (Google Play version is outdated and not recommended)
|
||||
- **~5 GB free storage**
|
||||
- **Internet connection**
|
||||
- **Android 7.0+** (tested on Android 14)
|
||||
|
||||
### Installation Steps
|
||||
|
||||
#### 1. Install Termux and Update Packages
|
||||
|
||||
```bash
|
||||
# Update package list
|
||||
pkg update -y
|
||||
|
||||
# Install proot-distro
|
||||
pkg install proot-distro -y
|
||||
```
|
||||
|
||||
#### 2. Install Ubuntu via Proot
|
||||
|
||||
```bash
|
||||
# Install Ubuntu rootfs
|
||||
proot-distro install ubuntu
|
||||
|
||||
# Login to Ubuntu environment
|
||||
proot-distro login ubuntu
|
||||
```
|
||||
|
||||
#### 3. Install System Dependencies
|
||||
|
||||
```bash
|
||||
# Update Ubuntu package list
|
||||
apt update
|
||||
|
||||
# Install required dependencies
|
||||
apt install -y \
|
||||
wget \
|
||||
libgtk-3-0 \
|
||||
libnss3 \
|
||||
libasound2 \
|
||||
libgbm1 \
|
||||
libxshmfence1 \
|
||||
ca-certificates
|
||||
|
||||
# Optional: Install additional libraries if needed
|
||||
apt install -y \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libatk1.0-0 \
|
||||
libcups2
|
||||
```
|
||||
|
||||
#### 4. Download and Install AionUi
|
||||
|
||||
```bash
|
||||
# Download the ARM64 .deb package (replace VERSION with the actual version)
|
||||
# Check latest version at: https://github.com/iOfficeAI/AionUi/releases
|
||||
wget https://github.com/iOfficeAI/AionUi/releases/download/vVERSION/AionUi_VERSION_arm64.deb
|
||||
|
||||
# Example (replace VERSION with the release tag, e.g. v1.5.2):
|
||||
wget https://github.com/iOfficeAI/AionUi/releases/download/vVERSION/AionUi_VERSION_arm64.deb
|
||||
|
||||
# Install the package
|
||||
apt install -y ./AionUi_*.deb
|
||||
|
||||
# Verify installation
|
||||
which AionUi
|
||||
```
|
||||
|
||||
#### 5. Launch AionUi WebUI
|
||||
|
||||
```bash
|
||||
# Start AionUi in WebUI mode with no-sandbox flag
|
||||
AionUi --no-sandbox --webui
|
||||
```
|
||||
|
||||
**Important**: The `--no-sandbox` flag is required in Termux/proot environments.
|
||||
|
||||
#### 6. Access the WebUI
|
||||
|
||||
Once started, open your browser and navigate to:
|
||||
|
||||
```
|
||||
http://localhost:25808
|
||||
```
|
||||
|
||||
**Note**: The default port is 25808. Check the terminal output if a different port is used.
|
||||
|
||||
### Expected Warnings (Non-Fatal)
|
||||
|
||||
You may see the following warnings in the terminal - these are normal and can be ignored:
|
||||
|
||||
```
|
||||
[WARNING] Could not connect to session bus: Using X11 for dbus-daemon autolaunch was disabled at compile time
|
||||
[ERROR] Failed to connect to the bus: Failed to connect to socket: No such file or directory
|
||||
[WARNING] Multiple instances of the app detected, but not running on display server
|
||||
```
|
||||
|
||||
These errors are related to D-Bus and X server, which are not needed for WebUI mode.
|
||||
|
||||
### Remote Access on LAN
|
||||
|
||||
To access AionUi from other devices on your local network:
|
||||
|
||||
```bash
|
||||
# Start with --remote flag
|
||||
AionUi --no-sandbox --webui --remote
|
||||
|
||||
# Find your Android device's IP address
|
||||
# In Termux (outside proot):
|
||||
# ifconfig or ip addr show
|
||||
```
|
||||
|
||||
Access from other devices: `http://YOUR_ANDROID_IP:25808`
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Port Already in Use
|
||||
|
||||
If port 25808 is occupied:
|
||||
|
||||
```bash
|
||||
# Specify a different port
|
||||
AionUi --no-sandbox --webui --port 8080
|
||||
```
|
||||
|
||||
#### Permission Denied Errors
|
||||
|
||||
```bash
|
||||
# Ensure the binary has execute permissions
|
||||
chmod +x /opt/AionUi/aionui
|
||||
```
|
||||
|
||||
#### Out of Memory
|
||||
|
||||
AionUi requires sufficient RAM. Close other apps if you encounter memory issues.
|
||||
|
||||
#### Cannot Access from Browser
|
||||
|
||||
1. Check if AionUi is running: look for "Server started" message
|
||||
2. Try using Termux's built-in browser or Chrome
|
||||
3. Clear browser cache
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Use a lightweight browser** - Chrome or Firefox Focus recommended
|
||||
2. **Close background apps** - Free up RAM for better performance
|
||||
3. **Use WiFi** - More stable than mobile data for remote access
|
||||
4. **Keep device charged** - Running AionUi consumes battery
|
||||
|
||||
### Tested Environment
|
||||
|
||||
- **Device**: Android 14
|
||||
- **Termux Version**: 0.118.0
|
||||
- **AionUi Version**: Latest release (e.g. 1.5.2)
|
||||
- **Proot-distro**: Ubuntu (latest)
|
||||
|
||||
### Creating a Startup Script
|
||||
|
||||
For convenience, create a script to launch AionUi quickly:
|
||||
|
||||
```bash
|
||||
# Create script in Ubuntu (proot)
|
||||
cat > ~/start-aionui.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "Starting AionUi WebUI..."
|
||||
AionUi --no-sandbox --webui --remote
|
||||
EOF
|
||||
|
||||
# Make executable
|
||||
chmod +x ~/start-aionui.sh
|
||||
|
||||
# Run anytime
|
||||
./start-aionui.sh
|
||||
```
|
||||
|
||||
### Quick Start Command (One-liner)
|
||||
|
||||
From Termux main shell:
|
||||
|
||||
```bash
|
||||
proot-distro login ubuntu -- bash -c "AionUi --no-sandbox --webui --remote"
|
||||
```
|
||||
|
||||
### Feedback and Improvements
|
||||
|
||||
If you encounter issues or have suggestions for improving Android support:
|
||||
|
||||
1. Check the [original community guide](https://gist.github.com/Manamama/b4f903c279b5e73bdad4c2c0a58d5ddd)
|
||||
2. Report issues at [GitHub Issues #217](https://github.com/iOfficeAI/AionUi/issues/217)
|
||||
3. Share your experience to help other Android users!
|
||||
|
||||
---
|
||||
|
||||
## Remote Access
|
||||
|
||||
To allow access from other devices on your network, use the `--remote` flag:
|
||||
|
||||
### Windows
|
||||
|
||||
```cmd
|
||||
AionUi.exe --webui --remote
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --webui --remote
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
aionui --webui --remote
|
||||
```
|
||||
|
||||
**Security Note**: Remote mode allows network access. Use only on trusted networks. Consider setting up authentication and firewall rules for production use.
|
||||
|
||||
### Finding Your Local IP Address
|
||||
|
||||
**Windows:**
|
||||
|
||||
```cmd
|
||||
ipconfig
|
||||
```
|
||||
|
||||
Look for "IPv4 Address" under your active network adapter.
|
||||
|
||||
**macOS/Linux:**
|
||||
|
||||
```bash
|
||||
ifconfig
|
||||
# or
|
||||
ip addr show
|
||||
```
|
||||
|
||||
Look for `inet` address (e.g., `192.168.1.100`).
|
||||
|
||||
Access from other devices: `http://YOUR_IP_ADDRESS:3000`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 3000 is already in use, the application will automatically try the next available port. Check the console output for the actual port number.
|
||||
|
||||
### Cannot Access from Browser
|
||||
|
||||
1. **Check if the application started successfully**
|
||||
- Look for "Server started on port XXXX" message in the console
|
||||
|
||||
2. **Try a different browser**
|
||||
- Chrome, Firefox, Safari, or Edge
|
||||
|
||||
3. **Clear browser cache**
|
||||
- Press `Ctrl+Shift+Delete` (Windows/Linux) or `Cmd+Shift+Delete` (macOS)
|
||||
|
||||
### Firewall Blocking Access
|
||||
|
||||
**Windows:**
|
||||
|
||||
```cmd
|
||||
# Allow through Windows Firewall
|
||||
netsh advfirewall firewall add rule name="AionUi WebUI" dir=in action=allow protocol=TCP localport=3000
|
||||
```
|
||||
|
||||
**Linux (UFW):**
|
||||
|
||||
```bash
|
||||
sudo ufw allow 3000/tcp
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
Go to **System Preferences** → **Security & Privacy** → **Firewall** → **Firewall Options** → Add AionUi
|
||||
|
||||
### Application Not Found
|
||||
|
||||
**Find application location:**
|
||||
|
||||
**Windows:**
|
||||
|
||||
```cmd
|
||||
where AionUi.exe
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
mdfind -name "AionUi.app"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
which aionui
|
||||
# or
|
||||
find /opt -name "aionui" 2>/dev/null
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\AionUi\AionUi.exe" --webui 2>&1 | Tee-Object -FilePath aionui.log
|
||||
```
|
||||
|
||||
**macOS/Linux:**
|
||||
|
||||
```bash
|
||||
/path/to/aionui --webui 2>&1 | tee aionui.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
You can customize WebUI behavior with environment variables:
|
||||
|
||||
```bash
|
||||
# Override the listening port
|
||||
export AIONUI_PORT=8080
|
||||
|
||||
# Allow remote access without passing --remote
|
||||
export AIONUI_ALLOW_REMOTE=true
|
||||
|
||||
# Optional host hint (0.0.0.0 behaves the same as AIONUI_ALLOW_REMOTE=true)
|
||||
export AIONUI_HOST=0.0.0.0
|
||||
|
||||
# Then start the application
|
||||
aionui --webui
|
||||
|
||||
# You can also pass the port directly via CLI
|
||||
aionui --webui --port 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Configuration File
|
||||
|
||||
From v1.5.0+, you can store persistent WebUI preferences in `webui.config.json` located in your Electron user-data folder:
|
||||
|
||||
| Platform | Location |
|
||||
| -------- | -------------------------------------------------------- |
|
||||
| Windows | `%APPDATA%/AionUi/webui.config.json` |
|
||||
| macOS | `~/Library/Application Support/AionUi/webui.config.json` |
|
||||
| Linux | `~/.config/AionUi/webui.config.json` |
|
||||
|
||||
Example file:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 8080,
|
||||
"allowRemote": true
|
||||
}
|
||||
```
|
||||
|
||||
Settings from CLI flags take priority, followed by environment variables, then the user config file.
|
||||
|
||||
---
|
||||
|
||||
## Command Line Options Summary
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------- |
|
||||
| `--webui` | Start in WebUI mode |
|
||||
| `--remote` | Allow remote network access |
|
||||
| `--webui --remote` | Combine both flags |
|
||||
|
||||
---
|
||||
|
||||
## Reset Admin Password
|
||||
|
||||
If you forgot your admin password in WebUI mode, you can reset it using the `--resetpass` command.
|
||||
|
||||
### Using --resetpass Command
|
||||
|
||||
**IMPORTANT:** The --resetpass command resets the password and generates a new random one. All existing JWT tokens will be invalidated.
|
||||
|
||||
**Windows:**
|
||||
|
||||
```cmd
|
||||
# Using full path
|
||||
"C:\Program Files\AionUi\AionUi.exe" --resetpass
|
||||
|
||||
# Or for a specific user
|
||||
"C:\Program Files\AionUi\AionUi.exe" --resetpass username
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
# Using full path
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --resetpass
|
||||
|
||||
# Or for a specific user
|
||||
/Applications/AionUi.app/Contents/MacOS/AionUi --resetpass username
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
# Using system path
|
||||
aionui --resetpass
|
||||
|
||||
# Or for a specific user
|
||||
aionui --resetpass username
|
||||
|
||||
# Or using full path
|
||||
/opt/AionUi/aionui --resetpass
|
||||
```
|
||||
|
||||
### What happens when you run --resetpass:
|
||||
|
||||
1. The command connects to the database
|
||||
2. Finds the specified user (default: `admin`)
|
||||
3. Generates a new random 12-character password
|
||||
4. Updates the password hash in the database
|
||||
5. Rotates the JWT secret (invalidating all previous tokens)
|
||||
6. Displays the new password in the terminal
|
||||
|
||||
### After running --resetpass:
|
||||
|
||||
1. The command will display your new password - **copy it immediately**
|
||||
2. Refresh your browser (Cmd+R or Ctrl+R)
|
||||
3. You will be redirected to the login page
|
||||
4. Login with the new password shown in the terminal
|
||||
|
||||
### Development Environment Only
|
||||
|
||||
If you're in a development environment with Node.js, you can also use:
|
||||
|
||||
```bash
|
||||
# In the project directory
|
||||
npm run resetpass
|
||||
|
||||
# Or for a specific user
|
||||
npm run resetpass -- username
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Main README](../readme.md)
|
||||
- [中文说明](./readme/readme_ch.md)
|
||||
- [日本語ドキュメント](./readme/readme_jp.md)
|
||||
- [GitHub Issues](https://github.com/iOfficeAI/AionUi/issues)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues:
|
||||
|
||||
1. Check the [Troubleshooting](#troubleshooting) section
|
||||
2. Search [existing issues](https://github.com/iOfficeAI/AionUi/issues)
|
||||
3. Create a [new issue](https://github.com/iOfficeAI/AionUi/issues/new) with:
|
||||
- Your OS and version
|
||||
- AionUi version
|
||||
- Steps to reproduce
|
||||
- Error messages or logs
|
||||
|
||||
---
|
||||
|
||||
**Happy using AionUi in WebUI mode!** 🚀
|
||||
@@ -0,0 +1,102 @@
|
||||
# ACP 单聊功能 PRD 索引
|
||||
|
||||
> 本目录下的文档由 PM 从 `prd-acp-scenarios.md` 抽取 ACP 单聊相关功能点,按模块拆分。
|
||||
> 排除了 F-TEAM-_(团队协作)和 F-CRON-_(定时任务)。
|
||||
|
||||
## 模块文件
|
||||
|
||||
| 模块文件 | 功能点数 | 编号范围 |
|
||||
| ---------------------------------- | -------- | ----------------------------------- |
|
||||
| [session.md](./session.md) | 10 | F-SESSION-01 ~ 10 |
|
||||
| [messaging.md](./messaging.md) | 9 | F-MSG-01 ~ 08 + F-FILE-02 |
|
||||
| [config.md](./config.md) | 10 | F-CONFIG-01 ~ 10 |
|
||||
| [permissions.md](./permissions.md) | 5 | F-PERM-01 ~ 04, 06 |
|
||||
| [display.md](./display.md) | 10 | F-DISPLAY-01 ~ 04, 06 ~ 07, 10 ~ 13 |
|
||||
| [reliability.md](./reliability.md) | 6 | F-RELIABILITY-01 ~ 02, 04 ~ 07 |
|
||||
| [skills.md](./skills.md) | 3 | F-SKILL-01 ~ 03 |
|
||||
|
||||
**总计:51 个独立功能点**(F-PERM-05 已合并至 F-PERM-03;F-DISPLAY-05/08/09/14、F-RELIABILITY-03/08/09 已合并至其他功能点)
|
||||
|
||||
## 功能点总表
|
||||
|
||||
| 编号 | 标题 | 状态 | 模块 | skip 白名单 |
|
||||
| ---------------- | ---------------------------- | -------- | ----------- | ------------------------------- |
|
||||
| F-SESSION-01 | 创建新会话 | 已实现 | session | |
|
||||
| F-SESSION-02 | 进入会话并建立连接 | 已实现 | session | |
|
||||
| F-SESSION-03 | 停止当前 AI 回复 | 已实现 | session | |
|
||||
| F-SESSION-04 | 意外断连自动处理 | 已实现 | session | |
|
||||
| F-SESSION-05 | 空闲会话自动释放 | 已实现 | session | |
|
||||
| F-SESSION-06 | 重置所有会话 | 已实现 | session | |
|
||||
| F-SESSION-07 | 删除会话 | 已实现 | session | |
|
||||
| F-SESSION-08 | 查看会话详情与状态 | 已实现 | session | |
|
||||
| F-SESSION-09 | 会话迁移 | 已实现 | session | |
|
||||
| F-SESSION-10 | AI 回复完成处理 | 已实现 | session | turn 完成边界待确认 |
|
||||
| F-MSG-01 | 发送文本消息 | 部分实现 | messaging | |
|
||||
| F-MSG-02 | 在消息中引用文件 | 部分实现 | messaging | |
|
||||
| F-MSG-03 | 首条消息自动注入 AI 规则 | 已实现 | messaging | |
|
||||
| F-MSG-04 | 隐藏消息与静默消息 | 已实现 | messaging | |
|
||||
| F-MSG-06 | 输入框历史记录 | 已实现 | messaging | |
|
||||
| F-MSG-07 | 重试与撤销上一轮对话 | 未实现 | messaging | skip: undo/redo 未实现 |
|
||||
| F-MSG-08 | `/btw` 追加上下文 | 部分实现 | messaging | |
|
||||
| F-FILE-02 | AI 读取和写入文件 | 已实现 | messaging | |
|
||||
| F-CONFIG-01 | 切换 AI 模型 | 已实现 | config | |
|
||||
| F-CONFIG-02 | 切换会话模式 | 已实现 | config | |
|
||||
| F-CONFIG-03 | 调整 AI 参数选项 | 已实现 | config | |
|
||||
| F-CONFIG-04 | 查看模型信息 | 已实现 | config | |
|
||||
| F-CONFIG-05 | 查看当前模式 | 已实现 | config | |
|
||||
| F-CONFIG-06 | AI 响应超时设置 | 已实现 | config | |
|
||||
| F-CONFIG-07 | 免确认模式的自动迁移 | 已实现 | config | |
|
||||
| F-CONFIG-08 | Codex 后端沙盒安全级别联动 | 已实现 | config | |
|
||||
| F-CONFIG-09 | 配置自动保存与恢复 | 已实现 | config | |
|
||||
| F-CONFIG-10 | 后端能力信息缓存 | 已实现 | config | |
|
||||
| F-PERM-01 | AI 操作权限审批 | 部分实现 | permissions | skip: 30 分钟超时自动拒绝未实现 |
|
||||
| F-PERM-02 | 权限确认操作 | 已实现 | permissions | |
|
||||
| F-PERM-03 | 免确认模式 / YOLO 模式 | 已实现 | permissions | skip: codebuddy 差异待研发确认 |
|
||||
| F-PERM-04 | 查看待确认操作列表 | 已实现 | permissions | |
|
||||
| F-PERM-06 | 会话创建时的模式与权限初始化 | 已实现 | permissions | |
|
||||
| F-DISPLAY-01 | AI 回复实时逐字显示 | 已实现 | display | |
|
||||
| F-DISPLAY-02 | AI 思考过程展示 | 已实现 | display | |
|
||||
| F-DISPLAY-03 | AI 工具调用展示 | 已实现 | display | |
|
||||
| F-DISPLAY-04 | AI 执行计划展示 | 已实现 | display | |
|
||||
| F-DISPLAY-06 | 网页预览打开 | 已实现 | display | |
|
||||
| F-DISPLAY-07 | 上下文用量展示 | 已实现 | display | skip: token 统计双路径待确认 |
|
||||
| F-DISPLAY-10 | 斜杠命令列表 | 已实现 | display | |
|
||||
| F-DISPLAY-11 | 请求追踪信息 | 已实现 | display | |
|
||||
| F-DISPLAY-12 | 环境检查与 AI 后端健康检查 | 部分实现 | display | |
|
||||
| F-DISPLAY-13 | 可用 AI 后端列表 | 已实现 | display | |
|
||||
| F-RELIABILITY-01 | 连接超时自动处理 | 已实现 | reliability | |
|
||||
| F-RELIABILITY-02 | AI 回复超时自动处理 | 已实现 | reliability | |
|
||||
| F-RELIABILITY-04 | 启动失败友好提示 | 已实现 | reliability | |
|
||||
| F-RELIABILITY-05 | 本地缓存损坏自动修复 | 已实现 | reliability | |
|
||||
| F-RELIABILITY-06 | 多候选安装策略 | 未实现 | reliability | skip: 架构改变后可能不适用 |
|
||||
| F-RELIABILITY-07 | 发送消息异常恢复 | 已实现 | reliability | |
|
||||
| F-SKILL-01 | AI 技能自动发现与注入 | 已实现 | skills | |
|
||||
| F-SKILL-02 | 指定技能注入(高级模式) | 已实现 | skills | |
|
||||
| F-SKILL-03 | MCP 工具服务注入 | 部分实现 | skills | |
|
||||
|
||||
## 状态统计
|
||||
|
||||
| 状态 | 数量 |
|
||||
| -------- | ------ |
|
||||
| 已实现 | 43 |
|
||||
| 部分实现 | 6 |
|
||||
| 未实现 | 2 |
|
||||
| **合计** | **51** |
|
||||
|
||||
> 注:源 PRD 中标注 57 个功能点(含 F-TEAM-_ 3 个 + F-CRON-_ 2 个 + 已合并功能点编号)。本索引仅列出 ACP 单聊范围内的独立功能点。
|
||||
|
||||
## skip 白名单
|
||||
|
||||
以下功能点或子场景在 E2E 测试中标记为 skip:
|
||||
|
||||
| 功能点 | skip 原因 |
|
||||
| -------------------------------------- | -------------------------------- |
|
||||
| F-MSG-07 | undo/redo 未实现 |
|
||||
| F-RELIABILITY-06 | 多候选安装,架构改变后可能不适用 |
|
||||
| F-PERM-01 的 30 分钟超时自动拒绝 | 未实现 |
|
||||
| F-PERM-03 的 codebuddy 差异 | 待研发确认 |
|
||||
| F-DISPLAY-07 的 token 统计双路径 | 待确认 |
|
||||
| F-SESSION-10 的 turn 完成边界 | 待确认 |
|
||||
| F-SESSION-05 的实际 5 分钟空闲触发验证 | E2E 等待成本过高 |
|
||||
| 所有首次认证流程 | 用户跳过 |
|
||||
| 所有 Gemini 专属差异 | 用户跳过 |
|
||||
@@ -0,0 +1,267 @@
|
||||
# ACP Agent Skill 发现机制调研
|
||||
|
||||
调研日期:2026-06-29
|
||||
|
||||
## 调研范围
|
||||
|
||||
本文调研 `agent_metadata` 表中 `agent_type = 'acp'` 的 agent,重点确认:
|
||||
|
||||
- AionCore 如何判断 ACP agent 是否支持原生 skill 目录。
|
||||
- agent 在什么时候扫描 skill 目录。
|
||||
- 会话开始后临时新增 skill 是否可以被当前会话识别。
|
||||
|
||||
简要结论:
|
||||
|
||||
- ACP 协议本身没有定义标准的 skill 加载方法。
|
||||
- AionCore 通过 `agent_metadata.native_skills_dirs` 判断 ACP backend 是否走原生 skill 目录。
|
||||
- 会话中新增 skill 是否可用,取决于底层 agent CLI 自己的扫描或热加载能力。
|
||||
- AionCore 当前只会重新链接会话创建时写入 `extra.skills` 的 skill 快照,因此产品层还没有完整支持“会话中由用户临时新增 skill”。
|
||||
|
||||
## ACP 协议边界
|
||||
|
||||
ACP v1 定义了 `session/new`、`session/load`、`session/resume`、`session/prompt` 等会话生命周期方法。`session/new` 和 `session/resume` 可以携带 workspace root、MCP servers 等上下文,但协议里没有一等的 `skill` 对象,也没有标准的“加载 skill”方法。
|
||||
|
||||
ACP 支持通过 `_meta` 字段和自定义扩展方法做 vendor 扩展,也支持 agent 通过 `available_commands_update` 更新 slash commands。它们是通用扩展点,不是跨 agent 可移植的 skill 加载协议。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://agentclientprotocol.com/protocol/v1/schema
|
||||
- https://agentclientprotocol.com/get-started/introduction
|
||||
|
||||
## AionCore 当前链路
|
||||
|
||||
### 判断来源
|
||||
|
||||
`agent_metadata.native_skills_dirs` 是 ACP backend 是否使用原生 skill 目录的开关。
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-db/migrations/001_initial_schema.sql`
|
||||
- Lines 153-168 定义 `native_skills_dirs` 字段。
|
||||
- Lines 191-327 seed 初始 ACP builtin agent。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/service.rs`
|
||||
- Lines 3210-3233:ACP agent 从 `agent_metadata` 查询 native skill dirs。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-common/src/enums.rs`
|
||||
- Lines 68-93:注释明确说明 ACP vendors 的 skill dirs 由 `agent_metadata` 管理;非 ACP 的 `Aionrs` 使用 `.aionrs/skills`。
|
||||
|
||||
### 会话创建时链接
|
||||
|
||||
创建会话时,AionCore 会计算 `initial_skills`,写入 `extra.skills`,并且只在 `initial_skills` 非空时把 skill 链接到 workspace 中。
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/service.rs`
|
||||
- Lines 884-885:计算 `initial_skills`。
|
||||
- Lines 887-919:对支持原生 skill 目录的 agent 创建 workspace skill 链接。
|
||||
- Lines 922-927:把 `extra.skills` 写入会话。
|
||||
|
||||
这意味着:如果初始 skill 为空,AionCore 当前不会主动创建顶层 native skill 目录,例如 `.claude/skills`。
|
||||
|
||||
### 每轮发送前重新链接
|
||||
|
||||
发送消息和 warmup 前,AionCore 会重新确保 workspace skill links 存在,但它读取的是 session context 中的 skill 列表,而这个列表来自不可变的 `extra.skills` 快照。
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/session_context.rs`
|
||||
- Lines 68-72:从 `extra.skills` 解析 skill 列表。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/service.rs`
|
||||
- Lines 2530-2556:发送消息前调用 `ensure_workspace_skill_links`。
|
||||
- Lines 2919-2971:重新链接 `context.skills`。
|
||||
- Lines 3206-3208:`context_skill_names(context)` 只返回 `context.skills`。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/service.rs`
|
||||
- Lines 1740-1752:拒绝会话创建后修改 `extra.skills`。
|
||||
|
||||
### Prompt injection fallback
|
||||
|
||||
如果 ACP agent 没有配置 native skill dirs,AionCore 会走 prompt injection。
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-ai-agent/src/capability/first_message_injector.rs`
|
||||
- Lines 12-21:说明 native support 和 injected skills 的区别。
|
||||
- Lines 24-30:native skill discovery 使用 light mode;否则使用 heavy mode 注入 skill index。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/stream_relay.rs`
|
||||
- Lines 784-801:skill body 加载请求会被过滤到允许的 skill 列表内。
|
||||
- `/Users/zhoukai/Documents/github/aioncore/crates/aionui-conversation/src/turn_orchestrator.rs`
|
||||
- Lines 309-317:`inject_skills` 会进入当前 turn,但 `allowed_skill_names` 仍然来自 context 快照。
|
||||
|
||||
## Builtin ACP Agent 清单
|
||||
|
||||
最终 builtin ACP 行来自 `001_initial_schema.sql` 和 `011_add_openclaw_acp_agent.sql`。后续迁移调整了 command 和 capability,但没有清空 seed 中已有的 `native_skills_dirs`。
|
||||
|
||||
| Backend | Agent | `native_skills_dirs` | 外部资料确认 | 当前会话新增 skill |
|
||||
| ----------- | ----------- | -------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
|
||||
| `claude` | Claude Code | `.claude/skills` | Claude Code 文档确认 | 支持,但要求顶层 `.claude/skills` 在 session 启动时已存在 |
|
||||
| `codex` | Codex CLI | `.codex/skills` | OpenAI 当前文档写的是 `.agents/skills`,不是 `.codex/skills` | 路径可能不匹配,需要结合 ACP adapter 和 Codex 版本复核 |
|
||||
| `gemini` | Gemini CLI | `.gemini/skills` | Gemini 文档确认 `.gemini/skills` 和 `/skills reload` | 可以支持,但通常需要触发 `/skills reload` |
|
||||
| `qwen` | Qwen | `.qwen/skills` | Qwen 文档确认 `.qwen/skills` | 文档说明需要重启,不能承诺当前会话原生热加载 |
|
||||
| `codebuddy` | CodeBuddy | `.codebuddy/skills` | CodeBuddy 文档确认 `.codebuddy/skills` | 未确认热加载;文档只说明 `/skills` 展示已加载 skill |
|
||||
| `droid` | Droid | `.factory/skills` | Factory 文档确认 `.factory/skills` | 未确认热加载;文档只说明发现和调用方式 |
|
||||
| `goose` | Goose | `.goose/skills` | 本轮未确认 | 未知 |
|
||||
| `kimi` | Kimi | `.kimi/skills` | 本轮未确认 | 未知 |
|
||||
| `opencode` | OpenCode | `.opencode/skills` | OpenCode 文档确认 `.opencode/skills`、`.claude/skills`、`.agents/skills` | 未确认热加载;文档只说明发现和按需加载 |
|
||||
| `vibe` | Vibe | `.vibe/skills` | Mistral 文档确认 `.vibe/skills` 和 `.agents/skills` | 未确认热加载;文档只说明发现路径 |
|
||||
| `cursor` | Cursor | `.cursor/skills` | 本轮未确认 | 未知 |
|
||||
| `auggie` | Auggie | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `copilot` | Copilot | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `qoder` | Qoder | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `kiro` | Kiro | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `hermes` | Hermes | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `snow` | Snow | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
| `openclaw` | OpenClaw | `NULL` | AionCore 行没有 native dir | 只能走 prompt injection |
|
||||
|
||||
## Agent 扫描 skill 目录的时机
|
||||
|
||||
### Claude Code
|
||||
|
||||
Claude Code 是最明确支持会话中插入 skill 的 agent。
|
||||
|
||||
官方文档说明:Claude Code 会自动监听所有 skill 目录中的 `SKILL.md` 变化;如果在 `~/.claude/skills/`、项目 `.claude/skills/` 或通过 `--add-dir` 加入的 `.claude/skills/` 中新增、修改、删除 skill,变更会在当前 session 生效。文档同时说明:如果 Claude Code 启动时顶层 skill 目录不存在,启动后再创建这个顶层目录需要重启 Claude Code 才能被检测到。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- 如果 `.claude/skills` 在 Claude session 启动前已经存在,那么下一轮 prompt 前插入新的 skill 软链,理论上可以在当前会话生效。
|
||||
- 如果会话初始没有 skill,AionCore 没有创建 `.claude/skills`,之后再创建这个顶层目录可能不会被 Claude Code watcher 发现,需要重启。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://code.claude.com/docs/en/skills
|
||||
|
||||
### Codex CLI
|
||||
|
||||
OpenAI 当前 Codex skills 文档说明:Codex 会自动检测 skill 变化,支持 symlinked skill folders,并扫描 repo、user、admin、system 等位置。当前公开文档中的 repo 路径是 `.agents/skills`,不是 `.codex/skills`。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- 当前 builtin `codex` 行配置的是 `.codex/skills`,这和 OpenAI 当前公开文档不一致。
|
||||
- 如果要支持 Codex 原生 skill,建议补充 `.agents/skills`,或者先确认 `@zed-industries/codex-acp` 对应 Codex 版本实际读取的目录。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://developers.openai.com/codex/skills
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Gemini CLI 文档说明:skills 在 session start 时从 built-in、extension、user、workspace 四个层级发现。workspace skills 可以放在 `.gemini/skills/` 或 `.agents/skills/`。getting-started 文档说明 `/skills reload` 可以在不重启 session 的情况下刷新 skill 列表。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- 链接到 `.gemini/skills` 是可行的。
|
||||
- 如果要在当前会话新增 skill,需要触发 `/skills reload`,或者引导 agent/user 执行 reload。
|
||||
- 可以考虑把 `.agents/skills` 也加入 Gemini 的 native dirs,提升兼容性。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://geminicli.com/docs/cli/skills/
|
||||
- https://geminicli.com/docs/cli/tutorials/skills-getting-started/
|
||||
|
||||
### Qwen Code
|
||||
|
||||
Qwen Code 文档说明:个人 skills 位于 `~/.qwen/skills/`,项目 skills 位于 `.qwen/skills/`,skill 变更会在下一次 Qwen Code 启动时生效;如果 Qwen Code 已在运行,需要重启才能加载更新。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- 软链插入可以为下一次 session 准备 skill 文件。
|
||||
- 它不能可靠支持同一会话内的原生热加载;如果要临时可用,应走 prompt injection 或新建会话。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/
|
||||
|
||||
### CodeBuddy
|
||||
|
||||
CodeBuddy 文档说明:项目 skills 位于 `.codebuddy/skills/`,用户 skills 位于 `~/.codebuddy/skills/`;skill 可以由 AI 自动选择,也可以手动调用,`/skills` 会展示当前加载的 skills。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- AionCore 当前 `.codebuddy/skills` 配置与文档一致。
|
||||
- 本轮查到的文档没有说明运行中新增 skill 是否会被自动检测。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://www.codebuddy.ai/docs/cli/skills
|
||||
|
||||
### Droid
|
||||
|
||||
Factory Droid 文档说明:skills 位于 `<repo>/.factory/skills/`、`~/.factory/skills/`,也兼容 `<repo>/.agent/skills/`。skill 可以通过 `/skill-name` 调用,也可以在相关任务中自动触发。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- AionCore 当前 `.factory/skills` 配置与文档一致。
|
||||
- 本轮查到的文档没有说明运行中新增 skill 是否会被自动检测。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://docs.factory.ai/cli/configuration/skills
|
||||
|
||||
### OpenCode
|
||||
|
||||
OpenCode 文档说明:会查找项目 `.opencode/skills/<name>/SKILL.md`、global `~/.config/opencode/skills`、Claude-compatible `.claude/skills` 和 agent-compatible `.agents/skills`。它会从当前工作目录向上查找到 git worktree。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- AionCore 当前 `.opencode/skills` 配置匹配其中一个文档路径。
|
||||
- 可以考虑额外加入 `.agents/skills` 或 `.claude/skills` 提升兼容性。
|
||||
- 本轮查到的文档没有说明 live watcher 或 reload 行为。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://opencode.ai/docs/skills/
|
||||
|
||||
### Vibe
|
||||
|
||||
Mistral Vibe 文档说明:CLI 会从 `skill_paths`、受信任目录中的项目 `./.vibe/skills/` 或 `./.agents/skills/`、以及用户 `~/.vibe/skills/` 发现 skills。
|
||||
|
||||
对 AionCore 的含义:
|
||||
|
||||
- AionCore 当前 `.vibe/skills` 配置与文档一致。
|
||||
- 可以考虑额外加入 `.agents/skills` 提升兼容性。
|
||||
- 本轮查到的文档没有说明 live watcher 或 reload 行为。
|
||||
|
||||
资料来源:
|
||||
|
||||
- https://docs.mistral.ai/vibe/code/cli/skills
|
||||
|
||||
## 当前产品缺口
|
||||
|
||||
针对 issue 3455 中“会话开始后添加新 skill”的问题,底层能力是部分具备的,但产品链路还没闭合。
|
||||
|
||||
已经具备:
|
||||
|
||||
- AionCore 可以通过 `link_workspace_skills` 在 workspace 中创建 skill 软链。
|
||||
- AionCore 会在发送消息和 warmup 前重新确保创建时快照里的 skill links。
|
||||
- AionUi 的 send message 参数里已经有 `inject_skills` 字段。
|
||||
|
||||
缺口:
|
||||
|
||||
- `extra.skills` 创建后不可修改。
|
||||
- `ensure_workspace_skill_links` 当前忽略 `request.inject_skills`。
|
||||
- `[LOAD_SKILL: ...]` middleware 的 `allowed_skill_names` 当前忽略 `request.inject_skills`。
|
||||
- 初始 skill 为空时,不会预创建 native 顶层目录。
|
||||
- 没有建模 agent-specific reload policy:Claude 可以监听已存在目录,Gemini 有 `/skills reload`,Qwen 需要重启。
|
||||
|
||||
## 支持建议
|
||||
|
||||
可以支持“会话中临时添加 skill”,但应该按 agent capability 处理,而不是作为 ACP 通用能力承诺。
|
||||
|
||||
1. 会话创建时,只要 agent 有 `native_skills_dirs`,即使 `initial_skills` 为空,也预创建 native skill 目录。这是满足 Claude Code watcher 规则的关键。
|
||||
2. 每次发送消息时,把 `conversation.extra.skills` 和 `request.inject_skills` 合并,并在 dispatch prompt 前把合并后的 skill set 链接到 workspace。
|
||||
3. 把 `request.inject_skills` 纳入 `allowed_skill_names`,让 middleware 可以加载本轮临时注入 skill 的正文。
|
||||
4. 增加 per-backend reload policy:
|
||||
- `claude`:prompt 前完成软链即可;前提是顶层目录在 session 启动时已存在。
|
||||
- `gemini`:prompt 前完成软链,并触发或提示 `/skills reload`。
|
||||
- `qwen`:不要承诺同会话原生热加载;用 prompt injection 或新会话。
|
||||
- 未确认热加载的 native agent:可以创建软链,但 UI/能力矩阵里标记为 best-effort。
|
||||
5. 考虑更新 `agent_metadata.native_skills_dirs`,补充已被文档确认的兼容路径:
|
||||
- Codex:`.agents/skills`
|
||||
- Gemini:`.gemini/skills`、`.agents/skills`
|
||||
- OpenCode:`.opencode/skills`、`.agents/skills`,可选 `.claude/skills`
|
||||
- Vibe:`.vibe/skills`、`.agents/skills`
|
||||
- Droid:`.factory/skills`,可选 `.agent/skills`
|
||||
|
||||
## 结论
|
||||
|
||||
对 Claude Code 来说,答案基本是“可以支持”:只要 AionCore 在 session 启动前确保 `.claude/skills` 存在,那么会话中插入新的 skill 目录或软链,应该能被当前 session 识别。
|
||||
|
||||
对整个 ACP agent 集合来说,不能说 ACP 本身支持动态 skill 加载。AionCore 能在会话中创建文件和软链,但最终是否生效由各 agent 的扫描机制决定。产品上应该做 capability matrix,而不是把它描述为 ACP 的统一特性。
|
||||
@@ -0,0 +1,248 @@
|
||||
# ACP 单聊 - AI 配置 (F-CONFIG)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-CFG-01 ~ S-CFG-11
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-01) 切换 AI 模型 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在对话中随时切换 AI 模型,以便根据任务复杂度选用最合适的模型。
|
||||
|
||||
**前置条件**:已打开一个 AI 会话
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击会话界面中的模型选择控件 -> 看到当前可用模型列表
|
||||
2. 用户选择目标模型 -> 系统提示模型切换中
|
||||
3. 切换成功后,模型选择控件更新为新模型名称
|
||||
4. 用户继续发送消息 -> 系统使用新选择的模型进行回复
|
||||
5. 关闭并重新打开该会话 -> 模型仍然是用户之前选择的模型(自动记忆)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 选择的模型不可用(已下线或无渠道):用户看到错误提示,系统自动回退到默认模型
|
||||
- AI 会话尚未完成初始化(如首次打开时切换过快):系统自动等待初始化完成后再执行切换
|
||||
- 切换失败:模型选择控件保持原模型不变,用户看到失败提示
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Claude 后端:切换模型后,下一条消息中 AI 会收到一条系统提醒通知其模型已变更,确保后续对话使用新选择的模型
|
||||
- 其他后端:切换立即生效,无额外提醒
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户可以在模型列表中看到当前后端所有可用模型
|
||||
- [ ] 切换模型后,后续对话确实使用新模型
|
||||
- [ ] 模型选择在会话关闭重开后仍然保持
|
||||
- [ ] 当所选模型不可用时,用户收到明确的错误提示并自动回退
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-02) 切换会话模式 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在对话中切换工作模式(如"规划模式"或"免确认模式"),以便让 AI 按不同策略工作。
|
||||
|
||||
**前置条件**:已打开一个 AI 会话
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击会话中的模式切换控件 -> 看到可用模式列表(如:默认模式、规划模式、免确认模式等)
|
||||
2. 用户选择目标模式 -> 系统提示模式切换中
|
||||
3. 切换成功后,模式控件更新为新模式
|
||||
4. 关闭并重新打开该会话 -> 模式仍然是用户之前选择的模式(自动记忆)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 会话尚未初始化:系统自动等待初始化完成后再执行切换
|
||||
- 切换失败:用户看到失败提示,模式保持不变
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Codex 后端:模式切换在本地完成,同时影响其沙盒安全级别
|
||||
- 其他后端:模式切换直接发送给 AI 后端
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户可以在模式列表中看到当前后端支持的所有模式
|
||||
- [ ] 切换模式后,AI 的行为符合所选模式的预期(如规划模式下 AI 先制定计划再执行)
|
||||
- [ ] 模式选择在会话关闭重开后仍然保持
|
||||
- [ ] 从免确认模式切回普通模式后,AI 操作需要重新逐一确认
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-03) 调整 AI 参数选项 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望调整 AI 的参数选项(如推理深度等),以便微调 AI 的行为表现。
|
||||
|
||||
**前置条件**:已打开一个 AI 会话,且 AI 后端支持参数配置
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户打开参数设置面板 -> 看到当前后端提供的可调参数列表(不包含模型和模式,它们有专门的控件)
|
||||
2. 用户修改某个参数的值 -> 系统立即应用
|
||||
3. 关闭并重新打开该会话 -> 参数设置仍然是用户之前调整的值(自动记忆)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 会话尚未初始化:系统自动等待初始化完成后再执行设置
|
||||
- 后端不支持任何参数配置:参数面板显示为空或不可用
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户看到的参数列表不包含"模型"和"模式"(这两项有专门的交互控件)
|
||||
- [ ] 参数修改后立即生效
|
||||
- [ ] 参数设置在会话关闭重开后仍然保持
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-04) 查看模型信息 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望查看当前会话使用的模型信息(名称、可用模型列表、是否支持切换),以便了解 AI 能力。
|
||||
|
||||
**前置条件**:已打开一个 AI 会话
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户查看会话顶部或侧边栏 -> 看到当前模型名称
|
||||
2. 若 AI 后端支持切换模型 -> 用户可以点击展开模型列表
|
||||
3. 若 AI 后端不支持切换模型 -> 模型名称仅为展示,不可交互
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 会话尚未初始化但之前使用过某模型:显示上次使用的模型名称(只读)
|
||||
- AI 会话尚未初始化且从未使用过:不显示模型信息
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Claude:模型信息优先从其专用配置通道读取
|
||||
- 其他后端:模型信息由后端在会话创建时返回
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户始终能看到当前使用的模型名称(AI 初始化后)
|
||||
- [ ] 不支持模型切换时,用户无法触发切换操作
|
||||
- [ ] 模型列表仅展示实际可用的模型
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-05) 查看当前模式 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望查看当前会话的工作模式,以便确认 AI 正在按预期方式工作。
|
||||
|
||||
**前置条件**:已打开一个 AI 会话
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户查看会话中的模式指示器 -> 看到当前模式名称(如"默认"、"规划"、"免确认"等)
|
||||
2. 若 AI 尚未初始化 -> 显示"默认"模式
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户始终能看到当前模式名称
|
||||
- [ ] 模式指示器准确反映实际运行模式
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-06) AI 响应超时设置 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望配置 AI 单次回复的最大等待时间,以便避免长时间无响应时一直等待。
|
||||
|
||||
**前置条件**:在应用设置中进行配置
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户进入设置 -> 找到"AI 响应超时"选项
|
||||
2. 用户可以为特定后端单独设置超时,也可以设置全局默认值
|
||||
3. 设置生效后,每次 AI 回复若超过设定时间,系统自动取消请求并提示超时
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 超时时间设置过小(低于 30 秒):系统自动调整为最小值 30 秒
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户可以设置全局默认超时时间
|
||||
- [ ] 用户可以为特定后端设置独立的超时时间(优先级高于全局)
|
||||
- [ ] 超时发生时,用户看到明确的超时提示
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-07) 免确认模式的自动迁移 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我之前在旧版设置中开启过"免确认模式",升级新版后希望该设置自动保留,无需重新配置。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户升级到新版本后打开已有会话 -> 之前的免确认设置自动生效,行为与升级前一致
|
||||
2. 若用户在新版引导页中显式选择了某个模式 -> 以用户的新选择为准,旧设置不再生效
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 旧版免确认设置自动迁移到新的模式系统中
|
||||
- [ ] 用户显式选择新模式后,旧的免确认设置被自动清理
|
||||
- [ ] 迁移过程对用户透明,无需手动操作
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-08) Codex 后端沙盒安全级别联动 [已实现]
|
||||
|
||||
**用户故事**:作为 Codex 后端用户,我希望切换模式时沙盒安全级别自动调整,以便安全策略与工作模式保持一致。
|
||||
|
||||
**前置条件**:当前使用的 AI 后端为 Codex
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户首次创建 Codex 会话 -> 系统根据默认模式自动设置对应的沙盒安全级别
|
||||
2. 用户切换模式 -> 沙盒安全级别随之自动调整
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Codex 会话的沙盒安全级别始终与当前模式匹配
|
||||
- [ ] 非 Codex 后端不受此逻辑影响
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-09) 配置自动保存与恢复 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望所有 AI 配置(模型、模式、参数选项)在每次修改后自动保存,重新打开会话或会话恢复(如应用重启、断连重连)时自动重新应用。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户修改任何配置(模型/模式/参数)-> 系统自动保存,无需手动确认
|
||||
2. 用户关闭会话后重新打开 -> 所有配置恢复到最后一次修改的状态
|
||||
3. 应用重启或连接恢复后,用户重新打开会话 -> AI 自动使用之前选择的模型和模式
|
||||
4. 用户在多个会话中分别设置不同的模型/模式 -> 每个会话独立保存,互不影响
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 保存失败(如磁盘空间不足):不影响当前使用,仅在下次打开时可能丢失未保存的设置
|
||||
- 之前选择的模型已下线或无可用渠道:用户看到错误提示(如"您选择的模型暂时不可用,请配置对应渠道或选择其他模型"),系统自动回退到默认模型
|
||||
- 之前选择的模型不在当前可用列表中:系统自动忽略,使用默认模型
|
||||
- 模式恢复失败:系统静默回退到默认模式,不中断启动
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 模型、模式、参数选项修改后自动保存
|
||||
- [ ] 每个会话的配置独立保存、独立恢复
|
||||
- [ ] 会话恢复后(重启、重连),模型和模式自动重新应用
|
||||
- [ ] 模型不可用时,用户收到明确提示并自动回退
|
||||
- [ ] 模式恢复失败不阻塞会话启动
|
||||
- [ ] 保存失败不阻塞当前操作
|
||||
|
||||
---
|
||||
|
||||
## (F-CONFIG-10) 后端能力信息缓存 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在 AI 连接建立前就能看到上次缓存的后端能力信息(可用模型列表、参数选项等),减少等待时间。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户打开一个之前用过的会话 -> 立即看到上次缓存的模型列表和参数选项(无需等待连接完成)
|
||||
2. AI 连接建立后 -> 列表自动更新为最新数据
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 首次连接后,后端能力信息被缓存
|
||||
- [ ] 后续打开会话时,缓存数据立即可用
|
||||
- [ ] 连接建立后,缓存自动更新为最新数据
|
||||
@@ -0,0 +1,226 @@
|
||||
# ACP 单聊 - 实时反馈与展示 (F-DISPLAY)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-UI-01 ~ S-UI-17(排除 S-UI-03/04/18 已合并至 F-SESSION-10)
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-01) AI 回复实时逐字显示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望 AI 的回复内容能实时逐字出现在对话界面中,以便我能及时了解 AI 正在回答什么,而不必等到回答全部完成才看到内容。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户发送消息后 -> 看到 AI 开始逐字输出回复内容
|
||||
2. 回复过程中 -> 文字持续追加,界面自动滚动
|
||||
3. AI 回复完成后 -> 文字停止追加,输入框恢复可用状态
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 网络抖动导致短暂停顿:用户可能观察到输出暂停数秒后继续,无需手动操作
|
||||
- AI 回复被中断(见 F-SESSION-03):已输出的内容保留在对话中
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 回复内容实时逐字显示,无明显卡顿
|
||||
- [ ] 回复过程中对话界面自动滚动至最新内容
|
||||
- [ ] 回复完成后输入框恢复可用
|
||||
- [ ] 页面刷新或重新进入会话后,已输出的部分回复内容不丢失
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-02) AI 思考过程展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望能看到 AI 的思考过程,以便了解它是如何分析问题的,同时不干扰正式回复的阅读。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 开始处理消息时 -> 若 AI 进行了思考,会在回复区域上方展示思考内容(折叠区域),显示思考主题和详细内容
|
||||
2. 思考过程实时更新 -> 思考内容逐步追加
|
||||
3. 思考结束、正式回复开始 -> 思考区域标记为"已完成",正式回复内容开始输出
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 思考了但最终没有输出正式回复内容:用户可以看到思考过程的记录
|
||||
- 思考内容嵌在正式回复中(部分 AI 模型的行为):系统自动提取并分离展示
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 的结构化思考内容以独立区域展示,与正式回复分开
|
||||
- [ ] 思考内容实时逐步更新
|
||||
- [ ] 思考结束后区域标记为"已完成"
|
||||
- [ ] 对于将思考内容嵌入正式回复的 AI 模型,系统能自动提取思考内容并单独展示
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-03) AI 工具调用展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望看到 AI 在回复过程中调用了哪些工具(如读文件、写文件、执行命令等),以便了解 AI 的操作进展。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 调用工具时 -> 界面显示工具调用卡片,包含工具名称和状态(进行中/已完成)
|
||||
2. 工具执行过程中 -> 卡片实时更新工具的输入和输出信息
|
||||
3. 工具执行完成 -> 卡片状态变为"已完成",显示最终结果
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 工具执行失败:卡片状态变为失败,显示错误信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 工具调用以卡片形式展示,包含工具名称
|
||||
- [ ] 工具执行状态实时更新(进行中 -> 已完成/失败)
|
||||
- [ ] 工具的输入和输出信息可查看
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-04) AI 执行计划展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望看到 AI 的执行计划,以便了解它打算分几步完成我的请求。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 生成执行计划时 -> 界面展示计划列表,包含各步骤描述
|
||||
2. 计划更新时 -> 列表实时刷新
|
||||
3. 同一轮对话中计划变化 -> 复用同一个计划展示区域,而非重复创建
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 执行计划以步骤列表形式展示
|
||||
- [ ] 计划内容实时更新
|
||||
- [ ] 同一轮对话中计划复用同一展示区域
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-06) 网页预览打开 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当 AI 在执行任务过程中打开网页(如调试工具页面)时,我希望系统能直接在预览面板中展示,无需我手动打开浏览器。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 执行中调用了浏览器导航工具 -> 系统自动在预览面板中打开对应网页
|
||||
2. 用户可在预览面板中查看和交互
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 调用浏览器导航工具时自动打开预览面板
|
||||
- [ ] 预览面板正确显示目标网页
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-07) 上下文用量展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望知道当前会话已使用了多少上下文窗口(token 用量),以便了解是否需要开启新会话。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 每次回复后 -> 界面更新上下文用量指示(如已用/总量)
|
||||
2. 用量接近上限时 -> 给予视觉提示
|
||||
|
||||
> **待确认点**:上下文用量数据存在两条获取路径,部分 AI 后端可能仅支持其中一条。需确认各后端的实际上报行为是否一致,以决定前端展示的可靠性。
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 部分后端在回复过程中实时上报用量,部分后端仅在回复结束后一次性上报
|
||||
- 部分后端可能不上报上下文上限信息,此时仅展示已用量
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 每次 AI 回复后更新上下文用量展示
|
||||
- [ ] 上下文用量数据持久化,重新打开会话后可见
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-10) 斜杠命令列表 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在输入框中输入 `/` 时能看到所有可用的斜杠命令,以便快速使用快捷功能。
|
||||
|
||||
**命令来源(按优先级合并显示)**:
|
||||
|
||||
1. **内置命令**:系统预设的命令(如 `/btw`、`/clear` 等)
|
||||
2. **内置 Skill**:系统预注入的技能命令
|
||||
3. **用户自定义 Skill**:用户在设置中添加的自定义技能
|
||||
4. **Agent 原生命令**:AI 后端自身支持的斜杠命令(通过 ACP 协议同步,需 Agent 支持)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在输入框输入 `/` -> 显示合并后的命令列表,包含命令名称、描述和来源标识
|
||||
2. 用户可输入关键字过滤命令
|
||||
3. 用户选择命令 -> 命令填充到输入框
|
||||
4. 命令列表会随 AI 后端能力动态更新(Agent 原生命令在连接建立后加载)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 尚未连接:仅显示内置命令和用户自定义 Skill,不显示 Agent 原生命令
|
||||
- Agent 原生命令加载超时(6 秒):显示已有的命令列表,不阻塞用户输入
|
||||
- 不同来源存在同名命令:需定义优先级策略(待确认)
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Agent 原生命令取决于 ACP 协议是否支持以及具体 Agent 是否提供
|
||||
- 部分后端可能不支持通过 ACP 同步命令列表
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 输入 `/` 时显示合并后的命令列表
|
||||
- [ ] 命令列表区分来源(内置 / 用户 Skill / Agent 原生)
|
||||
- [ ] 支持关键字过滤
|
||||
- [ ] Agent 原生命令在连接后动态加载
|
||||
- [ ] 命令加载超时不阻塞用户输入
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-11) 请求追踪信息 (ACP Log Panel) [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望每次 AI 开始生成回复时能看到本次请求的基本信息(如使用的模型、后端等),以便排查问题或确认配置生效。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 开始处理请求时 -> 界面显示本次请求的追踪信息(模型名称、后端类型等)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 每次 AI 开始生成时展示请求追踪信息
|
||||
- [ ] 追踪信息包含当前使用的模型名称
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-12) 环境检查与 AI 后端健康检查 [部分实现]
|
||||
|
||||
> **实现差距**:健康检查已有(ModelModalContent 中的按钮);环境变量逐项检查 UI 在 ACP V1 路径下缺失
|
||||
|
||||
**用户故事**:作为用户,我希望在配置 AI 后端时能检查环境是否正确配置以及后端是否可用,以便快速定位问题。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在设置页面点击"检查环境" -> 显示环境变量配置状态(已设置/未设置)
|
||||
2. 用户点击"健康检查" -> 系统尝试连接 AI 后端并发送测试消息 -> 显示是否可用及响应延迟
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 后端未认证:显示"未认证"提示,引导用户完成认证
|
||||
- 后端不可用:显示具体错误信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 环境检查显示各环境变量的配置状态
|
||||
- [ ] 健康检查能检测后端可用性并显示延迟
|
||||
- [ ] 认证相关错误给出明确提示
|
||||
|
||||
---
|
||||
|
||||
## (F-DISPLAY-13) 可用 AI 后端列表 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望看到系统中所有可用的 AI 后端列表及其详细信息,以便选择合适的后端创建会话。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户进入新建会话或设置页面 -> 显示所有可用 AI 后端列表
|
||||
2. 每个后端显示名称、类型、是否已安装、支持的扩展能力等信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 展示所有检测到的可用 AI 后端
|
||||
- [ ] 每个后端显示名称和类型等基本信息
|
||||
- [ ] 扩展后端有明确的标识
|
||||
@@ -0,0 +1,266 @@
|
||||
# ACP 单聊 - 消息发送与文件处理 (F-MSG + F-FILE-02)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-MSG-01 ~ S-MSG-12, S-FILE-03
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-01) 发送文本消息 [部分实现]
|
||||
|
||||
> **实现差距**:消息队列基础功能已实现(入队/编辑/删除/清空/自动发送),但缺少「多条排队消息合并为一条发出」
|
||||
|
||||
**用户故事**:作为用户,我希望在输入框中输入文字并发送,能立即看到我的消息出现在对话中,同时 AI 开始回复。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在输入框中输入文本,点击发送
|
||||
2. 用户的消息立即出现在对话区域(无需等待 AI 后端连接完成)
|
||||
3. 会话在列表中的排序位置更新为最新
|
||||
4. 系统自动完成后台连接(若尚未连接)
|
||||
5. AI 开始流式输出回复内容
|
||||
|
||||
**AI 回复期间的消息队列**:
|
||||
|
||||
- AI 正在回复时,输入框仍可输入。用户发送的消息进入等待队列
|
||||
- 队列中的消息可以逐条编辑或删除,也支持一键清空队列
|
||||
- 当前 turn 结束后,队列中的消息自动发出
|
||||
- 如果队列中有多条消息,系统在等待期间将其合并为一条发出
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 未输入任何内容点击发送:系统不响应
|
||||
- AI 后端连接失败:用户的消息已显示在对话中,系统显示错误提示,告知连接失败原因并建议操作
|
||||
- AI 回复过程中发生错误:系统显示错误消息,随后恢复输入框为可用状态
|
||||
- 会话不存在或已被删除:系统提示「会话未找到」
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Qwen:若出现内部错误,系统会提供增强的错误提示信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户消息立即显示,不等待后端连接
|
||||
- [ ] 会话列表排序实时更新
|
||||
- [ ] AI 连接失败时显示明确的错误提示
|
||||
- [ ] 错误发生后界面恢复可用状态(不卡在加载中)
|
||||
- [ ] 已缓冲的部分 AI 回复在发生错误时不丢失
|
||||
- [ ] AI 回复期间可继续输入并发送消息到队列
|
||||
- [ ] 队列中的消息可编辑、删除、一键清空
|
||||
- [ ] 当前 turn 结束后队列消息自动发出
|
||||
- [ ] 多条队列消息在等待期间合并为一条发出
|
||||
|
||||
> 覆盖场景:S-MSG-01, S-MSG-02, S-MSG-09, S-MSG-10, S-MSG-11, S-MSG-12
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-02) 在消息中引用文件 [部分实现]
|
||||
|
||||
> **实现差距**:三种引用方式均已实现,但缺少:(1) 多方式引用同一文件时自动去重;(2) 路径标记剥离不完整
|
||||
|
||||
**用户故事**:作为用户,我希望能以多种方式将文件内容提供给 AI,以便 AI 能基于这些文件进行回复。
|
||||
|
||||
**前置条件**:用户已设置工作区目录(Add to Chat 方式需要)
|
||||
|
||||
**文件引用的三种方式**:
|
||||
|
||||
**方式一:Add to Chat(从工作区目录树引用)**
|
||||
|
||||
1. 用户在工作区目录树中右键点击某个文件或目录,选择「Add to Chat」
|
||||
2. 文件/目录以 `@路径` 形式添加到输入框
|
||||
3. 发送时系统自动读取文件内容并附加到消息中
|
||||
4. 也可以手动在输入框中输入 `@文件名` 触发自动补全
|
||||
|
||||
**方式二:上传文件**
|
||||
|
||||
1. 用户点击上传按钮或拖拽文件到输入框区域
|
||||
2. 文件显示在输入框附近的预览区域
|
||||
3. 用户输入消息文本并点击发送
|
||||
4. 文件随消息一起发送给 AI
|
||||
|
||||
**方式三:粘贴板**
|
||||
|
||||
1. 用户复制文件内容或截图后,在输入框中粘贴
|
||||
2. 文本内容直接粘贴为消息文本
|
||||
3. 图片内容作为附件添加到消息中
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 引用的文件不存在:系统将原始 `@filename` 文本直接传递给 AI,由 AI 自行处理
|
||||
- 引用的是二进制文件(如图片、编译产物):系统不内联读取内容,而是将文件引用传递给 AI 后端处理
|
||||
- 同时通过多种方式引用了同一文件:系统自动去重
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Gemini:上传的文件会被临时复制到工作区目录中,AI 完成回复后临时文件自动清理
|
||||
- 其他后端:文件通过路径引用或内容内联的方式传递
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Add to Chat 方式:从目录树选择文件/目录后以 `@路径` 添加到输入框
|
||||
- [ ] 上传方式:支持拖拽和按钮上传
|
||||
- [ ] 粘贴板方式:支持粘贴文本和图片
|
||||
- [ ] 文件内容被附加到消息中供 AI 参考
|
||||
- [ ] 二进制文件不做内联读取
|
||||
- [ ] 多种方式引用同一文件时自动去重
|
||||
- [ ] 附件路径以工作区相对路径形式展示,工作区外的文件仅显示文件名
|
||||
- [ ] AI 收到的消息不包含界面展示用的路径标记
|
||||
- [ ] 上传失败不阻塞消息发送
|
||||
- [ ] Gemini 后端下上传文件自动复制到工作区,回复后自动清理;用户可配置是否保留
|
||||
|
||||
> 覆盖场景:S-MSG-03, S-MSG-07, S-FILE-01, S-FILE-02, S-FILE-04
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-03) 首条消息自动注入 AI 规则 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望每个新会话的第一条消息自动包含 AI 的行为规则和技能索引,以便 AI 从一开始就按照正确的规则工作,我无需每次手动说明。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在新会话中发送第一条消息
|
||||
2. 系统自动在消息中附加预设的 AI 规则和可用技能信息(用户不可见)
|
||||
3. AI 基于这些规则开始工作
|
||||
4. 后续消息不再重复注入规则
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 没有可用的规则和技能:消息原样发送,不做任何包装
|
||||
- 用户使用自定义工作区:系统使用文本注入方式而非目录符号链接方式注入规则
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 新会话首条消息自动注入 AI 规则
|
||||
- [ ] 仅首条消息注入,后续消息不重复
|
||||
- [ ] 注入内容对用户不可见
|
||||
- [ ] 自定义工作区场景下规则仍能正确注入
|
||||
|
||||
> 覆盖场景:S-MSG-04, S-MSG-05, S-MSG-06
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-04) 隐藏消息与静默消息 [已实现]
|
||||
|
||||
**用户故事**:作为系统(定时任务、频道集成等),我需要能够发送不显示在用户界面中的消息给 AI,以便后台自动化任务能够无干扰地运行。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. **静默消息**:系统后台发送的消息完全不在界面中显示,也不记录到消息历史。用户完全无感知。
|
||||
2. **隐藏消息**:消息存在于历史记录中,但不在对话界面中显示。用于后台自动化场景。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 静默消息不出现在对话界面
|
||||
- [ ] 静默消息不记录到消息历史
|
||||
- [ ] 隐藏消息不出现在对话界面但保留在历史中
|
||||
|
||||
> 覆盖场景:S-MSG-08
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-06) 输入框历史记录 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在输入框中通过上下键浏览历史输入,以便快速复用之前发过的消息内容。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 焦点在输入框时,用户按 `上` 键 -> 显示上一条历史输入内容
|
||||
2. 继续按 `上` 键 -> 继续往前翻历史
|
||||
3. 按 `下` 键 -> 往后翻历史,回到更近的输入
|
||||
4. 用户可以基于历史内容编辑后直接发送
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 上下键在输入框为空或光标在首/末行时触发历史浏览
|
||||
- [ ] 历史记录按时间倒序排列
|
||||
- [ ] 选择历史内容后可编辑再发送
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-07) 重试与撤销上一轮对话 [未实现]
|
||||
|
||||
> **实现差距**:代码库中无任何 redo/undo 相关实现
|
||||
|
||||
**用户故事**:作为用户,我希望能够编辑并重新发送最后一轮对话中的消息(重试),或撤销最后一轮对话回到发送前的状态,以便修正提问或尝试不同的表述。
|
||||
|
||||
**前置条件**:最后一个 prompt turn 已结束(AI 已完成回复)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. **重试(Redo)**:用户点击最后一条用户消息的「重试」按钮 -> 该消息内容回到输入框供编辑 -> 编辑后重新发送 -> AI 基于编辑后的内容重新回复
|
||||
2. **撤销(Undo)**:用户点击「撤销」-> 最后一轮对话(用户消息 + AI 回复)从对话历史中移除 -> 回到上一轮对话的状态
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 对话历史为空:重试和撤销按钮不可用
|
||||
- AI 正在回复中:重试和撤销按钮不可用,需等待当前 turn 结束
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 此功能可能依赖 ACP 协议和 Agent 后端是否支持会话历史回滚。不支持的后端可能无法实现撤销功能,仅支持重试(重新发送编辑后的消息)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 最后一轮用户消息可编辑后重新发送
|
||||
- [ ] 撤销操作移除最后一轮对话(用户消息 + AI 回复)
|
||||
- [ ] AI 回复中时重试/撤销不可用
|
||||
- [ ] 后端不支持回滚时,撤销按钮不显示或提示不支持
|
||||
|
||||
---
|
||||
|
||||
## (F-MSG-08) `/btw` 追加上下文 [部分实现]
|
||||
|
||||
> **实现差距**:/btw 已实现但采用 fork session 独立回答方式,非注入当前 AI 上下文;不支持的后端直接提示不支持而非入队
|
||||
|
||||
**用户故事**:作为用户,我希望在 AI 回复过程中通过 `/btw` 命令追加补充信息,AI 能在不中断当前回复的情况下参考这些补充内容。
|
||||
|
||||
**前置条件**:AI 正在回复中
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 正在回复
|
||||
2. 用户输入 `/btw 补充内容` 并发送
|
||||
3. 补充信息被注入到 AI 的当前上下文中
|
||||
4. AI 在后续回复中参考这些补充内容(不会重新开始回复)
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 目前仅 Claude 后端支持此功能
|
||||
- 其他后端:`/btw` 消息进入等待队列,等当前 turn 结束后作为普通消息发出
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Claude 后端下 `/btw` 消息实时注入到 AI 上下文
|
||||
- [ ] 不支持的后端下 `/btw` 消息进入等待队列
|
||||
- [ ] `/btw` 消息在对话界面中有区别于普通消息的展示
|
||||
|
||||
---
|
||||
|
||||
## (F-FILE-02) AI 读取和写入文件 [已实现]
|
||||
|
||||
> **实现差距**:V2 路径缺少 auto-mkdir 和编辑器通知,但当前走 V1 路径,V1 完整实现
|
||||
|
||||
**用户故事**:作为用户,我希望 AI 能够按需读取我工作区中的文件、以及在工作区中创建或修改文件,以便 AI 可以帮我完成代码编写、文档编辑等任务。
|
||||
|
||||
**前置条件**:用户已设置工作区目录
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在对话中要求 AI 读取某个文件或修改某个文件
|
||||
2. AI 自动执行文件读取或写入操作
|
||||
3. 对话中显示 AI 执行的文件操作记录(读取了哪个文件、编辑了哪个文件)
|
||||
4. 如果是写入操作,用户可以在编辑器中看到文件内容的实时更新
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 读取不存在的文件:AI 收到错误信息并在对话中告知用户
|
||||
- 写入时目标目录不存在:系统自动创建所需目录
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 能读取工作区内的文本文件
|
||||
- [ ] AI 能在工作区内创建新文件或修改现有文件
|
||||
- [ ] 文件操作以工具调用的形式展示在对话中
|
||||
- [ ] 写入时自动创建不存在的目录
|
||||
- [ ] 文件写入后编辑器实时收到内容更新通知
|
||||
|
||||
> 覆盖场景:S-FILE-03
|
||||
@@ -0,0 +1,147 @@
|
||||
# ACP 单聊 - 权限与安全 (F-PERM)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-PERM-01 ~ S-PERM-06
|
||||
|
||||
---
|
||||
|
||||
## (F-PERM-01) AI 操作权限审批 [部分实现]
|
||||
|
||||
> **实现差距**:核心审批流程完整,但缺少 30 分钟超时自动拒绝机制(`createdAt` 字段已预留,无定时扫描逻辑)
|
||||
|
||||
**用户故事**:作为用户,我希望 AI 在执行敏感操作(如写入文件、运行命令)前征求我的许可,以便我掌控 AI 的行为。
|
||||
|
||||
> **待确认**(源自 S-PERM-01 矛盾 1):权限审批存在两层自动判断机制,其确切执行顺序需开发确认。当前理解为:系统先检查"始终允许"记录,再检查是否处于免确认模式。两层判断的先后关系直接影响用户在不同场景下的体验。
|
||||
|
||||
**前置条件**:AI 会话处于活跃状态,且未开启免确认模式
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 在执行敏感操作前 -> 用户界面弹出权限确认卡片,显示操作类型(如"编辑文件")、操作内容(如文件路径)和可选项
|
||||
2. 用户选择"允许"-> AI 继续执行该操作
|
||||
3. 用户选择"始终允许"-> AI 继续执行该操作,且后续同类操作(相同类型 + 相同对象)自动放行,不再弹窗
|
||||
4. 用户选择"拒绝"-> AI 不执行该操作,继续对话
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 用户 30 分钟内未响应权限请求:系统自动拒绝该操作
|
||||
- AI 正在等待权限审批时,用户点击"停止"按钮:权限请求被取消,AI 停止当前任务
|
||||
- 多个权限请求同时到达:按顺序依次展示给用户
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 团队模式下:权限请求无超时限制(等待团队管理者审批)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 敏感操作前弹出权限确认卡片,包含操作类型和内容
|
||||
- [ ] 用户可选择"允许""始终允许"或"拒绝"
|
||||
- [ ] 选择"始终允许"后,同类操作在当前会话内自动放行
|
||||
- [ ] 会话重置后,"始终允许"记录被清除
|
||||
- [ ] 超时未响应时自动拒绝
|
||||
|
||||
---
|
||||
|
||||
## (F-PERM-02) 权限确认操作 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在权限确认卡片上快速做出决定,且操作后 AI 立即响应。
|
||||
|
||||
**前置条件**:界面上有未处理的权限确认卡片
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户看到权限确认卡片,卡片上显示操作描述和可选按钮
|
||||
2. 用户点击某个选项按钮 -> 确认卡片消失
|
||||
3. AI 根据用户选择继续或放弃操作,对话继续
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 用户在 AI 尚未完全初始化时做出确认:系统等待初始化完成后再执行确认
|
||||
- 确认卡片对应的操作已不存在(如 AI 已被停止):确认操作无效,卡片静默消失
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击确认按钮后,卡片立即从界面消失
|
||||
- [ ] AI 在用户确认后立即恢复执行
|
||||
- [ ] 无效的确认操作不会引发错误
|
||||
|
||||
---
|
||||
|
||||
## (F-PERM-03) 免确认模式 / YOLO 模式 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望开启免确认模式后,AI 可以自主执行所有操作而无需逐一征求我的许可,所有操作请求自动通过,以便提高效率。
|
||||
|
||||
> **待确认**(源自 S-PERM-03 矛盾 2):部分 AI 后端(codebuddy)的免确认模式内部标识存在不一致记录,需开发确认实际使用的值。此矛盾不影响用户体验,但可能影响该后端免确认模式能否正确生效。
|
||||
|
||||
**前置条件**:当前 AI 后端支持免确认模式
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在模式选择中切换到免确认模式 -> 系统提示模式已切换
|
||||
2. AI 需要执行操作时 -> 系统自动选择第一个选项(通常为"允许"),几乎即时通过
|
||||
3. 用户不会看到任何确认卡片,AI 连续执行操作
|
||||
4. 定时任务复用已有会话时,系统自动为其启用免确认模式(用户无感知)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 后端不支持免确认模式(如 Codex、Gemini):该模式选项不出现在模式列表中
|
||||
- 启用失败:用户看到错误提示,模式保持不变
|
||||
- 操作请求没有可选项(罕见情况):即使在免确认模式下也会弹出卡片,需要用户手动处理
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 支持免确认模式的后端:Claude、Codebuddy、Qwen、iFlow
|
||||
- 不支持免确认模式的后端:Codex、Gemini 及其他通用后端 -- 模式列表中不展示此选项
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 支持免确认的后端,模式列表中包含免确认选项
|
||||
- [ ] 不支持免确认的后端,模式列表中不包含免确认选项
|
||||
- [ ] 开启免确认模式后,所有带有可选项的操作请求自动通过
|
||||
- [ ] 自动通过的速度足够快(用户几乎无感知延迟)
|
||||
- [ ] 没有可选项的操作请求不会被自动处理
|
||||
- [ ] 从免确认模式切回其他模式后,操作重新需要确认
|
||||
- [ ] 定时任务复用会话时可自动进入免确认模式
|
||||
|
||||
---
|
||||
|
||||
## (F-PERM-04) 查看待确认操作列表 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望查看当前会话中所有等待我确认的操作,以便不遗漏任何需要审批的请求。
|
||||
|
||||
**前置条件**:AI 会话处于活跃状态
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户打开/切换到某个会话 -> 界面显示该会话中所有待确认的操作卡片
|
||||
2. 若无待确认操作 -> 不显示任何确认卡片
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 会话未初始化或已结束:返回空列表,不显示确认卡片
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 进入会话时,所有待确认操作以卡片形式展示
|
||||
- [ ] 确认或拒绝操作后,对应卡片从列表中消失
|
||||
- [ ] 无待确认操作时,界面干净无多余元素
|
||||
|
||||
---
|
||||
|
||||
## (F-PERM-06) 会话创建时的模式与权限初始化 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望新建会话时的模式和权限设置来自我的默认偏好或引导页选择,无需每次重新配置。
|
||||
|
||||
**前置条件**:用户新建一个 AI 会话
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在引导页选择了"免确认模式" -> 新建的会话自动进入免确认模式
|
||||
2. 用户未做任何选择 -> 新建的会话使用默认模式,操作需要逐一确认
|
||||
3. 之前在设置中开启过全局免确认 -> 新建会话自动继承该设置
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 引导页选择的模式在新会话中立即生效
|
||||
- [ ] 默认情况下新会话使用默认模式(需逐一确认)
|
||||
- [ ] 全局免确认设置可被引导页的显式选择覆盖
|
||||
@@ -0,0 +1,138 @@
|
||||
# ACP 单聊 - 可靠性与错误处理 (F-RELIABILITY)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-ERR-01 ~ S-ERR-09(排除 S-ERR-03 已合并至 F-SESSION-04,S-ERR-08/09 已合并至 F-SESSION-10)
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-01) 连接超时自动处理 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当系统与 AI 后端的连接因网络问题等原因超时时,我希望系统能给出明确提示,而不是无限等待。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统开始连接 AI 后端 -> 显示"连接中"状态
|
||||
2. 连接超时 -> 显示"连接超时"错误提示
|
||||
3. 用户再次发送消息 -> 系统自动尝试重新连接
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 首次连接失败:系统自动重试一次(用户可能观察到短暂延迟后恢复)
|
||||
- 重试仍失败:显示错误提示
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 部分后端的连接等待时间较长(最多约 2.5 分钟),部分后端约 1 分钟
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 连接超时后显示明确的错误提示
|
||||
- [ ] 首次连接失败自动重试一次
|
||||
- [ ] 超时后用户可通过发送新消息触发重连
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-02) AI 回复超时自动处理 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当 AI 长时间没有响应时,我希望系统能自动取消等待并告知我,而不是让我无限等待。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户发送消息 -> AI 开始处理
|
||||
2. AI 在配置的超时时间内无响应 -> 系统自动取消等待,显示超时提示
|
||||
3. 用户可以重新发送消息
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 正在进行工具调用(如读写文件)时不会触发超时
|
||||
- AI 等待用户审批权限时不会触发超时
|
||||
- AI 有持续输出时不会触发超时(每次收到内容都会重置超时计时)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 无响应超过配置时间后自动取消并提示
|
||||
- [ ] 工具调用和权限审批期间不触发超时
|
||||
- [ ] AI 有持续输出时不触发超时
|
||||
- [ ] 超时后用户可重新发送消息
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-04) 启动失败友好提示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当 AI 后端启动失败时,我希望看到有用的错误提示,以便我知道如何解决问题。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统尝试启动 AI 后端失败 -> 显示针对性的错误提示:
|
||||
- "AI 工具未安装":引导安装
|
||||
- "配置文件错误":引导修复配置
|
||||
- "AI 工具不支持当前模式":引导切换模式或更新工具
|
||||
2. 首次启动失败 -> 系统自动重试一次(短暂延迟后)
|
||||
3. 重试仍失败 -> 显示最终错误提示
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 启动失败后显示用户可理解的错误提示
|
||||
- [ ] 不同原因的启动失败给出不同的针对性提示
|
||||
- [ ] 首次启动失败自动重试一次
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-05) 本地缓存损坏自动修复 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我不希望因为本地缓存损坏而无法使用 AI 功能,系统应该能自动修复这种问题。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统检测到本地缓存损坏(启动过程中自动检测)-> 自动清理损坏的缓存文件
|
||||
2. 清理后自动重新安装依赖
|
||||
3. 整个过程对用户透明,用户只会感知到稍长的启动时间
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 缓存损坏时自动修复,无需用户手动操作
|
||||
- [ ] 修复后 AI 功能正常可用
|
||||
- [ ] 修复过程不丢失用户数据
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-06) 多候选安装策略(特定后端)[未实现]
|
||||
|
||||
> **实现差距**:ACP 统一架构下 CLI 由用户预装,此功能可能不再适用,需与产品确认是否保留
|
||||
|
||||
**用户故事**:作为用户,我希望在使用特定 AI 后端时,即使某个安装包不兼容我的操作系统,系统也能自动尝试其他兼容包。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统启动特定 AI 后端 -> 按优先级尝试可用的安装包
|
||||
2. 首选包安装失败 -> 自动尝试备选包
|
||||
3. 最终成功启动 -> 用户无感知
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 所有候选包均失败:显示错误提示
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 自动尝试多个候选安装包
|
||||
- [ ] 安装失败自动降级到下一个候选包
|
||||
- [ ] 所有候选失败后给出明确错误
|
||||
|
||||
---
|
||||
|
||||
## (F-RELIABILITY-07) 发送消息异常恢复 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当消息发送过程中出现任何异常时,我希望系统能妥善处理并告知我,不会导致界面卡死。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 消息发送过程中发生异常 -> 界面显示错误消息
|
||||
2. AI 已部分输出的内容 -> 保留在对话中,不丢失
|
||||
3. 界面恢复可用状态 -> 用户可以继续发送新消息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 发送异常后显示错误提示消息
|
||||
- [ ] 已部分输出的 AI 回复内容不丢失
|
||||
- [ ] 错误消息和回复结束信号按正确顺序展示(先看到错误原因,再看到回复结束)
|
||||
- [ ] 界面不会卡在"正在回复"状态
|
||||
@@ -0,0 +1,304 @@
|
||||
# ACP 单聊 - 会话管理 (F-SESSION)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-LIFE-01 ~ S-LIFE-17
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-01) 创建新会话 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望点击新建按钮后立即看到一个新会话出现在列表中,以便我可以开始与 AI 对话。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击「新建会话」按钮,选择 AI 后端类型(Claude、Codex、Gemini 等)
|
||||
2. 系统在会话列表中立即显示新会话条目
|
||||
3. 系统托盘菜单同步更新,包含新会话
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 选择了无效的会话类型:系统不创建会话,无任何可见变化
|
||||
- 创建过程中发生内部错误:系统仍返回会话记录,但后续操作可能需要重试
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击新建后,会话列表立即出现新条目
|
||||
- [ ] 系统托盘菜单同步更新
|
||||
- [ ] 支持所有已配置的 AI 后端类型
|
||||
|
||||
> 覆盖场景:S-LIFE-01
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-02) 进入会话并建立连接 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望点击一个会话后系统自动完成所有准备工作(连接 AI 后端、认证、恢复上下文),以便我可以直接开始对话。
|
||||
|
||||
**前置条件**:会话已创建(F-SESSION-01)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击进入某个会话
|
||||
2. 系统显示「正在连接...」状态
|
||||
3. 系统自动完成以下动作(用户无需干预):
|
||||
- 连接到对应的 AI 后端
|
||||
- 完成认证(如需要)
|
||||
- 加载用户已配置且启用的 MCP 工具服务(如数据库查询、API 调用等外部工具)
|
||||
- 加载内置 MCP 工具服务(如图像生成等系统预装能力)
|
||||
- 加载扩展贡献的 MCP 工具服务(来自已安装的扩展插件)
|
||||
- 恢复之前的对话上下文(如有)
|
||||
- 恢复用户之前选择的模型和会话模式
|
||||
4. 状态变为「已就绪」,用户可以开始输入
|
||||
5. AI 可以在对话中调用已注入的所有 MCP 工具
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 连接超时(约 70 秒,Codex 约 150 秒):系统显示连接失败提示
|
||||
- 认证失败:系统引导用户手动登录 `TODO: 认证失败的引导方式需要具体设计`
|
||||
- 首次连接失败:系统自动重试一次(用户可能感知短暂延迟),二次失败后提示错误
|
||||
- 之前选择的模型已不可用:系统自动回退到默认模型,并提示用户「您之前选择的模型 [xxx] 已不可用,已切换至默认模型」 `TODO: 模型不可用的提示方式需要具体设计`
|
||||
- 会话模式恢复失败:静默回退到默认模式,不影响使用
|
||||
- MCP 工具服务加载失败:AI 仍可正常对话,但缺少对应外部工具能力(如无法查数据库) `TODO: MCP 工具加载失败的提示方式需要具体设计`
|
||||
- 用户未配置任何 MCP 工具服务:不影响基本对话功能,AI 仅使用内置能力
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- Claude:首次使用可能需要登录授权
|
||||
- Qwen:首次使用可能需要登录授权
|
||||
- Codex:连接时间较长(最长约 2.5 分钟),无需额外认证
|
||||
- 其他后端:连接时间约 70 秒,认证由后端自行处理
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 进入会话后自动完成连接,用户无需手动操作
|
||||
- [ ] 连接过程中显示状态指示(连接中 -> 已连接 -> 就绪)
|
||||
- [ ] 首次连接失败自动重试一次
|
||||
- [ ] 之前的对话上下文、模型选择、会话模式均能恢复
|
||||
- [ ] 认证失败时提供明确的引导
|
||||
- [ ] 并发进入同一会话不会导致重复初始化
|
||||
- [ ] 用户已启用的 MCP 工具服务在建立连接时自动注入
|
||||
- [ ] MCP 工具服务加载失败不阻塞会话连接
|
||||
|
||||
> 覆盖场景:S-LIFE-02, S-LIFE-03, S-LIFE-09, S-LIFE-10, S-LIFE-11, S-LIFE-12
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-03) 停止当前 AI 回复 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在 AI 回复过程中可以随时点击停止按钮中断回复,以便我可以修改问题或发送新的指令。
|
||||
|
||||
**前置条件**:AI 正在生成回复
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 正在流式输出内容,界面显示「停止」按钮
|
||||
2. 用户点击「停止」按钮
|
||||
3. AI 立即停止输出
|
||||
4. 已输出的内容保留在对话中
|
||||
5. 输入框恢复可用状态,用户可以继续发送消息
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 会话尚未初始化(用户很快点击停止):无任何效果,不报错
|
||||
- AI 尚未开始回复时点击停止:无明显效果,界面恢复可用状态
|
||||
- AI 恰好在用户点击停止的同时完成回复:用户看到完整回复,停止操作无可见效果
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 回复过程中显示「停止」按钮
|
||||
- [ ] 点击停止后 AI 立即停止输出
|
||||
- [ ] 连接保持活跃,后续消息可正常发送
|
||||
- [ ] 已生成的部分回复保留在对话历史中
|
||||
- [ ] 输入框恢复可用状态
|
||||
- [ ] 停止操作不会断开连接,后续消息可正常发送
|
||||
|
||||
> 覆盖场景:S-LIFE-05
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-04) 意外断连自动处理 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在 AI 后端意外断开时系统自动处理,下次发消息时无感重新连接,以便我不需要手动操作来恢复会话。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在会话中正常使用
|
||||
2. AI 后端因网络或其他原因意外断开
|
||||
3. 如果 AI 正在回复中:已输出的部分内容保留,界面恢复可用状态,系统提示「连接已断开」
|
||||
4. 用户再次发送消息时,系统自动重新连接并恢复会话
|
||||
5. 重连成功后消息正常发送,用户无需额外操作
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 自动重连失败:系统提示「无法连接到 AI 后端,请检查网络或稍后重试」
|
||||
- AI 后端崩溃:当前回复停止,已输出内容保留,后续消息触发自动重连
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 意外断连时,已输出的部分内容不丢失
|
||||
- [ ] 断连后当前回复停止,界面恢复可用状态
|
||||
- [ ] 断连后再次发送消息时自动重连
|
||||
- [ ] 重连成功后对话上下文不丢失
|
||||
- [ ] 重连失败时给出明确提示
|
||||
|
||||
> 覆盖场景:S-LIFE-06, S-ERR-03
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-05) 空闲会话自动释放 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望长时间不使用的会话能自动释放系统资源,但下次使用时可以无感恢复,以便系统不会因为大量闲置会话而占用过多资源。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户与 AI 完成对话后,不再发送消息
|
||||
2. 会话空闲超过设定时间(默认 5 分钟,可配置)后,系统自动释放后台资源
|
||||
3. 用户再次回到该会话发送消息时,系统自动重新连接(参见 F-SESSION-02),用户无需额外操作
|
||||
4. 对话历史完整保留,用户体验与未断开时一致
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 正在执行定时任务:系统不会释放该会话,直到任务完成
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 空闲超过默认 5 分钟后自动释放后台资源
|
||||
- [ ] 释放后对话历史完整保留
|
||||
- [ ] 用户再次操作时自动恢复连接,无缝体验
|
||||
- [ ] 正在执行定时任务的会话不被释放
|
||||
- [ ] 用户可在设置中调整空闲超时时间
|
||||
|
||||
> 覆盖场景:S-LIFE-07
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-06) 重置所有会话 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望可以一次性重置所有会话的运行状态,以便在系统出现异常时快速恢复。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户触发「重置所有会话」操作
|
||||
2. 系统关闭所有运行中的 AI 后端连接
|
||||
3. 会话列表中的会话数据保留,但所有会话回到「未连接」状态
|
||||
4. 用户重新进入任意会话时自动重新连接
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 某些会话关闭失败:不影响其他会话的关闭过程
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 所有运行中的连接全部关闭
|
||||
- [ ] 会话数据(历史、设置)不丢失
|
||||
- [ ] 单个会话关闭失败不影响整体操作
|
||||
|
||||
> 覆盖场景:S-LIFE-08
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-07) 删除会话 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望删除不需要的会话,以便保持会话列表整洁。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户选择某个会话并点击「删除」
|
||||
2. 该会话从列表中移除
|
||||
3. 系统托盘菜单同步更新
|
||||
4. 如果该会话正在运行中,AI 连接自动断开
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 删除过程中出错:会话可能仍然保留在列表中,用户可重试
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 删除后会话从列表中消失
|
||||
- [ ] 运行中的会话删除后自动断开连接
|
||||
- [ ] 系统托盘菜单同步更新
|
||||
- [ ] 关联的消息历史一并删除
|
||||
|
||||
> 覆盖场景:S-LIFE-16
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-08) 查看会话详情与状态 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望能看到会话的实时运行状态,以便了解 AI 当前是否正在处理任务。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户查看某个会话
|
||||
2. 系统显示该会话的详细信息,包括:
|
||||
- 会话基本信息(名称、创建时间等)
|
||||
- 实时运行状态(运行中 / 已完成 / 未连接)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 会话已被空闲释放或从未初始化:状态显示为「已完成」
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 会话详情页显示实时运行状态
|
||||
- [ ] 未连接的会话状态显示为「已完成」
|
||||
|
||||
> 覆盖场景:S-LIFE-14
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-09) 会话迁移 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望从旧版本升级后,之前的会话能自动迁移到新格式,以便我不丢失任何对话历史。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统检测到存在旧格式的会话数据
|
||||
2. 自动将旧会话迁移为新格式,包括对话历史和定时任务
|
||||
3. 旧会话从列表中消失,新格式会话出现
|
||||
4. 用户无需任何操作,迁移过程对用户透明
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 迁移过程失败:旧会话数据保留,不会丢失
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 旧格式会话自动迁移为新格式
|
||||
- [ ] 迁移后对话历史完整保留
|
||||
- [ ] 定时任务(如有)一并迁移
|
||||
- [ ] 迁移失败时旧数据不丢失
|
||||
|
||||
> 覆盖场景:S-LIFE-17
|
||||
|
||||
---
|
||||
|
||||
## (F-SESSION-10) AI 回复完成处理 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在 AI 完成回复后,界面立即恢复到可输入状态,以便我可以继续对话。无论 AI 是正常完成还是异常中断,界面都应可靠地恢复可用。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 完成回复,输出最后的文本内容
|
||||
2. 界面从「正在回复」状态变为「已完成」状态
|
||||
3. 输入框恢复可用,用户可以输入新消息
|
||||
4. 如果 AI 回复中包含定时任务指令(如设置提醒),系统自动创建或更新定时任务,并显示系统提示
|
||||
|
||||
**异常情况(自动恢复)**:
|
||||
|
||||
- AI 生成了内容但未正常发出完成信号:系统在 15 秒无新内容后自动判定回复完成,恢复界面状态
|
||||
- AI 回复请求已发送但未收到任何内容也未收到完成信号:系统自动补发完成信号,恢复界面状态
|
||||
- AI 输出了思考过程但没有生成实际内容:回复仍标记为完成
|
||||
- AI 正在等待用户审批权限时不会触发自动结束判定
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] AI 完成回复后界面立即恢复可输入状态
|
||||
- [ ] 界面从「正在回复」状态变为「已完成」状态
|
||||
- [ ] 15 秒无活动后自动恢复界面状态(防止界面卡在「正在回复」)
|
||||
- [ ] 权限审批期间不触发自动结束
|
||||
- [ ] 1 秒内重复完成的回合自动去重,不会重复触发通知
|
||||
- [ ] 定时任务指令被正确识别和执行
|
||||
- [ ] 无论 AI 是否正常完成回复,界面最终都能恢复可输入状态
|
||||
|
||||
> 覆盖场景:S-LIFE-15, S-LIFE-13, S-UI-03, S-UI-04, S-UI-18, S-ERR-08, S-ERR-09
|
||||
@@ -0,0 +1,100 @@
|
||||
# ACP 单聊 - 技能扩展 (F-SKILL)
|
||||
|
||||
> 本文档由 PM 从 `prd-acp-scenarios.md` 抽取,保持与源同步。
|
||||
> 覆盖技术场景:S-MSG-04 ~ S-MSG-06, S-SKILL-01
|
||||
|
||||
---
|
||||
|
||||
## (F-SKILL-01) AI 技能自动发现与注入 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望 AI 能自动发现并使用我配置的技能(如代码审查、测试等专业能力),以便获得更专业的帮助。
|
||||
|
||||
**前置条件**:用户已在技能目录中配置了技能
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户发送首条消息时 -> 系统自动扫描并发现可用技能
|
||||
2. 技能索引自动注入到 AI 的上下文中 -> AI 知道自己拥有哪些专业能力
|
||||
3. AI 根据任务需要自动调用相应技能
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 支持原生技能发现的后端:AI 通过工作目录中的技能文件自动获取技能信息
|
||||
- 不支持原生技能发现的后端:系统通过在首条消息中注入技能索引来告知 AI
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 首条消息时自动发现并注入可用技能
|
||||
- [ ] 支持内置技能、打包技能和用户自定义技能三种来源
|
||||
- [ ] 技能注入仅在首条消息时执行,后续消息不重复注入
|
||||
|
||||
---
|
||||
|
||||
## (F-SKILL-02) 指定技能注入(高级模式)[已实现]
|
||||
|
||||
**用户故事**:作为高级用户,我希望在发送消息时能指定使用哪些技能,以便精确控制 AI 的专业能力范围。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在高级编辑界面中选择要启用的技能 -> 选中指定技能
|
||||
2. 发送消息 -> 系统将选中技能的完整内容注入到消息上下文中
|
||||
3. AI 获得所选技能的详细指导 -> 按照技能要求执行任务
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 未选择任何技能:消息按原样发送,不注入技能信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 支持在发送消息时手动选择启用的技能
|
||||
- [ ] 选中技能的完整内容被注入到 AI 上下文
|
||||
- [ ] 注入的内容包含技能目录路径信息
|
||||
|
||||
---
|
||||
|
||||
## (F-SKILL-03) MCP 工具服务注入 [部分实现]
|
||||
|
||||
> **实现差距**:4/5 验收标准通过;缺失 OAuth 认证引导 UI(仅支持 header 传递 token)
|
||||
|
||||
**用户故事**:作为用户,我希望 AI 能自动使用我配置的外部工具服务,以便 AI 能够完成更多类型的任务(如数据库查询、API 调用等)。
|
||||
|
||||
**前置条件**:用户已在设置中添加并启用了 MCP 工具服务
|
||||
|
||||
**MCP 工具服务的三种来源**:
|
||||
|
||||
1. **用户手动配置**:用户在设置界面添加的外部工具服务(如数据库连接、API 网关等)
|
||||
2. **系统内置**:应用预装的工具服务(如图像生成),标记为"内置",用户可启用/禁用
|
||||
3. **扩展贡献**:通过已安装的扩展插件自动提供的工具服务
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在设置中配置 MCP 工具服务(添加服务地址、选择传输方式、配置认证信息)
|
||||
2. 用户启用该工具服务
|
||||
3. 用户创建或进入会话时,系统自动将所有已启用的工具服务注入到 AI 会话中
|
||||
4. AI 在对话中根据需要自动调用外部工具(用户会在界面中看到工具调用的过程和结果)
|
||||
5. 用户无需在每次对话中手动选择要使用的工具
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- MCP 工具服务连接失败:AI 仍可正常对话,但缺少对应外部工具能力
|
||||
- 需要 OAuth 认证的工具服务:系统会引导用户在设置中完成登录授权
|
||||
- 会话恢复时 MCP 工具服务自动重新加载
|
||||
- 工具服务在会话中途不可用:AI 会收到工具调用失败的反馈,并尝试其他方式完成任务
|
||||
|
||||
**重要限制**:
|
||||
|
||||
- MCP 工具服务仅在会话建立时注入,会话中途新增或修改的工具配置需要重新进入会话才生效
|
||||
- 禁用某个工具服务后,已建立的会话不受影响,新建会话才会排除该工具
|
||||
|
||||
**不同 AI 后端的差异**:
|
||||
|
||||
- 部分后端可能不支持 MCP 工具服务
|
||||
- 不同后端支持的 MCP 传输方式可能不同
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 已启用的 MCP 工具服务在进入会话时自动注入
|
||||
- [ ] 支持用户手动配置、系统内置、扩展贡献三种来源的工具服务
|
||||
- [ ] 需要 OAuth 认证的工具服务能引导用户完成授权
|
||||
- [ ] 工具服务加载失败不影响 AI 基本功能
|
||||
- [ ] 会话恢复时自动重新加载工具服务
|
||||
@@ -0,0 +1,34 @@
|
||||
# PRD 索引:Custom Agent(自定义 Agent 配置与连接测试)
|
||||
|
||||
## 文档
|
||||
|
||||
| 文件 | 说明 |
|
||||
| ---------------------------------- | ----------------------- |
|
||||
| [custom-agent.md](custom-agent.md) | PRD 最终版(16 功能点) |
|
||||
|
||||
## 功能点总览
|
||||
|
||||
| 编号 | 功能点 | 状态 | 模块 |
|
||||
| ----------- | ---------------------------------- | ------ | ---- |
|
||||
| F-CAGENT-01 | Agent 设置页入口与 Tab 切换 | 已实现 | 导航 |
|
||||
| F-CAGENT-02 | Detected Agents 列表展示 | 已实现 | 列表 |
|
||||
| F-CAGENT-03 | Custom Agent 列表展示 | 已实现 | 列表 |
|
||||
| F-CAGENT-04 | 创建 Custom Agent | 已实现 | CRUD |
|
||||
| F-CAGENT-05 | 编辑 Custom Agent | 已实现 | CRUD |
|
||||
| F-CAGENT-06 | InlineAgentEditor — Avatar 选择 | 已实现 | 表单 |
|
||||
| F-CAGENT-07 | InlineAgentEditor — 名称与命令输入 | 已实现 | 表单 |
|
||||
| F-CAGENT-08 | InlineAgentEditor — 参数输入 | 已实现 | 表单 |
|
||||
| F-CAGENT-09 | InlineAgentEditor — 环境变量管理 | 已实现 | 表单 |
|
||||
| F-CAGENT-10 | 高级 JSON 编辑器 | 已实现 | 表单 |
|
||||
| F-CAGENT-11 | 连接测试 | 已实现 | 测试 |
|
||||
| F-CAGENT-12 | 保存 Custom Agent | 已实现 | CRUD |
|
||||
| F-CAGENT-13 | 删除 Custom Agent | 已实现 | CRUD |
|
||||
| F-CAGENT-14 | 启用/禁用 Custom Agent | 已实现 | CRUD |
|
||||
| F-CAGENT-15 | Agent 自动检测机制 | 已实现 | 检测 |
|
||||
| F-CAGENT-16 | Custom Agent 数据加载(GuidPage) | 已实现 | 加载 |
|
||||
|
||||
## 工作记录
|
||||
|
||||
- R1: 静态分析(19 功能点)+ 动态分析(12 截图)
|
||||
- R2: PRD 初稿(16 功能点)+ DA 质疑(18 条)+ 测试计划(66 AC, 92% E2E 可执行)
|
||||
- R3: 回应(25 条,21 接受 + 2 部分接受)+ PRD 终稿 + 辩论记录
|
||||
@@ -0,0 +1,667 @@
|
||||
# 设置页 → Agent 管理 → Custom Agent (F-CAGENT)
|
||||
|
||||
> 本文档覆盖「设置 → Agents → 本地 Agents」页面中 **Custom Agent** 相关的全部功能,包括列表展示、创建/编辑/删除/启用禁用、连接测试、Agent 自动检测机制。
|
||||
> 基于静态代码分析和动态 UI 验证综合整理,经 DA 质疑和 Tester 反馈修正定稿。
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-01) Agent 设置页入口与 Tab 切换 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在设置中有一个统一的 Agent 管理入口,可以分别管理本地 Agent 和远端 Agent。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击左侧导航栏「AI 核心」分类下的「Agents」菜单项
|
||||
2. 进入 Agent 设置页(路由 `#/settings/agent`)
|
||||
3. 页面顶部显示两个 Tab:「本地 Agents」(默认激活)和「远端 Agents」
|
||||
4. 点击 Tab 切换内容区域,URL 同步更新 `?tab=local` / `?tab=remote`
|
||||
5. 内容区域使用滚动容器,支持长列表滚动
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- URL 中 `tab` 参数非 `local`/`remote`:保持当前 Tab 选中状态不变(首次加载时为 `local`)(`AgentModalContent.tsx:22-29`——useEffect 不执行时 activeTab 保持上一次设置的值)
|
||||
- 页面模式(`viewMode === 'page'`)下禁用自定义滚动,走原生滚动
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击「Agents」菜单进入设置页
|
||||
- [ ] 默认激活「本地 Agents」Tab
|
||||
- [ ] Tab 切换时 URL query param 同步更新
|
||||
- [ ] 直接访问 `?tab=remote` 可定位到远端 Tab
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-02) Detected Agents 列表展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望看到系统自动检测到的本地已安装 Agent CLI 工具,了解哪些 Agent 可用。
|
||||
|
||||
**前置条件**:系统启动时已通过 `which` 命令扫描 `POTENTIAL_ACP_CLIS` 列表
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 「本地 Agents」Tab 顶部显示说明文字(i18n key: `settings.agentManagement.localAgentsDescription`),末尾有"识别自定义 Agent"链接按钮(i18n key: `settings.agentManagement.detectCustomAgent`)
|
||||
2. 说明文字下方为「已检测」区域标题(i18n key: `settings.agentManagement.detected`)
|
||||
3. 已检测 Agent 以卡片网格布局展示(Tailwind 响应式断点:sm 及以下 2 列、md 3 列、lg 4 列、xl 5 列)
|
||||
4. 每张卡片包含:Agent Logo(40px 方形)、Agent 名称(最多 2 行截断)、"已检测"标签、"设置"按钮
|
||||
5. 卡片排列顺序:Aion CLI 置顶 → Gemini CLI 次之 → 其他按检测顺序排列。若 Aion CLI / Gemini CLI 未检测到,对应位置跳过,其他 Agent 紧邻排列
|
||||
6. Aion CLI 和 Gemini CLI 的"设置"按钮可用,点击分别跳转 `/settings/aionrs` 和 `/settings/gemini`
|
||||
7. 其他 detected Agent 的"设置"按钮为 disabled 状态(hover 时显示 Tooltip 提示,i18n key: `settings.agentManagement.settingsDisabledHint`)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- IPC 调用 `getAvailableAgents` 失败或返回 `success === false`:列表为空,显示空状态文案(i18n key: `settings.agentManagement.localAgentsEmpty`)
|
||||
- 无任何 Agent 被检测到:同上
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- 数据获取:`ipcBridge.acpConversation.getAvailableAgents.invoke()` → 主进程 AgentRegistry
|
||||
- 过滤逻辑(**设置页专用**):排除 `backend === 'remote'`、`backend === 'custom'`、`isPreset === true`(`LocalAgents.tsx:30`)。注意此过滤规则与 `useDetectedAgents` hook 不同——后者仅排除 `isPreset` 和 `remote`,不排除 `custom`(详见 F-CAGENT-15)
|
||||
- SWR key:`acp.agents.available.settings`
|
||||
- Logo 解析优先级:扩展资产 URL → 内置 `resolveAgentLogo` 映射 → fallback emoji '🤖'
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 页面加载后显示已检测 Agent 卡片网格
|
||||
- [ ] Aion CLI 和 Gemini CLI 排列在前,"设置"按钮可用
|
||||
- [ ] 其他 Agent 的"设置"按钮 disabled
|
||||
- [ ] 无检测结果时显示空状态文案(验证策略:单元测试——mock `getAvailableAgents` 返回空数组)
|
||||
- [ ] 卡片网格响应式列数:sm 及以下 2 列、md 3 列、lg 4 列、xl 5 列
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-03) Custom Agent 列表展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望看到我手动添加的自定义 Agent 列表,包括名称、命令、启用状态,以便管理。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 「已检测」区域下方为「自定义 Agents」区域(i18n key: `settings.agentManagement.customAgents`)
|
||||
2. 仅当存在 custom agent 或编辑器弹窗打开时才显示该区域标题
|
||||
3. 每个 custom agent 以行卡片展示,包含:
|
||||
- 左侧:Avatar 容器 32px 方形,emoji 字号 18px,有 emoji 时背景为 `color-fill-2`,无 emoji 时透明背景 + Robot 图标
|
||||
- 名称(无名称显示 "Custom Agent")
|
||||
- 名称下方灰色小字显示 CLI 命令路径及参数(`defaultCliPath` + `acpArgs` 空格拼接),单行截断
|
||||
- 右侧:启用/禁用 Switch + 编辑按钮(EditTwo 图标)+ 删除按钮(Delete 图标,红色)
|
||||
4. Switch 默认开启状态(`enabled !== false`,即 undefined 和 true 均视为开启)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- ConfigStorage 返回 null/undefined:使用空数组,不显示「自定义 Agents」区域
|
||||
- 无 custom agent 且编辑器未打开:不渲染区域标题
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- 数据来源:`ConfigStorage.get('acp.customAgents')` — 渲染进程本地读取
|
||||
- SWR key:`acp.customAgents.settings`
|
||||
- 类型:`AcpBackendConfig[]`
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 存在 custom agent 时显示行卡片列表
|
||||
- [ ] 每行显示 Avatar、名称、命令路径+参数
|
||||
- [ ] Switch 状态与 `enabled` 字段一致
|
||||
- [ ] 无 custom agent 时不显示区域标题
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-04) 创建 Custom Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过表单添加一个新的自定义 Agent,配置其 CLI 命令和参数后保存使用。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击顶部说明文字中的"识别自定义 Agent"链接按钮
|
||||
2. 弹出 Modal 对话框,标题为 `settings.agentManagement.detectCustomAgent`(中文参考:识别自定义 Agent),右上角有关闭按钮
|
||||
3. 弹窗内显示 InlineAgentEditor 表单(新建模式,所有字段为空/默认值):
|
||||
- Avatar:默认 '🤖',点击弹出 EmojiPicker
|
||||
- 显示名称:空,placeholder `settings.agentNamePlaceholder`(中文参考:请输入代理名称)
|
||||
- 命令:空,placeholder `settings.commandPlaceholder`(中文参考:例如 my-agent 或 /usr/local/bin/my-agent)
|
||||
- 参数:空,placeholder `settings.argsPlaceholder`(中文参考:例如 --acp --verbose)
|
||||
- 环境变量:空列表 + "添加变量"按钮
|
||||
- 测试连接按钮(disabled,需填入命令才启用)
|
||||
- 高级 (JSON) 折叠面板(默认收起)
|
||||
- 底部:取消 + 保存(disabled,需填入名称和命令才启用)
|
||||
4. 用户填写表单并点击"保存"
|
||||
5. 系统生成 uuid 作为 agent id,构建 `AcpBackendConfig` 对象
|
||||
6. 写入 `ConfigStorage('acp.customAgents')`,追加到列表末尾
|
||||
7. 弹窗关闭,列表刷新显示新 agent
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 用户可通过右上角关闭按钮、编辑器内取消按钮、或点击遮罩层(AionModal 默认行为)关闭弹窗,均不保存数据
|
||||
- ConfigStorage 写入失败:无 try-catch,异常 bubble up,弹窗可能未关闭(已知局限)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击"识别自定义 Agent"打开创建弹窗
|
||||
- [ ] 弹窗标题为 `settings.agentManagement.detectCustomAgent`
|
||||
- [ ] 表单字段全部为空/默认值
|
||||
- [ ] 名称和命令均填写后"保存"按钮启用(`InlineAgentEditor.tsx:221`)
|
||||
- [ ] 保存后弹窗关闭,列表中出现新 agent
|
||||
- [ ] 取消/关闭/点击遮罩层不保存数据
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-05) 编辑 Custom Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望修改已有 custom agent 的配置(如名称、命令、参数等)。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击 custom agent 行卡片上的编辑图标按钮
|
||||
2. 弹出 Modal 对话框,标题为 `settings.agentManagement.editCustomAgent`(中文参考:编辑自定义 Agent)
|
||||
3. 表单预填现有 agent 数据:
|
||||
- Avatar:当前 emoji(或 fallback '🤖')
|
||||
- 显示名称:当前名称
|
||||
- 命令:当前 `defaultCliPath`
|
||||
- 参数:当前 `acpArgs` 以空格拼接
|
||||
- 环境变量:当前 `env` 对象转为 key-value 列表
|
||||
4. 高级 JSON 编辑器默认收起(`InlineAgentEditor.tsx:131`——`setShowAdvanced(false)` 在 agent effect 中执行)
|
||||
5. 测试状态重置为 idle
|
||||
6. 用户修改字段后点击"保存"
|
||||
7. 系统按 id 查找并替换原有配置,写回 ConfigStorage
|
||||
8. 弹窗关闭,列表刷新
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- `agent.acpArgs` 为 undefined:参数字段显示为空
|
||||
- `agent.env` 为 undefined 或空对象:环境变量列表为空
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击编辑图标打开编辑弹窗
|
||||
- [ ] 弹窗标题为 `settings.agentManagement.editCustomAgent`
|
||||
- [ ] 表单预填现有数据
|
||||
- [ ] 修改后保存成功,列表反映变更
|
||||
- [ ] 保存时保留原有 id 和 enabled 状态
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-06) InlineAgentEditor 表单 — Avatar 选择 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望为自定义 Agent 选择一个 emoji 头像,以便在列表中快速识别。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 表单左上角显示方形 Avatar(48px,圆角 12px)
|
||||
2. 点击 Avatar 弹出 EmojiPicker
|
||||
3. EmojiPicker 显示分类标签(当前默认分类,由 EmojiPicker 组件决定,可能随组件版本变化)
|
||||
4. 选择 emoji 后立即更新 Avatar 显示
|
||||
5. 默认 Avatar 为 '🤖'
|
||||
|
||||
**异常情况**:无
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击 Avatar 弹出 EmojiPicker
|
||||
- [ ] 选择 emoji 后立即更新显示
|
||||
- [ ] 默认值为 '🤖'
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-07) InlineAgentEditor 表单 — 名称与命令输入 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望输入自定义 Agent 的名称和 CLI 命令,这是配置 Agent 的最基本信息。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. **显示名称**字段:
|
||||
- 标签:`settings.agentDisplayName`(中文参考:显示名称)
|
||||
- Input size=large,placeholder `settings.agentNamePlaceholder`
|
||||
- 影响保存按钮状态
|
||||
2. **命令**字段:
|
||||
- 标签:`settings.commandLabel`(中文参考:命令)
|
||||
- Input size=large,placeholder `settings.commandPlaceholder`
|
||||
- 下方帮助文案:`settings.commandHelp`(中文参考:运行 agent CLI 的可执行命令)
|
||||
- 影响保存按钮和测试连接按钮状态
|
||||
|
||||
**按钮禁用逻辑**(`InlineAgentEditor.tsx:221-222`):
|
||||
|
||||
| 条件 | 保存按钮 | 测试连接按钮 |
|
||||
| --------------------- | ----------- | ------------ |
|
||||
| 名称为空 AND 命令为空 | disabled | disabled |
|
||||
| 名称有值 AND 命令为空 | disabled | disabled |
|
||||
| 名称为空 AND 命令有值 | disabled | enabled |
|
||||
| 名称有值 AND 命令有值 | **enabled** | enabled |
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 无最大长度限制(名称和命令均无)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 名称和命令字段均有明确标签和 placeholder
|
||||
- [ ] 命令字段有帮助文案
|
||||
- [ ] 名称和命令均填写后保存按钮启用
|
||||
- [ ] 仅命令填写后测试连接按钮启用,但保存按钮仍为 disabled
|
||||
- [ ] E2E 应显式验证:仅填命令时保存按钮仍为 disabled
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-08) InlineAgentEditor 表单 — 参数输入 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望为 CLI 命令配置额外的启动参数(如 `--acp`、`--verbose`)。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 标签:`settings.argsLabel`(中文参考:参数)
|
||||
2. Input size=large,placeholder `settings.argsPlaceholder`(中文参考:例如 --acp --verbose)
|
||||
3. 下方帮助文案:`settings.argsHelp`(中文参考:传递给命令的空格分隔参数)
|
||||
4. 用户输入空格分隔的参数字符串
|
||||
5. 支持引号包裹(单引号/双引号)包含空格的参数
|
||||
|
||||
**解析规则**(`parseArgsString`,`InlineAgentEditor.tsx:34-59`):
|
||||
|
||||
- 空格分隔 token
|
||||
- 单/双引号内的空格不分割
|
||||
- 引号不保留在结果中
|
||||
- 未闭合引号:当前 token 照常推入(不报错不提示)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 输入为空:提交时 `acpArgs` 设为 `undefined`(不传空数组)
|
||||
- 未闭合引号:静默处理,可能导致参数解析不符合用户预期
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 参数字段有标签、placeholder 和帮助文案
|
||||
- [ ] 空格分隔的参数正确解析为数组
|
||||
- [ ] 引号包裹的参数作为整体保留
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-09) InlineAgentEditor 表单 — 环境变量管理 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望为 Agent CLI 进程配置自定义环境变量(如 API Key、DEBUG 开关等)。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 标签:`settings.envLabel`(中文参考:环境变量)
|
||||
2. 初始为空列表
|
||||
3. 用户点击"添加变量"按钮(`settings.addEnvVar`),底部追加一行:Key 输入框 + Value 输入框 + 删除按钮
|
||||
4. 布局:三列网格(Key 1fr, Value 1.4fr, 删除按钮 auto)
|
||||
5. 用户填写 Key 和 Value
|
||||
6. 可继续添加多行
|
||||
7. 点击行末删除按钮移除该行
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- Key 为空(仅空格)的行:提交时被跳过,不写入配置
|
||||
- 多行同名 Key:后面的覆盖前面的(Record 语义)
|
||||
- 无 Key/Value 格式校验、无数量限制
|
||||
- 提交时所有 Key 为空:`env` 设为 `undefined`(不传空对象)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] "添加变量"按钮可追加 Key-Value 行
|
||||
- [ ] 每行的删除按钮可移除该行
|
||||
- [ ] Key 和 Value 输入框可编辑
|
||||
- [ ] Key 为空的行提交时被忽略
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-10) 高级 JSON 编辑器 [已实现]
|
||||
|
||||
**用户故事**:作为高级用户,我希望直接编辑 Agent 配置的 JSON 源码,方便批量修改或精确控制配置。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 表单底部有可折叠面板"高级 (JSON)"(i18n key: `settings.advancedMode`),创建和编辑模式下均默认收起(`InlineAgentEditor.tsx:131`——`setShowAdvanced(false)`)
|
||||
2. 点击展开后显示 CodeMirror JSON 编辑器(高度 200px)
|
||||
3. 编辑器内容为当前表单数据的 JSON 表示:
|
||||
```json
|
||||
{
|
||||
"name": "TestAgent",
|
||||
"defaultCliPath": "bun",
|
||||
"enabled": true,
|
||||
"acpArgs": ["run", ".../agent.ts", "acp"],
|
||||
"env": {}
|
||||
}
|
||||
```
|
||||
4. JSON 语法高亮、行号显示、代码折叠
|
||||
5. **双向同步**:
|
||||
- 修改表单字段 → JSON 编辑器自动更新
|
||||
- 修改 JSON → 表单字段实时同步(name, defaultCliPath, acpArgs, env)
|
||||
6. `avatar` 字段不在 JSON 中——Emoji 头像仅通过 EmojiPicker 设置
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- JSON 格式错误:编辑器边框变红,下方显示 "Invalid JSON" 错误提示(此文案为硬编码,未做 i18n)
|
||||
- JSON 中的 `enabled` 字段完全不被 handleSubmit 读取(`InlineAgentEditor.tsx:214`——`enabled: agent?.enabled !== false`)。新建时 enabled 固定为 `true`(`undefined !== false` → `true`);编辑时保留 props 传入的原值
|
||||
- JSON 中添加额外字段:不被表单消费,且保存时会被丢弃(handleSubmit 重新构建对象,仅包含表单字段——详见附录 D)
|
||||
- JSON 编辑后 500ms 内修改表单字段:JSON 编辑器可能短暂保持旧值(`isJsonEditingRef` 竞争窗口)
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- 同步方向由 `isJsonEditingRef` 控制:JSON 编辑后 500ms(setTimeout)自动切回表单主导
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 折叠面板可展开/收起,创建和编辑模式下均默认收起
|
||||
- [ ] JSON 内容与表单数据保持同步(双向)
|
||||
- [ ] JSON 格式错误时显示红色边框和错误提示
|
||||
- [ ] 修改 JSON 中的 name/defaultCliPath/acpArgs/env 后表单相应更新
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-11) 连接测试 [已实现]
|
||||
|
||||
> 建议验证策略:Step 1 CLI 检测可通过单元测试覆盖;Step 2 ACP 连接通过集成测试(mock ProcessAcpClient);E2E 验证 UI 状态切换(有 fake agent 可触发三种结果)
|
||||
|
||||
**用户故事**:作为用户,我希望在保存前测试 Agent 的连接是否正常,确认 CLI 命令可用且 ACP 协议能正常工作。
|
||||
|
||||
**前置条件**:命令字段已填写(非空)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击"测试连接"按钮(全宽 outline 样式,i18n key: `settings.testConnectionBtn`)
|
||||
2. 按钮变为 loading 态(旋转动画 + `settings.testConnectionTesting` 文案)
|
||||
3. 后端执行两步检测:
|
||||
- **Step 1 — CLI 检测**:使用 `which`(macOS/Linux)或 `where`(Windows)检查 `command.split(' ')[0]` 是否存在(超时 5 秒,`execFileSync` + `timeout: 5000`)
|
||||
- **Step 2 — ACP 连接**:通过 `ProcessAcpClient.start()` 启动并连接 CLI 进程(内部包含进程 spawn 和 ACP 协议初始化),成功后调用 `client.close()` 清理
|
||||
4. 结果以 Alert 显示在按钮下方(`role="alert"` + `aria-live="assertive"`):
|
||||
|
||||
| 结果 | Alert 类型 | 图标 | i18n Key | 当前中文参考 |
|
||||
| -------------- | --------------- | -------- | -------------------------------- | --------------------------------------- |
|
||||
| 成功 | success(绿色) | CheckOne | `settings.testConnectionSuccess` | 连接成功!CLI 存在且 ACP 协议正常工作。 |
|
||||
| CLI 未找到 | error(红色) | CloseOne | `settings.testConnectionFailCli` | 未找到命令。请确保已安装并在 PATH 中。 |
|
||||
| ACP 初始化失败 | warning(黄色) | CloseOne | `settings.testConnectionFailAcp` | 找到 CLI 但 ACP 初始化失败。 |
|
||||
|
||||
5. 测试完成后可再次点击重新测试
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 测试进行中关闭弹窗:后台进程不会被取消(已知局限:无取消机制。非 bug,为设计约束)
|
||||
- ACP 连接步骤(Step 2)无超时机制:如果 CLI 进程 hang,测试可能永远不返回(已知局限)
|
||||
- IPC 调用本身抛出异常时(如 IPC 通道断开,非后端正常错误返回):统一显示为 fail_cli(`InlineAgentEditor.tsx:201-203`)
|
||||
- 每次打开编辑器或切换 agent 时,测试状态重置为 idle
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- IPC 链路:`acpConversation.testCustomAgent.invoke({ command, acpArgs?, env? })` → 主进程 `testCustomAgentConnection()`
|
||||
- Step 1 使用 `execFileSync`(同步阻塞),timeout 5000ms
|
||||
- Step 2 使用 `ProcessAcpClient` + `spawnGenericBackend('custom', ...)`,工作目录为 `os.tmpdir()`
|
||||
- 测试仅验证可连接性,不验证功能完整性
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 命令为空时测试按钮 disabled
|
||||
- [ ] 点击测试后显示 loading 状态
|
||||
- [ ] 三种测试结果分别显示对应的 Alert(颜色、图标、i18n 文案)
|
||||
- [ ] 测试成功后可重新测试
|
||||
- [ ] CLI 不存在时在 5 秒(Step 1 超时限制)内返回 fail_cli 结果
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-12) 保存 Custom Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望保存自定义 Agent 配置后立即在列表中看到更新。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在表单中填写/修改完毕,点击"保存"按钮(i18n key: `common.save`)
|
||||
2. 系统构建 `AcpBackendConfig` 对象:
|
||||
- 新建时 `id = uuid()`,编辑时保留原 `id`
|
||||
- `enabled`:新建时固定为 `true`;编辑时保留 props 传入的原值(`agent?.enabled !== false`)
|
||||
- `acpArgs`:解析参数字符串,为空则 `undefined`
|
||||
- `env`:转换 key-value 列表,为空则 `undefined`
|
||||
3. 从 ConfigStorage 读取最新列表(非 SWR 缓存,避免并发问题)
|
||||
4. 按 id 查找:存在则替换(更新),不存在则追加(创建)
|
||||
5. 写回 ConfigStorage
|
||||
6. 触发 SWR mutate 刷新列表
|
||||
7. 弹窗关闭,表单状态清空
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- ConfigStorage 读写失败:无 try-catch,异常 bubble up(弹窗可能不关闭)
|
||||
- 无重名校验(允许多个同名 agent)
|
||||
- 无命令重复校验
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- handleSubmit(`InlineAgentEditor.tsx:206-219`)重新构建 `AcpBackendConfig` 对象,仅包含表单暴露的 5 个字段(name, avatar, defaultCliPath, acpArgs, env)+ id + enabled。JSON 高级编辑器中手动添加的额外字段会被丢弃——详见附录 D
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 新建保存后列表追加新 agent
|
||||
- [ ] 编辑保存后列表反映修改
|
||||
- [ ] 保存后弹窗自动关闭
|
||||
- [ ] 编辑保存时保留原 id 和 enabled 状态
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-13) 删除 Custom Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望删除不再需要的自定义 Agent。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击 custom agent 行卡片右侧的删除按钮(红色 Delete 图标)
|
||||
2. **无确认对话框** — 直接执行删除
|
||||
3. 从 ConfigStorage 读取列表,按 id 过滤掉目标 agent,写回
|
||||
4. SWR 刷新,列表中该 agent 立即消失
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 操作不可撤销,无 undo 机制
|
||||
- ConfigStorage 读写失败:无 try-catch
|
||||
- 删除正在对话中使用的 agent:可能导致该对话异常(需进一步验证)
|
||||
|
||||
**已知局限**:
|
||||
|
||||
- 缺少删除确认对话框是一个 UX 风险,用户可能误触删除
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击删除按钮后 agent 立即从列表消失(无确认弹窗)
|
||||
- [ ] 删除后 ConfigStorage 中不再包含该 agent
|
||||
- [ ] 删除是不可恢复操作
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-14) 启用/禁用 Custom Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望临时禁用某个自定义 Agent 而不删除它,需要时再启用。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 每个 custom agent 行卡片右侧有 Switch 开关(size=small)
|
||||
2. 默认为开启状态(`enabled !== false`)
|
||||
3. 用户切换 Switch:
|
||||
- Switch `onChange` 回调传入切换后的目标 `enabled` 值(非取反逻辑),直接写入 ConfigStorage
|
||||
- 从 ConfigStorage 读取最新列表,按 id 更新目标 agent 的 `enabled` 字段
|
||||
- 写回 ConfigStorage,SWR 刷新
|
||||
4. 禁用后 agent 仍在列表中显示,仅 Switch 为关闭态
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 如果 id 在列表中不存在(极端情况):安全检查 `updatedAgents.some()` 阻止写入
|
||||
- 禁用后对 agent 可用性的影响:`useCustomAgentsLoader` 通过 `availableCustomAgentIds` 过滤,禁用的 agent 不会出现在对话选择中
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Switch 切换后立即更新 enabled 状态
|
||||
- [ ] 禁用的 agent 仍在列表中显示
|
||||
- [ ] 禁用的 agent 不出现在对话选择的 agent 列表中
|
||||
- [ ] 无额外确认步骤
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-15) Agent 自动检测机制 [已实现]
|
||||
|
||||
> 建议验证策略:检测逻辑为后端实现,E2E 仅验证已检测列表的显示结果
|
||||
|
||||
**用户故事**:作为用户,我希望系统能自动检测本地安装的 Agent CLI 工具,无需手动配置即可使用。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 系统启动时自动扫描已知 ACP CLI 工具列表
|
||||
2. 对每个候选 CLI 执行 `which` 命令检测是否安装
|
||||
3. 检测结果以 `DetectedAgent` 类型返回,包含 kind-specific 字段
|
||||
4. 用户在设置页看到已检测 Agent 的卡片列表
|
||||
5. `useDetectedAgents` hook 提供 `refreshAgentDetection()` 方法可触发重新扫描
|
||||
|
||||
**检测范围**:
|
||||
|
||||
- 所有在 `ACP_BACKENDS_ALL` 中 `enabled=true` 且有 `cliCommand` 的后端(排除 `custom`)
|
||||
- 执行引擎层分类(`DetectedAgentKind`,`detectedAgent.ts:27`):`gemini`、`acp`、`remote`、`aionrs`、`openclaw-gateway`、`nanobot`。注意此类型与 ACP 协议层分类 `AcpBackendAll`(18 种 ACP 后端)是不同维度——DetectedAgentKind 区分执行引擎/通信协议,AcpBackendAll 区分具体 ACP CLI 产品
|
||||
|
||||
**过滤规则差异**:
|
||||
|
||||
- 设置页 `LocalAgents.tsx:30`:排除 remote + custom + preset
|
||||
- `useDetectedAgents.ts:26`:仅排除 preset + remote(不排除 custom)——此 hook 用于后端选择器(如 AssistantEditDrawer),需要包含 custom 类型
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- IPC 调用失败:返回空数组(`fetchDetectedAgents` catch → [])
|
||||
- `refreshCustomAgents.invoke()` 失败:静默忽略
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- `POTENTIAL_ACP_CLIS` 使用 Proxy 延迟初始化,从 `ACP_BACKENDS_ALL` 自动生成,避免数据冗余和循环依赖
|
||||
- Custom agent 走不同的数据通路(ConfigStorage),不参与自动检测
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 系统启动后自动显示已检测的 Agent
|
||||
- [ ] 刷新检测后列表更新(验证策略:内部行为,由 SWR revalidation 自动触发;手动触发场景建议通过单元测试覆盖)
|
||||
- [ ] Custom agent 不出现在 detected 列表中
|
||||
|
||||
---
|
||||
|
||||
## (F-CAGENT-16) Custom Agent 数据加载(GuidPage 场景)[已实现]
|
||||
|
||||
> 建议验证策略:合并逻辑通过 React Testing Library mock ConfigStorage 和 IPC 覆盖;E2E 仅验证最终显示结果
|
||||
|
||||
**用户故事**:作为用户,我希望在新建对话选择 Agent 时看到所有可用的 custom agent,包括预设助手和扩展贡献的 agent。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户进入 GuidPage(引导页/对话选择页)
|
||||
2. 系统加载并合并三个数据源:
|
||||
- 预设助手(`ConfigStorage('assistants')` 中 `isPreset === true`)
|
||||
- 用户自定义 agent(`ConfigStorage('acp.customAgents')` 中被 `availableCustomAgentIds` 过滤的条目)
|
||||
- 扩展贡献的助手(`ipcBridge.extensions.getAssistants.invoke()`,去重——已有 id 跳过)
|
||||
3. 返回合并后的列表及 `customAgentAvatarMap`(id → avatar 映射)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- `extensions.getAssistants.invoke()` 失败:catch → 空数组,不影响其他数据
|
||||
- `loadCustomAgents` 整体失败:`console.error`,不影响应用其他功能
|
||||
|
||||
**技术说明(两次加载机制)**:
|
||||
|
||||
`useCustomAgentsLoader` 内部有两个独立的 useEffect:
|
||||
|
||||
1. **Initial load**(`useCustomAgentsLoader.ts:73-75`):仅读 ConfigStorage + extensions,触发条件为 `loadCustomAgents` 引用变化
|
||||
2. **Refresh**(`useCustomAgentsLoader.ts:88-90`):调用 IPC `refreshCustomAgents.invoke()` → SWR mutate(`DETECTED_AGENTS_SWR_KEY`) → 重新读 ConfigStorage。触发条件为 `refreshCustomAgents` 引用变化
|
||||
|
||||
两次加载可能导致列表短暂闪烁(已知局限)。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] GuidPage 显示可用的 custom agent
|
||||
- [ ] 预设助手、用户自定义和扩展贡献正确合并(验证策略:单元测试)
|
||||
- [ ] 扩展贡献的 agent 去重(验证策略:单元测试)
|
||||
- [ ] 仅显示后端确认可用的 custom agent
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:IPC 通信链路
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 渲染进程 (Renderer) │
|
||||
│ │
|
||||
│ LocalAgents │
|
||||
│ ├─ ConfigStorage.get('acp.customAgents') → 本地读取 │
|
||||
│ ├─ ConfigStorage.set('acp.customAgents', [...])→ 本地写入 │
|
||||
│ └─ ipcBridge.acpConversation │
|
||||
│ .getAvailableAgents.invoke() → IPC invoke │
|
||||
│ │
|
||||
│ InlineAgentEditor │
|
||||
│ └─ acpConversation │
|
||||
│ .testCustomAgent.invoke({...}) → IPC invoke │
|
||||
│ │
|
||||
│ useDetectedAgents │
|
||||
│ ├─ acpConversation.getAvailableAgents.invoke() → IPC invoke │
|
||||
│ └─ acpConversation.refreshCustomAgents.invoke()→ IPC invoke │
|
||||
│ │
|
||||
│ useCustomAgentsLoader │
|
||||
│ ├─ ConfigStorage.get('assistants') → 本地读取 │
|
||||
│ ├─ ConfigStorage.get('acp.customAgents') → 本地读取 │
|
||||
│ ├─ extensions.getAssistants.invoke() → IPC invoke │
|
||||
│ └─ acpConversation.refreshCustomAgents.invoke()→ IPC invoke │
|
||||
└────────────────────────┬─────────────────────────────────────────┘
|
||||
│ IPC Bridge
|
||||
┌────────────────────────▼─────────────────────────────────────────┐
|
||||
│ 主进程 (Main) │
|
||||
│ │
|
||||
│ testCustomAgentConnection.ts │
|
||||
│ ├─ Step 1: execFileSync('which'/'where', [baseCmd]) — 5s 超时 │
|
||||
│ └─ Step 2: ProcessAcpClient.start() → spawn + ACP init │
|
||||
│ │
|
||||
│ AgentRegistry (getAvailableAgents provider) │
|
||||
│ └─ 扫描 POTENTIAL_ACP_CLIS + ConfigStorage custom agents │
|
||||
│ │
|
||||
│ refreshCustomAgents provider │
|
||||
│ └─ 重新扫描并更新 agent 可用性 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:Toast / Alert 消息汇总
|
||||
|
||||
> 以 i18n key 为权威列,中文文案为当前参考(以 i18n 翻译文件为准)
|
||||
|
||||
| 场景 | 组件 | 类型 | i18n Key | 当前中文参考 |
|
||||
| -------------- | --------------- | ---- | -------------------------------- | --------------------------------------- |
|
||||
| 连接测试成功 | Alert (success) | 绿色 | `settings.testConnectionSuccess` | 连接成功!CLI 存在且 ACP 协议正常工作。 |
|
||||
| CLI 检测失败 | Alert (error) | 红色 | `settings.testConnectionFailCli` | 未找到命令。请确保已安装并在 PATH 中。 |
|
||||
| ACP 初始化失败 | Alert (warning) | 黄色 | `settings.testConnectionFailAcp` | 找到 CLI 但 ACP 初始化失败。 |
|
||||
| JSON 解析失败 | 内联文本 | 红色 | **硬编码** | "Invalid JSON"(未 i18n) |
|
||||
|
||||
---
|
||||
|
||||
## 附录 C:已知局限汇总
|
||||
|
||||
| # | 功能点 | 局限描述 |
|
||||
| --- | ----------- | --------------------------------------------------------------------- |
|
||||
| 1 | F-CAGENT-13 | 删除无确认对话框,不可撤销,存在误操作风险 |
|
||||
| 2 | F-CAGENT-11 | ACP 连接步骤(Step 2)无超时机制,CLI 进程 hang 时测试永远不返回 |
|
||||
| 3 | F-CAGENT-11 | 测试进行中关闭弹窗,后台进程不会被取消(设计约束,非 bug) |
|
||||
| 4 | F-CAGENT-10 | "Invalid JSON" 错误信息未 i18n |
|
||||
| 5 | F-CAGENT-10 | handleSubmit 重建对象时丢弃 JSON 高级编辑器中的额外字段(详见附录 D) |
|
||||
| 6 | F-CAGENT-12 | ConfigStorage 读写操作无 try-catch,失败时弹窗可能异常 |
|
||||
| 7 | F-CAGENT-07 | 名称和命令字段无最大长度限制 |
|
||||
| 8 | F-CAGENT-09 | 环境变量无数量限制,无 Key/Value 格式校验 |
|
||||
| 9 | F-CAGENT-08 | 未闭合引号静默处理,可能导致参数解析不符合用户预期 |
|
||||
| 10 | F-CAGENT-16 | useEffect 双重加载(initial + refresh)可能导致列表短暂闪烁 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 D:设计约束
|
||||
|
||||
### D-1: Custom Agent 编辑器字段覆盖范围
|
||||
|
||||
`AcpBackendConfig` 接口(`acpTypes.ts:124-302`)包含 30+ 字段,但 Custom Agent 编辑器(InlineAgentEditor)仅暴露以下 5 个字段供用户配置:
|
||||
|
||||
| 表单字段 | AcpBackendConfig 字段 | 说明 |
|
||||
| -------- | --------------------- | ---------------------------------------------- |
|
||||
| 显示名称 | `name` | 必填 |
|
||||
| Avatar | `avatar` | emoji,仅通过 EmojiPicker 设置(不在 JSON 中) |
|
||||
| 命令 | `defaultCliPath` | 必填 |
|
||||
| 参数 | `acpArgs` | 可选,空格分隔解析为数组 |
|
||||
| 环境变量 | `env` | 可选,key-value 对 |
|
||||
|
||||
以下字段由系统自动管理,不在编辑器中暴露:
|
||||
|
||||
- `id`:新建时自动生成 uuid,编辑时保留原值
|
||||
- `enabled`:通过列表中的 Switch 控制
|
||||
|
||||
其余 AcpBackendConfig 字段(如 `authRequired`, `supportsStreaming`, `skillsDirs`, `isPreset`, `context`, `models`, `enabledSkills` 等)对 custom agent 不适用或不可配置。如果用户通过 JSON 高级编辑器手动添加这些字段,handleSubmit 会丢弃它们(重新构建对象仅包含上述字段)。
|
||||
|
||||
### D-2: Agent Hub 入口(仅开发环境)
|
||||
|
||||
`LocalAgents.tsx:104-132` 包含一个 Agent Hub 市场入口横幅,仅在 `process.env.NODE_ENV === 'development'` 时渲染。生产环境用户不可见。该功能为开发中的 Agent 市场预留入口,不属于当前正式功能范围。
|
||||
@@ -0,0 +1,155 @@
|
||||
# 对话 - 模型选择器(父级 + 二级子菜单)
|
||||
|
||||
> 本文档聚焦「模型选择器」的下拉交互重构。
|
||||
> 涉及范围:对话面板(单聊 + 群聊,含 Claude/Codex 等 ACP 平台与 Aion CLI/aionrs 平台)以及首页新建对话页的模型选择器。不改后端,全部在渲染层完成。
|
||||
|
||||
---
|
||||
|
||||
## 背景与目标
|
||||
|
||||
**现状**:模型选择器承载两组配置——「模型」和「推理强度」(thought level / reasoning effort)。当前下拉把两组竖着堆在同一个菜单里(上面一组推理强度,分隔线,下面一组模型)。
|
||||
|
||||
**问题**:当模型列表很长时,加上推理强度那一组,整个下拉菜单被撑得很长,需要大量滚动,不好用。模型越多越明显。
|
||||
|
||||
**目标**:把下拉改成**一级菜单 + 二级子菜单**的结构。一级菜单永远只有两行(模型、推理强度),各组的完整选项收进各自的二级子菜单(hover 展开)。这样无论模型多少,一级菜单都不会被撑长。
|
||||
|
||||
**名词说明**:
|
||||
|
||||
- **模型选择器**:对话输入区/面板右上角,显示当前模型与推理强度的按钮(pill),点击展开下拉。
|
||||
- **推理强度**:部分 Agent 支持的思考程度配置(如 轻度 / 中 / 高 / 极高)。并非所有 Agent 都有。
|
||||
- **一级菜单 / 二级子菜单**:点击 pill 后先看到的是一级菜单;hover 一级菜单某行后,在其一侧弹出的是二级子菜单。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-01) 只有模型、无推理强度 → 保持现状 [不变]
|
||||
|
||||
**用户故事**:作为使用不支持推理强度的 Agent 的用户,我点击模型选择器时希望直接看到模型列表,简单直接。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 点击 pill,下拉**直接展开模型列表**(无一级/二级结构)。
|
||||
2. 选中某个模型即切换,当前模型打勾。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 无推理强度的 Agent,下拉直接是模型列表,交互与当前版本一致。
|
||||
- [ ] 模型数量超过阈值时,列表顶部出现搜索框(见 F-MODEL-04)。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-02) 有模型 + 推理强度 → 一级菜单两行 [新增]
|
||||
|
||||
**用户故事**:作为使用支持推理强度的 Agent 的用户,我点开选择器时希望先看到简洁的两行概览,而不是一长串选项。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 点击 pill,一级菜单展开,**只有两行**,自上而下:
|
||||
- **模型**:右侧显示当前模型名 + `›` 箭头。
|
||||
- **推理强度**:右侧显示当前强度 + `›` 箭头。
|
||||
2. 顺序固定为**模型在上、推理强度在下**,与 pill 上「模型 · 推理强度」的显示顺序一致。
|
||||
3. hover(或点击)任一行,在其一侧弹出对应的二级子菜单。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 有推理强度时,一级菜单固定两行,模型在上、推理强度在下。
|
||||
- [ ] 每行显示对应的当前值和 `›` 箭头。
|
||||
- [ ] hover 一级菜单某行时该行高亮并弹出二级子菜单。
|
||||
- [ ] 一级菜单高度不随模型数量变化。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-03) 二级子菜单:选项列表 [新增]
|
||||
|
||||
**用户故事**:作为用户,我 hover 到「模型」或「推理强度」后,希望在弹出的子菜单里看到完整选项并选择。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. hover(或点击)「模型」→ 弹出模型列表;「推理强度」→ 弹出强度列表。
|
||||
2. 每个选项:当前选中项左侧打 ✓。选项若带说明,通过 **hover Tooltip** 展示(并非所有选项都有说明,用 Tooltip 保持列表对齐整齐,不做参差的副标题)。
|
||||
3. 点击某选项即切换,pill 与一级菜单的当前值同步更新。
|
||||
|
||||
**弹出方向**:
|
||||
|
||||
- 子菜单默认往**左**弹出(模型选择器位于对话面板右上角,右侧空间不足)。
|
||||
- 当左侧空间也不足时,自动翻向可容纳的一侧。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] hover 或点击一级行弹出对应二级子菜单,内容正确。
|
||||
- [ ] 当前项打 ✓,有说明的选项通过 hover Tooltip 展示,列表保持对齐。
|
||||
- [ ] 点击选项即切换并同步 pill / 一级菜单显示。
|
||||
- [ ] 子菜单默认向左弹出,空间不足时自动翻向另一侧,不被窗口边缘截断。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-04) 模型二级子菜单:搜索 + 固定高度滚动 [新增]
|
||||
|
||||
**用户故事**:作为模型很多的用户,我希望能在子菜单里搜到目标模型,并且列表再长也不会撑爆屏幕。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 模型二级子菜单在**模型数量超过阈值(5 个)**时,顶部显示搜索框。
|
||||
2. 输入关键字即时过滤模型(按模型名匹配,忽略大小写)。
|
||||
3. 模型列表有**固定最大高度**,超出时在子菜单内部滚动,不影响菜单整体尺寸。
|
||||
4. 无匹配结果时给出空状态提示。
|
||||
|
||||
**范围一致性**:
|
||||
|
||||
- 搜索能力跟随「模型列表」本身,而非「是否有二级菜单」。即无推理强度时(F-MODEL-01 的直接列表)同样按阈值显示搜索框,两种情况逻辑一致。
|
||||
- 推理强度选项通常很少,**不加搜索**。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 模型数 > 5 时,模型列表顶部显示搜索框;≤ 5 不显示。
|
||||
- [ ] 输入即时过滤,按模型名忽略大小写匹配。
|
||||
- [ ] 模型列表固定最大高度 + 内部滚动,菜单整体尺寸不被撑大。
|
||||
- [ ] 无匹配时显示空状态。
|
||||
- [ ] F-MODEL-01 的直接模型列表遵循同一搜索显示规则。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-05) 各入口场景一致 [新增]
|
||||
|
||||
**用户故事**:作为用户,我希望不管在哪里选模型,交互都一致。
|
||||
|
||||
**覆盖入口**:
|
||||
|
||||
- **ACP 平台**(Claude / Codex 等)单聊与群聊 —— 扁平模型列表。
|
||||
- **首页新建对话页** —— ACP agent 扁平模型列表;aionrs 走 provider 分组(见 F-MODEL-06)。
|
||||
- **Aion CLI(aionrs)平台** —— 见 F-MODEL-06(模型按 provider 分组)。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 各入口共用同一套一级/二级菜单结构、搜索/滚动逻辑与向左弹出方向。
|
||||
2. 位于面板右上角的选择器,二级子菜单一律向左弹出。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] ACP 单聊、群聊、首页三处交互一致。
|
||||
- [ ] 二级子菜单弹出方向正确、不被窗口边缘截断。
|
||||
|
||||
---
|
||||
|
||||
## (F-MODEL-06) Aion CLI 平台:模型按 provider 分组 [新增]
|
||||
|
||||
**用户故事**:作为使用 Aion CLI 的用户,我的模型是按 provider(如 Anthropic / OpenAI)组织的,我希望在二级子菜单里仍按 provider 分组查看,同时能搜索。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 一级/二级结构与其它入口一致:一级两行(模型、推理强度),hover 弹出二级。
|
||||
2. 模型二级子菜单里**保留 provider 分组标题**(不显示健康状态圆点,各入口样式统一)。
|
||||
3. 顶部搜索框**跨所有分组过滤**模型名;只显示有命中的分组,无命中时显示空状态。
|
||||
4. 搜索框按总模型数超过阈值时显示。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Aion CLI 模型二级子菜单按 provider 分组(不显示健康圆点)。
|
||||
- [ ] 搜索跨分组过滤,仅显示命中的分组,无命中显示空状态。
|
||||
- [ ] 一级两行结构与向左弹出与其它入口一致。
|
||||
|
||||
---
|
||||
|
||||
## 待讨论模块
|
||||
|
||||
- **搜索组件复用**:搜索框计划复用已抽离的 `AionInlineSearchInput`(下拉列表专用轻量搜索框,见 PR #3532)。开发时该组件尚未合并到主干,已按其相同接口内联一份等价组件(同名同路径),待 #3532 合并后可直接合并、无需改调用方。
|
||||
- **搜索阈值**:当前定为模型数 > 5 才显示搜索框。后续可根据反馈调整。
|
||||
@@ -0,0 +1,803 @@
|
||||
# 设置页 → Agents → 远端 Agents (F-RAGENT)
|
||||
|
||||
> 本文档覆盖「设置 → Agents → 远端 Agents」页面的全部功能,包括 Remote Agent 的列表展示、创建/编辑/删除 (CRUD)、连接测试、OpenClaw 配对握手、会话管理、消息收发与流式响应、工具调用展示、权限审批、连接状态管理。
|
||||
> 基于静态代码分析和动态 UI 验证综合整理,经 DA 质疑(23 条)和 Tester 反馈(8 条)修正定稿。
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-01) Remote Agent 列表展示 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在远端 Agents 页面看到所有已配置的远端 Agent 列表,了解它们的名称、状态和协议类型。
|
||||
|
||||
**前置条件**:用户已进入「设置 → Agents → 远端 Agents」Tab
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户打开「设置 → Agents」页面,点击「远端 Agents」Tab(URL query: `?tab=remote`)
|
||||
2. 页面顶部显示说明横幅:"目前只支持远程连接 OpenClaw,其他 Agent 正在开发中。" + "查看配置指南"链接
|
||||
3. 右上角显示"+ 添加"按钮
|
||||
4. 下方以响应式网格布局展示已配置的 Agent 卡片
|
||||
- 小屏: 1 列 (`grid-cols-1`)
|
||||
- 中屏: 2 列 (`md:grid-cols-2`)
|
||||
- 大屏: 3 列 (`xl:grid-cols-3`)
|
||||
5. 每张卡片显示:
|
||||
- 居中的 Avatar(48x48px, 方形 borderRadius=12, emoji 字号 24px;无 avatar 时显示 Robot outline 图标 18px, @icon-park/react)
|
||||
- 名称(14px 加粗,最多 2 行截断 `line-clamp-2`)
|
||||
- 标签区域:连接状态标签(条件渲染)+ 协议标签(始终渲染, arcoblue)
|
||||
- URL(12px 次级文字,最多 2 行截断 `line-clamp-2`)
|
||||
- 底部操作按钮:"编辑" + "删除"(红色 danger 样式)
|
||||
|
||||
**状态标签渲染**:
|
||||
|
||||
渲染条件:`agent.status` 存在且不为 `'unknown'` 时渲染标签。`status` 为 `undefined` 时同样不渲染。
|
||||
|
||||
颜色映射(`statusColor` 函数):
|
||||
|
||||
| status 值 | 颜色 |
|
||||
| ----------------------------------- | ---------------------- |
|
||||
| `'connected'` | green |
|
||||
| `'pending'` | orange |
|
||||
| `'error'` | red |
|
||||
| 其他(含 `'unknown'`, `undefined`) | gray(但渲染层不渲染) |
|
||||
|
||||
**空列表状态**:
|
||||
|
||||
- 居中文案提示(14px, `settings.remoteAgent.emptyTitle`)+ "添加"按钮(`settings.remoteAgent.emptyAction`)
|
||||
- 无卡片渲染
|
||||
|
||||
**"查看配置指南"链接**:
|
||||
|
||||
- 点击通过 `openExternalUrl` 在系统浏览器中打开 `https://github.com/iOfficeAI/AionUi/wiki/Remote-Agent-Guide-Chinese`
|
||||
- 说明横幅和弹窗内的警告 banner 中均有此链接
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 外部链接打开失败:仅 `console.error`(通过 `.catch(console.error)`),无用户提示
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Tab 切换正确,URL query parameter `?tab=remote` 双向同步
|
||||
- [ ] 响应式网格在三种屏幕宽度下正确布局(1/2/3 列)
|
||||
- [ ] 卡片信息完整(头像、名称、状态标签、协议标签、URL、操作按钮)
|
||||
- [ ] `status` 为 `'unknown'` 或 `undefined` 时不显示状态标签
|
||||
- [ ] 空列表显示引导文案(`settings.remoteAgent.emptyTitle`)和添加按钮
|
||||
- [ ] "查看配置指南"链接在系统浏览器中打开目标 URL
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-02) 创建 Remote Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过表单配置一个新的远端 Agent,填写连接信息并保存,以便后续使用该 Agent 进行对话。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击"+ 添加"按钮
|
||||
2. 弹出"添加远程 Agent"弹窗(AionModal + 遮罩层)
|
||||
3. 弹窗顶部显示黄色警告 banner(说明文案 + "查看配置指南"链接)
|
||||
4. 用户填写表单:
|
||||
- **Avatar**:点击头像区域打开 Emoji 选择器(8 个分类 Tab),选中后立即应用。默认值 `🤖`(`\u{1F916}`,固定值非随机)
|
||||
- **名称**(必填):Input size=large,placeholder `settings.remoteAgent.namePlaceholder`
|
||||
- **URL**(必填):placeholder `wss://example.com/gateway`
|
||||
- **认证方式**(必填):下拉选择,选项:"无" (none) / "Bearer Token" (bearer)。默认"无"。注:类型定义支持 `'password'` 但 UI 不暴露此选项(见设计约束)
|
||||
- **认证令牌**(条件必填):仅当认证方式为 "Bearer Token" 时显示,Input.Password(有眼睛图标切换可见性),placeholder `settings.remoteAgent.tokenPlaceholder`
|
||||
- **允许不安全连接**(条件显示):仅当 URL 以 `wss://` 开头时显示,Switch 默认关闭,附说明 `settings.remoteAgent.allowInsecureHint`
|
||||
5. 用户可选择先"测试连接"(见 F-RAGENT-05)
|
||||
6. 用户点击"保存"
|
||||
7. 表单验证通过后,创建配置并自动触发 OpenClaw 握手(见 F-RAGENT-06)
|
||||
|
||||
**内部机制 — 创建流程**:
|
||||
|
||||
1. 渲染进程调用 `ipcBridge.remoteAgent.create.invoke(payload)`
|
||||
2. 主进程 Bridge 生成:
|
||||
- `id`: UUID
|
||||
- OpenClaw 协议: 生成 Ed25519 密钥对(`deviceId` = 公钥 SHA256 指纹, `devicePublicKey`, `devicePrivateKey`)
|
||||
- 其他协议: 无密钥生成
|
||||
- `status`: `'unknown'`
|
||||
- `createdAt` / `updatedAt`: 当前时间戳
|
||||
3. 写入 SQLite `remote_agents` 表
|
||||
4. 触发 `agentRegistry.refreshRemoteAgents()` 刷新检测列表(fire-and-forget)
|
||||
5. 返回完整 `RemoteAgentConfig`
|
||||
|
||||
**协议说明**:
|
||||
|
||||
UI 当前不暴露协议选择器,新建 Agent 硬编码为 `'openclaw'` 协议。类型定义支持 `'openclaw' | 'zeroclaw' | 'acp'` 三种协议。编辑时从 DB 回填实际协议值。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 表单验证失败:名称或 URL 为空时,字段下方显示红色错误文字(`settings.remoteAgent.nameRequired` / `settings.remoteAgent.urlRequired`),使用 `role="alert"` + `aria-live="assertive"`
|
||||
- 创建/保存 API 失败:catch 块为空(无用户提示),finally 块恢复 saving 状态(已知局限)
|
||||
- DB 写入失败:抛出异常 "Failed to create remote agent"
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 弹窗标题为"添加远程 Agent"(`settings.remoteAgent.addTitle`)
|
||||
- [ ] 表单验证:名称和 URL 为必填,Bearer Token 模式下令牌为必填
|
||||
- [ ] Emoji 选择器正常工作,默认头像为 `🤖`(固定值)
|
||||
- [ ] 认证方式切换时,令牌字段正确显示/隐藏
|
||||
- [ ] URL 为 `wss://` 时正确显示不安全连接开关
|
||||
- [ ] 保存后自动触发 OpenClaw 握手流程
|
||||
- [ ] 创建成功后列表自动刷新(SWR mutate)
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-03) 编辑 Remote Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望修改已配置的远端 Agent 信息(名称、URL、认证等),修改后重新验证连接。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击卡片底部"编辑"按钮
|
||||
2. 弹出"编辑远程 Agent"弹窗(AionModal)
|
||||
3. 所有已保存数据完整回填:
|
||||
- Avatar emoji
|
||||
- 名称
|
||||
- URL
|
||||
- 认证方式(自动选中)
|
||||
- Token(密文显示)
|
||||
- 允许不安全连接(保持之前的状态)
|
||||
- 协议从 DB 回填(`setActiveProtocol(editAgent.protocol)`)
|
||||
4. 用户修改字段后点击"保存"
|
||||
5. 调用 `ipcBridge.remoteAgent.update.invoke({ id, updates })`
|
||||
6. 对 OpenClaw 协议 Agent,编辑保存后同样触发握手流程
|
||||
|
||||
**内部机制 — 更新流程**:
|
||||
|
||||
Bridge 端逐字段映射到 DB 列名(仅更新 `updates` 中不为 `undefined` 的字段):
|
||||
|
||||
| 前端字段 | DB 列名 | 说明 |
|
||||
| --------------- | ---------------- | ------------- |
|
||||
| `name` | `name` | |
|
||||
| `protocol` | `protocol` | |
|
||||
| `url` | `url` | |
|
||||
| `authType` | `auth_type` | |
|
||||
| `authToken` | `auth_token` | |
|
||||
| `avatar` | `avatar` | |
|
||||
| `description` | `description` | |
|
||||
| `allowInsecure` | `allow_insecure` | boolean → 0/1 |
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 更新 API 失败:catch 块为空(无用户提示),finally 块恢复 saving 状态(已知局限)
|
||||
- Agent 已被删除:DB 返回失败,无特殊处理
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 弹窗标题为"编辑远程 Agent"(`settings.remoteAgent.editTitle`)
|
||||
- [ ] 所有字段正确回填
|
||||
- [ ] 修改后保存成功,列表刷新
|
||||
- [ ] OpenClaw 协议 Agent 保存后触发握手流程
|
||||
- [ ] 关闭弹窗方式:取消按钮 / Escape 键 / X 按钮
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-04) 删除 Remote Agent [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望删除不再需要的远端 Agent 配置。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击卡片底部"删除"按钮(红色 danger 样式)
|
||||
2. 弹出确认对话框(Arco 原生 `Modal.confirm`,区别于创建/编辑使用的 AionModal 封装):
|
||||
- 标题:`settings.remoteAgent.deleteConfirm`("删除远程 Agent")
|
||||
- 正文:`settings.remoteAgent.deleteConfirmContent`("确定要删除「{agent名称}」吗?",使用直角引号包裹名称)
|
||||
- 按钮:"取消" / "确定"(确定按钮为 danger 样式)
|
||||
3. 用户点击"确定"
|
||||
4. 卡片立即从列表中移除
|
||||
5. 显示成功 toast:`settings.remoteAgent.deleted`("远程 Agent 已删除")
|
||||
|
||||
**内部机制**:
|
||||
|
||||
1. 调用 `ipcBridge.remoteAgent.delete.invoke({ id })`
|
||||
2. Bridge 端: `db.deleteRemoteAgent(id)`
|
||||
3. 成功后: `agentRegistry.refreshRemoteAgents()`(fire-and-forget)
|
||||
4. 渲染进程: SWR mutate 刷新列表
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 删除 API 失败:当前无特殊错误处理
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 删除前弹出确认对话框,包含 Agent 名称
|
||||
- [ ] 确认后卡片立即从列表移除
|
||||
- [ ] 删除成功显示 toast 通知(`settings.remoteAgent.deleted`)
|
||||
- [ ] 确认对话框关闭后 UI 无残留元素(Modal.confirm 默认行为)
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-05) 连接测试 [已实现]
|
||||
|
||||
> 建议验证策略:WebSocket 连接通过集成测试 mock;URL 验证和 SSRF 防护通过单元测试
|
||||
|
||||
**用户故事**:作为用户,我希望在保存前测试远端 Agent 的连接是否可用,确认 URL 和认证信息正确。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在创建/编辑弹窗中填写 URL 和认证信息
|
||||
2. 点击"测试连接"按钮(全宽 outline 样式,带刷新图标)
|
||||
3. 按钮进入 loading 状态(源码 `loading={testing}` prop 已传入)
|
||||
4. 测试完成后显示 toast:
|
||||
- 成功:`settings.remoteAgent.testSuccess`(绿色勾号 "连接成功")
|
||||
- 失败(result):`settings.remoteAgent.testFailed`(红色, 参数 `{ error }`)
|
||||
- 失败(catch):`settings.remoteAgent.testError`(红色, 参数 `{ error }`)
|
||||
|
||||
**内部机制**:
|
||||
|
||||
1. 从表单获取 `url, authType, authToken, allowInsecure`
|
||||
2. URL 为空 → warning toast `settings.remoteAgent.urlRequired`,不发起测试
|
||||
3. 调用 `ipcBridge.remoteAgent.testConnection.invoke({...})`
|
||||
4. Bridge 端处理:
|
||||
- **URL 验证** (`validateWebSocketUrl`):
|
||||
- trim 输入
|
||||
- 无协议前缀 → 自动补 `ws://`(支持裸 `host:port` 格式如 `127.0.0.1:42617`)
|
||||
- 仅允许 `ws:` / `wss:` 协议(防 SSRF)
|
||||
- 解析失败 → 返回 `{ success: false, error: 'Invalid URL' }`
|
||||
- **WebSocket 连接**:
|
||||
- Node.js `ws` 库
|
||||
- headers: bearer 认证时添加 `Authorization: Bearer {token}`
|
||||
- 选项: `handshakeTimeout: 10_000`, `rejectUnauthorized: !allowInsecure`
|
||||
- 超时: 10 秒
|
||||
- `ws.on('open')` → `{ success: true }`
|
||||
- `ws.on('error')` → `{ success: false, error: err.message }`
|
||||
- 通过 `settled` flag 确保只 resolve 一次
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- URL 为空:warning toast,不发起连接
|
||||
- 非 ws/wss 协议:返回 `Unsupported protocol: {protocol}`
|
||||
- 连接超时(10s):返回 `Connection timed out (10s)`
|
||||
- WebSocket 构造失败:catch 后返回错误信息
|
||||
- IPC 调用异常:catch → error toast
|
||||
|
||||
**待验证问题**:
|
||||
|
||||
- 源码 `RemoteAgentManagement.tsx:398` 传了 `loading={testing}` prop,但动态分析未观察到明显 spinner 效果。需在真实窗口确认 Arco Button `type='outline'` 模式下 `loading` prop 的视觉表现。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] URL 为空时给出 warning 提示(`settings.remoteAgent.urlRequired`),不发起连接
|
||||
- [ ] 仅允许 ws/wss 协议(SSRF 防护)
|
||||
- [ ] 支持裸 host:port 格式(自动补 ws://)
|
||||
- [ ] 10 秒超时后返回明确错误
|
||||
- [ ] Bearer 认证时正确传递 Authorization header
|
||||
- [ ] `allowInsecure` 正确控制 TLS 证书验证
|
||||
- [ ] 成功/失败通过 toast 反馈(`settings.remoteAgent.testSuccess` / `testFailed` / `testError`)
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-06) OpenClaw 握手与设备认证 [已实现]
|
||||
|
||||
> 建议验证策略:握手协议通过集成测试 mock WebSocket;设备密钥生成通过单元测试
|
||||
|
||||
**用户故事**:作为用户,当我保存 OpenClaw 协议的远端 Agent 时,系统应自动与远端 Gateway 完成身份验证握手。
|
||||
|
||||
**前置条件**:Agent 协议为 `'openclaw'`
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击"保存"后,按钮显示"握手中..."(`settings.remoteAgent.handshaking`,加载态)
|
||||
2. 系统自动与远端 Gateway 进行身份验证
|
||||
3. 成功:toast `settings.remoteAgent.created` 或 `settings.remoteAgent.updated`,弹窗关闭
|
||||
4. 需要审批:切换到配对等待界面(见 F-RAGENT-07)
|
||||
5. 失败:warning toast("已创建/更新 — 握手失败详情"),弹窗关闭
|
||||
|
||||
**内部机制 — OpenClaw 握手协议**:
|
||||
|
||||
1. 调用 `ipcBridge.remoteAgent.handshake.invoke({ id })`
|
||||
2. Bridge 端创建 `OpenClawGatewayConnection` 实例
|
||||
3. **协议握手流程**:
|
||||
```
|
||||
Client Gateway
|
||||
|── WebSocket connect ───────────────>|
|
||||
| |
|
||||
| 路径 A: Challenge 到达 (<750ms) |
|
||||
|<── EVENT connect.challenge {nonce} ─|
|
||||
|── REQ connect {v2 签名, 含 nonce} ─|
|
||||
| |
|
||||
| 路径 B: Challenge 未到达(750ms超时)|
|
||||
|── REQ connect {v1 签名, 无 nonce} ─|
|
||||
| |
|
||||
|<── RES hello-ok {auth.deviceToken, | (成功)
|
||||
| policy, features} |
|
||||
| 或 |
|
||||
|<── RES error {PAIRING_REQUIRED} ─| (需审批)
|
||||
```
|
||||
4. 三个回调(`onHelloOk`、`onConnectError`、`onClose`)共用同一 Promise,依赖 Promise 只能 resolve 一次的语义保证互斥
|
||||
|
||||
**设备认证签名**:
|
||||
|
||||
使用 Ed25519 私钥对管道分隔字符串签名:
|
||||
|
||||
- **v1 格式**(无 nonce,750ms 超时强制 connect 时): `"v1|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}"`
|
||||
- **v2 格式**(有 nonce,收到 challenge 后): `"v2|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}|{nonce}"`
|
||||
|
||||
版本自动判定: `params.nonce ? 'v2' : 'v1'`
|
||||
|
||||
**Connect 请求参数**:
|
||||
|
||||
| 参数 | 值 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------ |
|
||||
| `minProtocol` / `maxProtocol` | `3` / `4`(同时兼容 v3 与 v4 Gateway,2026.5.12 起 Gateway 默认要求 v4) |
|
||||
| `client.id` | `'gateway-client'` |
|
||||
| `client.displayName` | `'AionUI'` |
|
||||
| `client.mode` | `'backend'` |
|
||||
| `caps` | `['tool-events']`(必须声明以接收 tool call 事件) |
|
||||
| `role` | `'operator'` |
|
||||
| `scopes` | `['operator.admin']` |
|
||||
|
||||
**技术说明**: password 认证路径在 Bridge 层(`remoteAgentBridge.ts:184`)和 Connection 层(`OpenClawGatewayConnection.ts:263`)已实现,但当前 UI 不暴露 password 选项。如果通过直接修改 DB 将 `auth_type` 改为 `'password'`,握手流程会正确使用 password 认证。
|
||||
|
||||
**Hello-ok 处理**:
|
||||
|
||||
- 存储 Gateway 签发的 `deviceToken`(远程场景: 回调 `onDeviceTokenIssued` → 写入 DB `device_token` 字段)
|
||||
- 更新 DB: `status = 'connected'`, `last_connected_at = Date.now()`
|
||||
|
||||
**PAIRING_REQUIRED 判定**: `details.recommendedNextStep === 'wait_then_retry'` 或 `/pairing.required/i`
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- Gateway 不可达:WebSocket 连接失败,15 秒超时后返回错误
|
||||
- Challenge 未到达:750ms 后强制发送 connect 请求(使用 v1 签名,无 nonce)
|
||||
- 设备令牌无效:本地场景可清除 token 并降级;远程场景直接返回错误
|
||||
- handshake 超时(15s):`conn.stop()` + 返回 `{ status: 'error', error: 'Handshake timed out (15s)' }`
|
||||
- WebSocket 意外关闭:返回 `{ status: 'error', error: 'Connection closed (code): reason' }`(若已被 onHelloOk/onConnectError resolve 则忽略)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 保存后按钮显示"握手中..."加载态(`settings.remoteAgent.handshaking`)
|
||||
- [ ] 握手成功:toast + 弹窗关闭 + 列表刷新
|
||||
- [ ] 需要审批:进入配对等待界面
|
||||
- [ ] 握手失败:warning toast 包含错误详情
|
||||
- [ ] 设备令牌正确持久化到 DB
|
||||
- [ ] 连接状态正确更新(connected / pending / error)
|
||||
- [ ] 15 秒超时后正确返回错误
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-07) 配对等待与轮询 [已实现]
|
||||
|
||||
**用户故事**:作为用户,当远端 Gateway 要求设备审批时,我希望看到等待界面和倒计时,并可以随时取消。
|
||||
|
||||
**前置条件**:F-RAGENT-06 握手返回 `pending_approval`
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 弹窗切换到等待审批界面:
|
||||
- 居中 Spinner 动画(Arco Spin, size=32)
|
||||
- 主文案:`settings.remoteAgent.pendingApproval`("等待网关审批...")
|
||||
- 副文案:`settings.remoteAgent.pendingApprovalHint`("请在 OpenClaw Gateway 上批准此设备")
|
||||
- 倒计时:`settings.remoteAgent.pendingTimeRemaining`("剩余时间:M:SS",从 5:00 开始)
|
||||
- 底部仅显示"取消"按钮(`settings.remoteAgent.pendingCancel`)
|
||||
2. 同时,背景中 Agent 卡片已创建,状态为 `pending`(橙色标签)
|
||||
3. 系统每 5 秒轮询一次 Gateway(调用 `ipcBridge.remoteAgent.handshake.invoke`)
|
||||
4. Gateway 审批通过 → 握手返回 `ok` → 成功 toast(固定使用 `settings.remoteAgent.created` key,不区分创建/编辑场景,已知局限)+ 弹窗关闭
|
||||
5. 5 分钟超时 → 显示超时文案(`settings.remoteAgent.pendingTimeout`,warning 样式)
|
||||
6. 用户点击"取消" → 停止轮询 + 弹窗关闭(Agent 保持 pending 状态)
|
||||
|
||||
**倒计时机制**:
|
||||
|
||||
- 总时长: 300,000 ms (5 分钟)
|
||||
- 更新间隔: 1 秒
|
||||
- 计算方式: `remaining = max(0, PAIRING_TIMEOUT - (Date.now() - startedAt))`
|
||||
- 格式化: `M:SS`(如 `4:45`)
|
||||
- `remaining <= 0` → 停止所有定时器 + `pairingState = 'timeout'`
|
||||
|
||||
**轮询机制**:
|
||||
|
||||
- 间隔: 5,000 ms
|
||||
- 每次调用 `handshake.invoke({ id })`
|
||||
- `status === 'ok'` → 停止轮询 + success toast + 关闭弹窗
|
||||
- `status === 'pending_approval'` → 继续轮询
|
||||
- 异常 → 忽略(`catch {}`),继续轮询(已知局限:无错误反馈)
|
||||
|
||||
**取消配对行为**:
|
||||
|
||||
1. 清除轮询定时器和倒计时定时器
|
||||
2. `pairingState = 'idle'`
|
||||
3. 触发 `onSaved()` 刷新列表 + `onClose()` 关闭弹窗
|
||||
4. Agent 卡片保留在列表中,状态保持 `pending`
|
||||
5. 无"重试配对"独立入口(需通过编辑再保存触发)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 轮询中 handshake 异常:完全忽略(`catch {}`),继续轮询
|
||||
- 弹窗关闭后(afterClose):停止轮询 + 重置 pairingState + 重置表单
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 等待界面正确显示 Spinner + 主副文案 + 倒计时
|
||||
- [ ] 倒计时从 5:00 开始,每秒递减,格式 M:SS
|
||||
- [ ] 配对期间定期轮询 Gateway(验证策略:集成测试 mock 网络请求)
|
||||
- [ ] 审批通过后自动关闭弹窗并显示成功 toast(`settings.remoteAgent.created`)
|
||||
- [ ] 配对超时后显示超时提示(`settings.remoteAgent.pendingTimeout`)(验证策略:单元测试 mock 时间)
|
||||
- [ ] 取消后停止轮询,Agent 保持 pending 状态
|
||||
- [ ] 弹窗关闭时清理所有定时器(验证策略:单元测试)
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-08) Remote Agent 会话创建与恢复 [已实现]
|
||||
|
||||
> 建议验证策略:集成测试;需 mock Gateway sessions API
|
||||
|
||||
**用户故事**:作为用户(或系统),当使用远端 Agent 发起对话时,系统应自动创建或恢复与 Gateway 的会话。
|
||||
|
||||
**正常流程**(系统视角):
|
||||
|
||||
1. `RemoteAgentCore.start()` 被调用
|
||||
2. emit 状态消息 `'connecting'`
|
||||
3. 创建 `OpenClawGatewayConnection` 并启动
|
||||
4. 等待连接建立(轮询 `isConnected`,100ms 间隔,默认参数 30 秒超时)
|
||||
5. emit `'connected'`
|
||||
6. 解析会话 (`resolveSession`):
|
||||
- 有 `resumeKey` → 尝试 `sessions.resolve({ key: resumeKey })`
|
||||
- 失败或无 resumeKey → `sessions.reset({ key: conversationId, reason: 'new' })`
|
||||
- reset 失败 → 降级 `sessions.resolve({ key: conversationId })`
|
||||
- 再失败 → 直接使用 `conversationId` 作为 sessionKey
|
||||
7. emit `'session_active'`
|
||||
8. sessionKey 变更时通知上层持久化到 conversation extra
|
||||
|
||||
**会话持久化**:
|
||||
|
||||
- `RemoteAgentManager.saveSessionKey(sessionKey)`:
|
||||
- 读取 conversation(确认 `type === 'remote'`)
|
||||
- 更新 `conversation.extra.sessionKey`
|
||||
- 下次打开同一对话时可通过 `resumeKey` 恢复会话
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 连接超时(默认 30s):抛出 `Remote agent connection timeout`
|
||||
- sessions.resolve 失败:降级到 sessions.reset
|
||||
- sessions.reset 失败:降级到 sessions.resolve
|
||||
- 所有会话 API 失败:直接使用 conversationId 作为 sessionKey(已知局限:可能导致 Gateway 侧无匹配会话)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 连接建立后自动创建/恢复会话
|
||||
- [ ] 支持会话恢复(通过 sessionKey)
|
||||
- [ ] 三级 fallback 机制正确执行
|
||||
- [ ] sessionKey 正确持久化到 conversation extra
|
||||
- [ ] 连接超时(默认 30s)返回明确错误
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-09) 消息发送与流式响应 [已实现]
|
||||
|
||||
> 建议验证策略:集成测试验证流式事件处理;E2E 需要真实 Gateway
|
||||
|
||||
**用户故事**:作为用户,我希望向远端 Agent 发送消息后,能实时看到流式响应内容。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户在对话窗口输入消息并发送
|
||||
2. 页面显示连接状态提示(connecting → connected → session_active)
|
||||
3. Agent 开始流式回复:文字逐字/逐段出现
|
||||
4. 回复中可能穿插工具调用展示(见 F-RAGENT-10)
|
||||
5. 回复完成后,对话结束
|
||||
|
||||
**内部机制 — 消息发送**:
|
||||
|
||||
1. `RemoteAgentManager.sendMessage(data)`:
|
||||
- `cronBusyGuard.setProcessing(conversationId, true)` 标记忙碌
|
||||
- 保存用户消息到 DB(非 silent 时)
|
||||
- 调用 `RemoteAgentCore.sendMessage()`
|
||||
- `cronBusyGuard.setProcessing(conversationId, false)` 在 finish 信号事件或 sendMessage 异常时触发
|
||||
2. `RemoteAgentCore.sendMessage()`:
|
||||
- 检查连接状态,断开时自动重新 `start()`(已知局限:可能导致意外重连和会话重置)
|
||||
- 文件附件转换:路径含空格时用 `@"filepath"` 格式,否则 `@filepath`,多文件空格连接后追加到消息内容前
|
||||
- 调用 `connection.chatSend({ sessionKey, message, idempotencyKey: UUID })`
|
||||
|
||||
**内部机制 — 流式响应处理**:
|
||||
|
||||
Gateway 通过 `chat` / `chat.event` 事件推送响应:
|
||||
|
||||
- **delta 事件**:
|
||||
- 携带累积文本
|
||||
- 智能增量计算:若累积文本以已接收文本开头 → 截取增量部分
|
||||
- emit `content` 事件到渲染进程
|
||||
- **final 事件**:
|
||||
- 检查是否有遗漏文本(final 中的完整文本 > 已累积文本)
|
||||
- 若无 delta 到达但有 `agentAssistantFallbackText` → 使用 fallback(agent.event assistant stream 缓存)
|
||||
- 若仍无内容 → `fetchAndEmitHistoryFallback(runId)`: 从最近 5 条消息中反向查找匹配当前 runId 的 assistant 消息(无 runId 时匹配任意 assistant 消息)
|
||||
- 触发 `handleEndTurn()`
|
||||
- **aborted 事件**:触发 `handleEndTurn()`
|
||||
- **error 事件**:emit 错误消息 + `handleEndTurn()`
|
||||
|
||||
**事件转发路径**:
|
||||
|
||||
```
|
||||
RemoteAgentCore → RemoteAgentManager
|
||||
→ ipcBridge.conversation.responseStream.emit() → 渲染进程对话 UI
|
||||
→ channelEventBus.emitAgentMessage() → Telegram/Lark 频道
|
||||
→ teamEventBus.emit('responseStream') → 团队协作(仅 finish/error)
|
||||
```
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 连接断开时发送消息:自动重连后发送
|
||||
- chatSend 异常:返回 `{ success: false, error }` + emit 错误消息
|
||||
- Gateway 返回 error 事件:emit 错误消息到对话界面
|
||||
- history fallback 中所有消息 runId 不匹配:无 fallback 文本,直接 handleEndTurn
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 用户消息正确保存到 DB
|
||||
- [ ] 流式文本逐步渲染
|
||||
- [ ] 文件附件正确转换(空格路径带双引号)
|
||||
- [ ] final 事件后对话状态正确结束
|
||||
- [ ] error 事件显示错误消息
|
||||
- [ ] 消息正确转发到频道和团队事件总线
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-10) 工具调用展示 [已实现]
|
||||
|
||||
> 建议验证策略:集成测试 mock agent event 解析
|
||||
|
||||
**用户故事**:作为用户,我希望看到远端 Agent 使用的工具调用过程和结果。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. Agent 回复过程中,工具调用以折叠卡片形式展示
|
||||
2. 卡片显示工具名称、状态(执行中/完成/失败)、参数/结果
|
||||
|
||||
**内部机制**:
|
||||
|
||||
Gateway 通过 `agent` / `agent.event` 事件推送工具调用信息:
|
||||
|
||||
- **stream: tool / tool_call**:
|
||||
- phase 映射: `start/update/partialResult` → `in_progress`, `result` → `completed/failed`
|
||||
- 工具名称: `toolData.name` 或 `toolData.title`,fallback 为 `"Tool Call"`
|
||||
- 工具类型推断 (`inferToolKind`): 基于子串匹配(非单词边界),对组合命名的工具可能误判(如 `'Breadcrumb'` 匹配到 `'read'`,已知局限)
|
||||
- `read/view/list/search/grep/glob/find/get/fetch` → `'read'`
|
||||
- `write/edit/create/delete/patch/update/insert/remove` → `'edit'`
|
||||
- `exec/run/bash/shell/terminal` → `'execute'`
|
||||
- 导航工具特殊处理:`NavigationInterceptor` 检测 → 提取 URL → 创建预览消息
|
||||
- 通过 `AcpAdapter.convertSessionUpdate()` 转换为 `TMessage` → emit
|
||||
|
||||
- **stream: thinking / thought**:
|
||||
- emit `thought` 信号事件(subject: "Thinking")
|
||||
|
||||
- **stream: assistant**:
|
||||
- 缓存 `agentAssistantFallbackText`(当 chat delta 未覆盖时的后备文本)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 工具调用状态正确映射(in_progress → completed/failed)
|
||||
- [ ] 工具类型(read/edit/execute)根据名称正确推断
|
||||
- [ ] 导航工具触发 URL 预览
|
||||
- [ ] Thinking 过程显示为 thought 信号
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-11) 权限审批 [已实现]
|
||||
|
||||
> 建议验证策略:集成测试 mock approval request event
|
||||
|
||||
**用户故事**:作为用户,当远端 Agent 需要执行敏感操作时,我希望收到权限请求并可以选择允许或拒绝。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. Agent 执行需要审批的操作时,对话界面弹出权限确认卡片
|
||||
2. 卡片显示:
|
||||
- 工具名称
|
||||
- 操作参数
|
||||
- 三个选项:Allow / Always Allow / Reject
|
||||
3. 用户选择后,UI 层处理完成
|
||||
4. 70 秒无响应自动拒绝
|
||||
|
||||
**内部机制**:
|
||||
|
||||
1. Gateway 发送 `exec.approval.request` 事件
|
||||
2. `handleApprovalRequest()`:
|
||||
- 在 `pendingPermissions` Map 中创建条目
|
||||
- emit `acp_permission` 信号事件到渲染进程
|
||||
- 默认选项: `allow_once / allow_always / reject_once`
|
||||
- 超时: 70 秒自动 reject
|
||||
3. `RemoteAgentManager.handleSignalEvent()`:
|
||||
- 将 `acp_permission` 转换为 `IConfirmation`
|
||||
- 调用 `this.addConfirmation(confirmation)`
|
||||
4. 用户选择后:
|
||||
- `confirmMessage({ confirmKey, callId })` 从 `pendingPermissions` 找到条目 → resolve
|
||||
|
||||
**已知局限(重要)**:当前 `pendingPermissions` Map 中存入的 `resolve` 函数为空函数 `(_response) => {}`,`confirmMessage` 调用 resolve 不产生实际效果。源码中未找到 `exec.approval.respond` 等将审批结果实际传回 Gateway 的调用。**权限审批的用户选择可能未实际传递给 Gateway**。需确认是否有其他代码路径(如 BaseAgentManager.confirm)处理了实际的 Gateway 通信。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 权限请求正确展示工具信息和选项
|
||||
- [ ] 用户选择后 UI 层正确响应
|
||||
- [ ] 70 秒超时自动拒绝
|
||||
- [ ] 多个并发权限请求互不干扰
|
||||
|
||||
---
|
||||
|
||||
## (F-RAGENT-12) 连接状态管理与重连 [已实现]
|
||||
|
||||
> 建议验证策略:集成测试 mock WebSocket 断连/重连场景
|
||||
|
||||
**用户故事**:作为用户,我希望系统能自动管理与远端 Gateway 的连接状态,在断连时自动重连。
|
||||
|
||||
**正常流程**(系统视角):
|
||||
|
||||
1. 连接建立后,Gateway 定期发送 `tick` 心跳事件
|
||||
2. 客户端监控 tick 间隔,超过 2 倍间隔未收到 → 判定断连
|
||||
3. 断连后自动重连(指数退避策略)
|
||||
4. 重连成功后恢复正常工作
|
||||
|
||||
**心跳监控 (Tick Watch)**:
|
||||
|
||||
- 默认间隔: 30,000 ms(可由 Gateway HelloOk.policy.tickIntervalMs 覆盖)
|
||||
- 检测逻辑: `gap = Date.now() - lastTick > tickIntervalMs * 2` → 关闭连接 (code 4000, 'tick timeout')
|
||||
- 检测 timer 间隔: `Math.max(tickIntervalMs, 1000)`
|
||||
- 生命周期: `scheduleReconnect` 清理旧 tickTimer → 重连成功后 `startTickWatch`(先 clear 再 setInterval)重建 timer
|
||||
|
||||
**重连策略**:
|
||||
|
||||
- 最大重连次数: 10
|
||||
- 退避策略: 初始 1s, 每次翻倍, 最大 30s (`1s → 2s → 4s → 8s → 16s → 30s → 30s → ...`)
|
||||
- 重连时重置 `lastSeq`(事件序列追踪)
|
||||
- 达到上限 → 触发 `onConnectError` 通知上层
|
||||
|
||||
**`closed` flag 互锁**:
|
||||
|
||||
- `stop()` 设 `closed = true` → 阻止所有后续重连(`start()` 和 `scheduleReconnect()` 均检查此 flag)
|
||||
- handshake 场景: `onHelloOk` 调用 `conn.stop()` 后,`onClose` 触发的 `scheduleReconnect` 被 `closed = true` 正确阻止
|
||||
|
||||
**事件序列追踪**:
|
||||
|
||||
- 每个 event 帧可携带 `seq` 字段
|
||||
- 检测到 gap (`seq > lastSeq + 1`) → 打印 warn(不做恢复,已知局限)
|
||||
|
||||
**连接状态更新到 DB**:
|
||||
|
||||
| 触发点 | 状态 |
|
||||
| --------------------------------------------- | ----------- |
|
||||
| `RemoteAgentManager.initCore()` 成功 | `connected` |
|
||||
| `RemoteAgentManager.initCore()` 失败 | `error` |
|
||||
| handshake `onHelloOk` | `connected` |
|
||||
| handshake `onConnectError` (pairing required) | `pending` |
|
||||
| handshake `onConnectError` (other) | `error` |
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 心跳超时(2 倍间隔未收到 tick)触发断连
|
||||
- [ ] 自动重连使用指数退避策略(1s-30s)
|
||||
- [ ] 最大重连次数为 10
|
||||
- [ ] 重连成功后恢复正常工作(含 tick timer 重建)
|
||||
- [ ] 连接状态正确同步到 DB(用于 UI 标签展示)
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:IPC 通信链路
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 渲染进程 (Renderer) │
|
||||
│ │
|
||||
│ RemoteAgentManagement.tsx │
|
||||
│ ├─ useSWR → ipcBridge.remoteAgent.list.invoke() → 列表 │
|
||||
│ │ │
|
||||
│ └─ RemoteAgentFormModal (AionModal) │
|
||||
│ ├─ ipcBridge.remoteAgent.create.invoke() → 创建 │
|
||||
│ ├─ ipcBridge.remoteAgent.update.invoke() → 编辑 │
|
||||
│ ├─ ipcBridge.remoteAgent.testConnection.invoke()→ 连接测试 │
|
||||
│ ├─ ipcBridge.remoteAgent.handshake.invoke() → 握手 │
|
||||
│ └─ [5s 轮询] handshake.invoke() → 配对等待 │
|
||||
│ │
|
||||
│ handleDelete (Modal.confirm) │
|
||||
│ └─ ipcBridge.remoteAgent.delete.invoke() → 删除 │
|
||||
│ │
|
||||
│ 对话窗口 (Chat) │
|
||||
│ ├─ ipcBridge.conversation.responseStream.on() ← 消息流 │
|
||||
│ └─ confirm → RemoteAgentManager.confirm() → 权限确认 │
|
||||
└────────────────────┬─────────────────────────────────────────────┘
|
||||
│ IPC Bridge (invoke/provider)
|
||||
┌────────────────────▼─────────────────────────────────────────────┐
|
||||
│ 主进程 (Main) │
|
||||
│ │
|
||||
│ remoteAgentBridge.ts (7 个 provider) │
|
||||
│ ├─ list / get / create / update / delete → SQLite CRUD │
|
||||
│ ├─ testConnection → validateWebSocketUrl + WebSocket (10s) │
|
||||
│ └─ handshake → OpenClawGatewayConnection (15s) │
|
||||
│ │
|
||||
│ RemoteAgentManager → RemoteAgentCore → OpenClawGatewayConnection │
|
||||
│ ├─ 会话: sessions.resolve / sessions.reset │
|
||||
│ ├─ 消息: chat.send → chat events (delta/final/error) │
|
||||
│ ├─ 工具: agent events (tool/thinking/assistant) │
|
||||
│ └─ 权限: exec.approval.request → confirm │
|
||||
└────────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────▼─────────────────────────────────────────────┐
|
||||
│ 外部依赖 │
|
||||
│ ├─ Remote OpenClaw Gateway (WebSocket, 协议 v3) │
|
||||
│ ├─ SQLite Database (remote_agents 表) │
|
||||
│ └─ AgentRegistry (检测列表同步) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:Toast/Alert 消息汇总
|
||||
|
||||
| 场景 | 类型 | i18n Key / 文案 |
|
||||
| ---------------------- | ------------ | --------------------------------------------------------- |
|
||||
| URL 为空时测试连接 | warning | `settings.remoteAgent.urlRequired` |
|
||||
| 连接测试成功 | success | `settings.remoteAgent.testSuccess` |
|
||||
| 连接测试失败(result) | error | `settings.remoteAgent.testFailed` (参数 `{ error }`) |
|
||||
| 连接测试异常(catch) | error | `settings.remoteAgent.testError` (参数 `{ error }`) |
|
||||
| 创建成功 | success | `settings.remoteAgent.created` |
|
||||
| 编辑成功 | success | `settings.remoteAgent.updated` |
|
||||
| 握手失败但已保存 | warning | 拼接: `created`/`updated` + error |
|
||||
| 配对成功 | success | `settings.remoteAgent.created`(固定,不区分创建/编辑) |
|
||||
| 删除成功 | success | `settings.remoteAgent.deleted` |
|
||||
| 远程连接错误 | error (tips) | 直接文案 `Connection error: {msg}`(非 i18n) |
|
||||
| 远程 Agent 启动失败 | error | 直接文案 `Failed to start remote agent: {msg}`(非 i18n) |
|
||||
| 远程消息发送失败 | error | 直接文案 `Failed to send message: {msg}`(非 i18n) |
|
||||
|
||||
---
|
||||
|
||||
## 附录 C:超时常量汇总
|
||||
|
||||
| 超时 | 位置 | 用途 | 备注 |
|
||||
| ----------------- | -------------------------------------- | ---------------------- | ---------------------------- |
|
||||
| 5,000 ms | UI 配对轮询间隔 | handshake 轮询 | 常量 `PAIRING_POLL_INTERVAL` |
|
||||
| 300,000 ms (5min) | UI 配对总超时 | 配对等待上限 | 常量 `PAIRING_TIMEOUT` |
|
||||
| 10,000 ms | Bridge testConnection | WebSocket 连接测试超时 | 含 handshakeTimeout |
|
||||
| 15,000 ms | Bridge handshake | 握手超时 | |
|
||||
| 30,000 ms | RemoteAgentCore.waitForConnection | 等待连接建立超时 | 默认参数值 |
|
||||
| 70,000 ms | RemoteAgentCore.handleApprovalRequest | 权限请求超时 | 硬编码 |
|
||||
| 750 ms | OpenClawGatewayConnection.queueConnect | connect challenge 等待 | |
|
||||
| 1s → 30s | OpenClawGatewayConnection 重连退避 | 指数退避 | 每次翻倍 |
|
||||
| 10 次 | OpenClawGatewayConnection 最大重连 | 重连上限 | |
|
||||
| 30,000 ms | OpenClawGatewayConnection tick | 默认心跳间隔 | 可由 HelloOk.policy 覆盖 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 D:已知局限汇总
|
||||
|
||||
| # | 功能点 | 局限描述 |
|
||||
| --- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | F-RAGENT-02 | 协议默认硬编码 `'openclaw'`,UI 无协议选择器。类型支持三种协议但用户无法选择 |
|
||||
| 2 | F-RAGENT-02 | 认证方式仅 none/bearer 两个选项,缺少 password(Bridge/Connection 层已实现 password 路径,但 UI 不暴露) |
|
||||
| 3 | F-RAGENT-02/03 | 保存时 catch 块为空(无用户提示),finally 块恢复 saving 状态 |
|
||||
| 4 | F-RAGENT-05 | `allowInsecure` 开关仅在 URL 以 `wss://` 开头时显示 |
|
||||
| 5 | F-RAGENT-05 | 测试连接按钮 loading 状态待验证:源码传了 `loading` prop 但动态分析未观察到 spinner 效果 |
|
||||
| 6 | F-RAGENT-07 | 配对轮询中 handshake 异常完全忽略 (`catch {}`),用户无错误反馈 |
|
||||
| 7 | F-RAGENT-07 | 取消配对后无"重试配对"独立入口,需通过编辑再保存触发 |
|
||||
| 8 | F-RAGENT-07 | 配对成功 toast 固定使用 `settings.remoteAgent.created` key(不区分创建/编辑场景) |
|
||||
| 9 | F-RAGENT-08 | `waitForConnection` 使用 100ms 轮询检测状态(busy-wait),非事件驱动 |
|
||||
| 10 | F-RAGENT-09 | `sendMessage` 在连接断开时自动重新 `start()`,可能导致意外重连和会话重置 |
|
||||
| 11 | F-RAGENT-10 | 工具类型推断基于子串匹配而非单词边界,组合命名的工具可能误判(如 'Breadcrumb' 匹配 'read') |
|
||||
| 12 | F-RAGENT-11 | **权限审批的用户选择可能未实际传回 Gateway**:`pendingPermissions` 中 resolve 为空函数,未找到 `exec.approval.respond` 调用 |
|
||||
| 13 | F-RAGENT-11 | 权限请求超时 70 秒硬编码,超时后自动 reject 无用户可见提示 |
|
||||
| 14 | F-RAGENT-12 | 事件序列 gap 仅打印 warn,不做重新同步 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 E:设计约束
|
||||
|
||||
| # | 约束 | 说明 |
|
||||
| --- | ---------------------- | ------------------------------------------------------------------ |
|
||||
| 1 | OpenClaw 协议版本 | 固定 v3 (`OPENCLAW_PROTOCOL_VERSION = 3`) |
|
||||
| 2 | 设备密钥算法 | Ed25519,密钥对随 Agent 创建一次性生成 |
|
||||
| 3 | 设备认证签名格式 | 管道分隔字符串,v1(无 nonce)/ v2(含 nonce)两种版本 |
|
||||
| 4 | WebSocket 最大 payload | 25MB (`maxPayload: 25 * 1024 * 1024`) |
|
||||
| 5 | 默认 Gateway 端口 | 18789 |
|
||||
| 6 | 客户端标识 | `gateway-client` / backend / operator / operator.admin |
|
||||
| 7 | Capabilities | `caps: ['tool-events']` — 必须声明以接收 tool call 事件 |
|
||||
| 8 | DB 存储 | SQLite `remote_agents` 表,字段 snake_case |
|
||||
| 9 | 数据加载 | 使用 SWR(key: `'remote-agents.list'`),支持自动重验证 |
|
||||
| 10 | 弹窗组件 | 创建/编辑使用 AionModal 封装,删除确认使用 Arco 原生 Modal.confirm |
|
||||
|
||||
---
|
||||
|
||||
## 附录 F:关联模块 — OpenClaw 凭据冲突检测
|
||||
|
||||
> 此功能与 Remote Agent 设置页无直接 UI 交互,降级为关联模块说明。
|
||||
|
||||
`openclawConflictDetector.ts` 检测 OpenClaw 的 Lark/Telegram channels 是否与 AionUi Channels 使用相同凭据:
|
||||
|
||||
- **Lark 冲突**: 比较 `channels.feishu.accounts[*].appId` 与 AionUi appId
|
||||
- **Telegram 冲突**: 比较 `channels.telegram.botToken` 与 AionUi botToken
|
||||
- 配置读取路径: 环境变量 → `~/.openclaw/openclaw.json` → 遗留路径
|
||||
|
||||
**当前限制**:冲突检测结果通过 `console.warn` 输出,无 UI 呈现。导出的 `getConflictResolutionSteps()` 提供解决方案建议文本,但尚未集成到任何 UI 组件中。
|
||||
@@ -0,0 +1,197 @@
|
||||
# 对话 - 发送草稿箱(消息排队交互优化)
|
||||
|
||||
> 本文档聚焦「AI 回复期间的消息排队」这一块的交互优化,是对现有排队能力的体验升级。
|
||||
> 涉及范围:对话输入区上方的排队面板。不改后端,全部在渲染层完成。
|
||||
|
||||
---
|
||||
|
||||
## 背景与目标
|
||||
|
||||
**现状**:AI 正在回复时,用户仍可继续输入并发送消息,这些消息进入一个「排队面板」等待发出。这个能力已经存在——支持入队、编辑、删除、清空、当前 turn 结束后自动发送,也支持拖拽调整顺序。
|
||||
|
||||
**问题**(来自微信群用户反馈):
|
||||
|
||||
- 每条排队消息只有「删除」是直接可见的,「编辑」被折进「⋯」更多菜单,要点两下才找得到。
|
||||
- 没有「立即发送」——排队的消息只能等 AI 回完后被动等待,用户想让某条马上发出去做不到。
|
||||
- 排队面板缺少一个「是否自动发送」的总控,用户无法选择「我先攒着、想发再发」。
|
||||
|
||||
**目标**:把排队面板重新定位成一个「**发送草稿箱**」——AI 忙时你写的消息先存在这里,你能清楚看到、随手操作(立即发送 / 编辑 / 删除 / 调序),并能选择让草稿箱**自动**依次发出、还是**手动**由你逐条发。
|
||||
|
||||
**名词说明**:
|
||||
|
||||
- **发送草稿箱**:面板的新名字。指 AI 回复期间用户已发送、正在等待发出的消息集合。下文沿用旧代码里的「排队 / 队列」时,指的是同一个东西。
|
||||
- **草稿**:草稿箱里的单条待发消息(含文字与文件)。
|
||||
- **自动 / 手动**:草稿箱的两种发送模式,见 F-DRAFT-03。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-01) 每条草稿的三个常驻操作 [优化]
|
||||
|
||||
**用户故事**:作为用户,我希望每条排队的消息都能一眼看到「立即发送 / 编辑 / 删除」三个操作,随手就能点,不用翻菜单。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. AI 回复期间,用户发送的消息进入发送草稿箱,逐条列在输入框上方。
|
||||
2. 每条草稿右侧常驻三个操作按钮,鼠标不悬停也始终可见:
|
||||
- **立即发送**:见 F-DRAFT-02。
|
||||
- **编辑**:把这条草稿的文字与文件放回输入框供修改,该草稿从箱中移除,改完重新发送即重新入箱。
|
||||
- **删除**:把这条草稿从箱中移除,不再发出。
|
||||
3. 三个操作在移动端、窄栏下同样常驻可点(不依赖 hover)。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 草稿箱为空:面板整体不显示。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 每条草稿右侧常驻显示「立即发送 / 编辑 / 删除」三个按钮,无需 hover 或展开菜单。
|
||||
- [ ] 「编辑」不再藏在「⋯」菜单里。
|
||||
- [ ] 「删除」将该条从草稿箱移除。
|
||||
- [ ] 「编辑」将该条内容与文件回填输入框并从箱中移除。
|
||||
- [ ] 三个按钮在移动端 / 窄栏下均可直接点击。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-02) 立即发送(打断当前回复插队发出)[新增]
|
||||
|
||||
**用户故事**:作为用户,当我发现某条消息在排队、但我想它马上就走,我希望点一下就让它立刻发出去。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户点击某条草稿的「立即发送」。
|
||||
2. 系统**打断 AI 当前正在进行的回复**,将该条草稿立即出队并发送。
|
||||
3. 用户能理解这打断了当前回复——**不额外弹提示条**。
|
||||
4. 草稿箱里剩余的草稿**行为完全不变**:原本自动就继续自动依次发,原本手动就继续等待用户逐条发。发送模式、内容、顺序都不受这次插队影响。
|
||||
|
||||
**边界说明**:
|
||||
|
||||
- 「立即发送」只作用于**被点击的这一条**。它是一次「让这条插队立刻走」的动作,不是模式切换,不引入「暂停 / 继续」这类额外状态。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- AI 当前没有正在进行的回复(已空闲):直接发送该条,无需打断。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 点击「立即发送」会中止 AI 当前回复并立刻发出该条草稿。
|
||||
- [ ] 不出现「已插队 / 队列已暂停」之类的提示条。
|
||||
- [ ] 剩余草稿的发送模式、内容、顺序均不因这次插队而改变。
|
||||
- [ ] AI 空闲时点击「立即发送」直接发出,不产生打断副作用。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-03) 自动 / 手动发送模式 [新增]
|
||||
|
||||
**用户故事**:作为用户,我希望能选择草稿箱是「AI 回完自动帮我依次发」还是「先攒着、我想发再发」。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 草稿箱标题行右侧有一个**模式切换按钮**,显示当前模式(自动 / 手动),点一下即切到另一模式,无需二次确认。
|
||||
2. **自动模式**(默认):AI 每结束一轮回复,草稿箱自动把下一条发出去,一条接一条,直到发完,用户无需操作。
|
||||
3. **手动模式**:草稿只暂存在箱中、**不自动发出**,由用户逐条点「立即发送」决定何时发。
|
||||
|
||||
**作用范围**(会话级,纯前端):
|
||||
|
||||
- 模式是**每个对话各自记录**的,A 对话设手动不影响 B 对话。
|
||||
- **每个对话启动时都是「自动」**。
|
||||
- 该设置与现有排队内容一样保存在前端会话级存储中:应用运行期间切走再切回该对话,模式与草稿都保留;整个应用关闭重开后重置(草稿与模式都回到初始,与现有排队行为一致)。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 草稿箱为空:面板不显示,模式设置在后台保留,下次有草稿入箱时按该模式运作。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 标题行右侧的切换按钮显示当前模式,点击即切换,无二次确认。
|
||||
- [ ] 自动模式下,AI 每轮结束后自动依次发出草稿。
|
||||
- [ ] 手动模式下,草稿不自动发出,仅在用户点「立即发送」时发出。
|
||||
- [ ] 新对话默认自动模式。
|
||||
- [ ] 模式按对话独立记录,互不影响。
|
||||
- [ ] 应用关闭重开后模式回到默认(自动)。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-04) 草稿箱标题行与说明 [优化]
|
||||
|
||||
**用户故事**:作为用户,我希望一眼看懂这个面板是什么、当前处于什么模式,需要时能查到自动/手动的含义。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
标题行(草稿箱框内顶部一行)自左到右:
|
||||
|
||||
1. **标题「发送草稿箱」+ 数量徽标**(显示当前草稿条数)。
|
||||
2. **模式切换按钮**(F-DRAFT-03)。
|
||||
3. **帮助「?」**:hover 显示说明——草稿箱是什么,以及自动 / 手动分别的含义。
|
||||
4. **更多「⋯」**:见 F-DRAFT-05。
|
||||
|
||||
**帮助文案**(tooltip):
|
||||
|
||||
> 发送草稿箱:AI 回复期间你发的消息会先存到这里。
|
||||
> 自动:AI 回复结束后,自动依次发送草稿箱里的消息。
|
||||
> 手动:消息只暂存、不自动发,由你逐条点「立即发送」。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 标题行显示「发送草稿箱」与草稿数量。
|
||||
- [ ] 「?」hover 出说明,覆盖草稿箱含义与两种模式。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-05) 清空草稿箱(防误触)[优化]
|
||||
|
||||
**用户故事**:作为用户,我希望能一次清空整箱草稿,但不要让我轻易误点到。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 「清空草稿箱」放在标题行「⋯」更多菜单里,菜单项以危险色(红)呈现。
|
||||
2. 点击后**再弹一次二次确认**,确认后才清空全部草稿。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 「清空草稿箱」位于「⋯」菜单内,非标题行直接可点。
|
||||
- [ ] 菜单项以危险色呈现。
|
||||
- [ ] 点击后需二次确认才执行清空。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-06) 拖拽调整顺序 [保留]
|
||||
|
||||
**用户故事**:作为用户,我希望能调整草稿的发送先后顺序。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 每条草稿最前有一个拖拽手柄,**hover 时显现**,按住可上下拖动调整顺序。
|
||||
2. 顺序即草稿箱自动/手动发出的先后次序。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 桌面端 hover 草稿时显现拖拽手柄,可上下拖动排序。
|
||||
- [ ] 排序结果即后续发送次序。
|
||||
|
||||
---
|
||||
|
||||
## (F-DRAFT-07) 窄栏 / 移动端适配 [新增]
|
||||
|
||||
**用户故事**:作为在小屏、移动端或团队并排 Agent 视图下使用的用户,我希望草稿箱在窄栏里也摆得下、点得到。
|
||||
|
||||
**适配策略**(约 300px 宽及以下):
|
||||
|
||||
- **标题**「发送草稿箱」收成一个图标 + 数量徽标,hover / 长按显示全名,省出宽度。
|
||||
- **模式切换按钮始终保留**(核心功能),文字压到「自动 / 手动」两字。
|
||||
- **「?」帮助并入「⋯」菜单**:窄栏头部只保留一个「⋯」,其中包含「使用说明」与「清空草稿箱」。
|
||||
- **每条草稿的三个操作按钮不变**,消息文字自动截断。
|
||||
- **拖拽手柄在窄栏隐藏**,改为长按整条拖动。
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 窄栏下标题收成图标 + 徽标,切换按钮保留。
|
||||
- [ ] 窄栏下「?」并入「⋯」菜单,头部仅一个「⋯」。
|
||||
- [ ] 窄栏下每条三个操作按钮仍可点击,文字自动截断。
|
||||
- [ ] 团队并排 Agent 视图下草稿箱布局不溢出、不错位。
|
||||
|
||||
---
|
||||
|
||||
## 待讨论模块
|
||||
|
||||
- **控件形态已定**:模式切换采用「单按钮状态切换」(按钮显示当前模式,点击切到另一模式),不采用拨动开关或分段按钮。
|
||||
- **是否需要「全局默认模式」**:当前为纯会话级、新对话默认自动。若后续收到「每次开新对话都要重设太麻烦」的反馈,再评估增加一个全局默认值(设置项)。此项本期不做。
|
||||
@@ -0,0 +1,97 @@
|
||||
# 定时任务模块体验优化 (F-CRON-OPT)
|
||||
|
||||
> 本文档覆盖定时任务(Scheduled Tasks / Cron)模块的两项体验优化:对话头部入口的「空态收敛」,以及任务列表页「创建入口」的双路径化(手动创建 / 对话创建)。
|
||||
> 面向新读者:定时任务是 AionUi 中让助手按计划自动执行的能力,用户可在「定时任务」列表页集中管理,也可在对话内由助手协助创建。
|
||||
|
||||
---
|
||||
|
||||
## 背景与名词
|
||||
|
||||
- **对话头部入口**:每个对话页面左上角的一个时钟图标(组件 `CronJobManager`),用于提示当前对话是否关联了定时任务,并提供跳转。
|
||||
- **定时任务列表页**:路由 `#/scheduled`,集中展示用户的所有定时任务,右上角有「创建」按钮。
|
||||
- **创建弹窗**:列表页点击创建后弹出的表单(`CreateTaskDialog`),用户在其中手动配置频率、执行模式、助手等。
|
||||
- **首页**:路由 `#/guid`,新建对话的起始页,中部有输入框(sendbox)。
|
||||
- **助手记忆**:首页会自动恢复用户上一次选择的助手(assistant)及其相关配置,这是已有行为。
|
||||
|
||||
---
|
||||
|
||||
## (F-CRON-OPT-01) 对话头部入口空态收敛
|
||||
|
||||
**想解决的问题**
|
||||
|
||||
当前对话页面左上角,即便用户从未创建过任何定时任务,只要该对话加载了 cron 技能,就会显示一个**灰色时钟图标**,hover 弹出「立即创建」气泡。这个灰色图标对绝大多数没有定时任务诉求的用户是干扰——它占据标题栏视觉位置,却不对应任何已存在的对象。
|
||||
|
||||
**期望行为**
|
||||
|
||||
- **无关联定时任务时**:不显示任何图标(无论该对话是否加载了 cron 技能)。
|
||||
- **有关联定时任务时**:保持现状——显示彩色状态图标(绿色=运行中 / 橙色=已暂停 / 红色=出错),点击跳转到该任务的详情页。
|
||||
|
||||
**改动前后对比**
|
||||
|
||||
| 场景 | 改动前 | 改动后 |
|
||||
| --------------------- | -------------------------------- | -------------------------------- |
|
||||
| 无任务 + 无 cron 技能 | 不显示 | 不显示 |
|
||||
| 无任务 + 有 cron 技能 | 灰色图标 + hover「立即创建」气泡 | **不显示** |
|
||||
| 有任务 | 彩色状态图标,点击跳详情 | 彩色状态图标,点击跳详情(不变) |
|
||||
|
||||
**验收标准**
|
||||
|
||||
- [ ] 一个全新对话(未关联任务)的标题栏左上角不出现时钟图标
|
||||
- [ ] 当对话关联的任务被创建后,标题栏出现对应状态色的图标
|
||||
- [ ] 点击该图标跳转到 `#/scheduled/:job_id` 详情页
|
||||
- [ ] 任务暂停 / 出错时图标颜色相应变化
|
||||
- [ ] 子对话(由定时任务派生)仍能通过 `cron_job_id` 正确显示来源任务图标
|
||||
|
||||
---
|
||||
|
||||
## (F-CRON-OPT-02) 列表页创建入口双路径化
|
||||
|
||||
**想解决的问题**
|
||||
|
||||
定时任务的配置项较多(频率、cron 表达式、执行模式、助手、模型、工作空间),手动填表对新用户有门槛。AionUi 已具备「让助手在对话中帮你创建定时任务」的能力,但列表页的「创建」按钮只通向手动弹窗,用户感知不到对话创建这条更轻量的路径。
|
||||
|
||||
**期望行为**
|
||||
|
||||
列表页右上角的单一「创建」按钮,改为一个**分体下拉按钮(split button)**:
|
||||
|
||||
- **主按钮区域**:默认动作 = **对话创建(Create via chat)**。点击后跳转到首页,并自动在输入框填入一段定时任务创建提示词,用户可直接发送或修改后发送,由助手完成创建。
|
||||
- **下拉箭头区域**:展开两个选项
|
||||
- **Create via chat**(对话创建):同主按钮动作。
|
||||
- **Create manually**(手动创建):保持现状,弹出 `CreateTaskDialog` 表单。
|
||||
|
||||
**对话创建路径的细节**
|
||||
|
||||
- 跳转到首页后,输入框预填提示词,内容沿用现有的默认提示词文案(i18n key `cron.status.defaultPrompt`),不改动文案。
|
||||
- **助手沿用首页既有逻辑**:本功能只负责填入提示词,不指定 / 不重置助手。首页默认恢复用户上一次选择的助手,符合「轻量引导、尊重用户上下文」的预期。
|
||||
- 预填发生在首页挂载时:首页原本会在挂载时清空输入框草稿,新增逻辑为——若本次跳转携带了预填提示词,则填入提示词而非清空。
|
||||
|
||||
**验收标准**
|
||||
|
||||
- [ ] 列表页右上角显示分体下拉按钮,主区域文案为「对话创建」
|
||||
- [ ] 点击主区域 → 跳转首页,输入框自动填入默认提示词
|
||||
- [ ] 跳转后首页选中的助手与用户上一次使用的一致(未被强制重置)
|
||||
- [ ] 展开下拉 → 显示「对话创建」「手动创建」两项
|
||||
- [ ] 点击「手动创建」→ 弹出创建表单(与原「创建」按钮行为一致)
|
||||
- [ ] 点击「对话创建」(下拉项)→ 与主区域动作一致
|
||||
- [ ] 预填后,首页其他草稿状态(文件、工作空间)按既有规则处理,不被预填逻辑破坏
|
||||
|
||||
---
|
||||
|
||||
## 国际化
|
||||
|
||||
- 复用:`cron.status.defaultPrompt`(对话创建预填的提示词)。
|
||||
- 新增:分体下拉两项的文案
|
||||
- 「对话创建」label
|
||||
- 「手动创建」label
|
||||
- 覆盖语言:与 cron 模块现有 i18n 一致的全部语言。
|
||||
|
||||
---
|
||||
|
||||
## 影响范围(供技术参考)
|
||||
|
||||
| 模块 | 文件 | 改动性质 |
|
||||
| -------------- | ------------------------------------------ | ------------------------------------ |
|
||||
| 对话头部入口 | `pages/cron/components/CronJobManager.tsx` | 移除无任务时的灰色入口分支 |
|
||||
| 列表页创建入口 | `pages/cron/ScheduledTasksPage/index.tsx` | 单按钮改分体下拉,新增 via chat 跳转 |
|
||||
| 首页预填 | `pages/guid/GuidPage.tsx` | 挂载时按 location.state 预填提示词 |
|
||||
| 文案 | cron 模块 i18n 资源 | 复用 1 + 新增 2 |
|
||||
@@ -0,0 +1,22 @@
|
||||
# PRD 索引:设置页 → 远程连接(Channels)
|
||||
|
||||
## 文档
|
||||
|
||||
| 文件 | 说明 |
|
||||
| -------------------------- | ---------------------------------- |
|
||||
| [channels.md](channels.md) | Channels 渠道接入 PRD(10 功能点) |
|
||||
|
||||
## 功能点总览
|
||||
|
||||
| 编号 | 功能点 | 状态 | 模块 |
|
||||
| ---------- | ---------------------------------- | ------ | ------------ |
|
||||
| F-WEBUI-12 | Channels 页面总览与渠道卡片结构 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-13 | Telegram 渠道配置 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-14 | Lark / 飞书渠道配置 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-15 | DingTalk 渠道配置 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-16 | WeChat 渠道配置(扫码登录) | 已实现 | Channels Tab |
|
||||
| F-WEBUI-17 | WeCom 渠道配置(WebSocket 长连接) | 已实现 | Channels Tab |
|
||||
| F-WEBUI-18 | 渠道 Agent 选择 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-19 | 渠道默认模型选择 | 已实现 | Channels Tab |
|
||||
| F-WEBUI-20 | 渠道配对与用户授权(通用流程) | 已实现 | Channels Tab |
|
||||
| F-WEBUI-21 | 扩展渠道支持 | 已实现 | Channels Tab |
|
||||
@@ -0,0 +1,477 @@
|
||||
# 设置页 → 远程连接 — Channels 渠道接入 (F-CHAN)
|
||||
|
||||
> 本文档覆盖「设置 → 远程连接」页面 **Channels Tab** 的全部功能,包括渠道卡片总览、各 IM 渠道配置(Telegram/Lark/DingTalk/WeChat/WeCom)、Agent/模型选择、配对授权通用流程、扩展渠道支持。
|
||||
> 基于静态代码分析和动态 UI 验证综合整理。
|
||||
>
|
||||
> WebUI Tab 相关内容见 [webui.md](../webui/webui.md)。
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-12) Channels 页面总览与渠道卡片结构 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望在"远程连接"设置页面中浏览所有支持的 IM 渠道,了解每个渠道的状态,并展开配置面板。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 用户进入「设置 → 远程连接」页面,切换到 "Channels" Tab(桌面端);或直接看到 Channels 内容(非桌面端/WebUI 浏览器访问)
|
||||
2. 页面顶部显示标题"Channels"、功能描述文本("Connect Telegram, Lark, and DingTalk to interact with AionUi from IM apps."),以及 2 步引导提示
|
||||
3. 下方为渠道卡片列表(Arco Design Collapse),每个渠道一张可折叠卡片
|
||||
4. 卡片 header 包含:渠道图标/logo、渠道名、描述、连接状态徽标(Connected/未连接)、Switch 启用开关
|
||||
5. 默认所有卡片折叠(`collapseKeys` 默认 `true`);点击卡片 header 展开详细配置面板
|
||||
6. Switch 开关可直接启用/禁用渠道,无需展开卡片
|
||||
|
||||
**渠道列表与排序**:
|
||||
|
||||
| 顺序 | 渠道 | pluginId | 状态 |
|
||||
| ---- | ------------- | ----------------------- | ----------- |
|
||||
| 1 | Telegram | `telegram_default` | active |
|
||||
| 2 | Lark / Feishu | `lark_default` | active |
|
||||
| 3 | DingTalk | `dingtalk_default` | active |
|
||||
| 4 | WeChat | `weixin_default` | active |
|
||||
| 5 | WeCom | `wecom_default` | active |
|
||||
| 6+ | 扩展渠道 | 动态(`extensionMeta`) | active |
|
||||
| 末尾 | Slack | - | coming_soon |
|
||||
| 末尾 | Discord | - | coming_soon |
|
||||
|
||||
**说明**:Slack/Discord 若已被扩展渠道实现(`extensionTypeSet` 包含 `slack`/`discord`),则隐藏对应 coming_soon 占位卡片。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- `channel.getPluginStatus.invoke()` 失败:仅记录日志,渠道状态保持初始值(未启用/未连接)
|
||||
- `channel.pluginStatusChanged.on()` 事件推送:实时更新对应渠道的状态(启用/连接/botUsername 等)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 页面显示标题、描述和引导提示
|
||||
- [ ] 所有渠道按指定顺序排列
|
||||
- [ ] 卡片默认折叠,点击可展开/收起
|
||||
- [ ] Switch 开关可在卡片 header 直接操作
|
||||
- [ ] 渠道状态(Connected/未连接)实时更新
|
||||
- [ ] 非桌面端直接显示 Channels 内容(无 Tab 切换)
|
||||
- [ ] Slack/Discord 显示"coming soon"占位文案
|
||||
- [ ] 已被扩展实现的 coming_soon 渠道自动隐藏
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-13) Telegram 渠道配置 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过输入 Telegram Bot Token 连接 Telegram,并管理通过 Telegram 与 AionUi 交互的授权用户。
|
||||
|
||||
**前置条件**:用户已在 Telegram @BotFather 创建 Bot 并获取 Token
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 展开 Telegram 卡片,显示配置面板
|
||||
2. **Bot Token 输入**:Password 类型输入框(240px 宽),带可见性切换,placeholder `123456:ABC-DEF...`
|
||||
3. 点击"Test"按钮测试连接
|
||||
4. 测试成功:toast 提示"Connected! Bot: @xxx",自动启用插件(`enablePlugin`),刷新状态
|
||||
5. 启用成功后 header 的 Switch 变为 ON,状态徽标变为 Connected
|
||||
6. 下方出现"Next Steps"引导卡片(4 步操作指引),引导用户在 Telegram 搜索 Bot 发送消息触发配对
|
||||
7. 用户在 Telegram 发送消息后,"Pending Pairing Requests"区域实时出现配对请求(通过 `channel.pairingRequested.on` 事件推送)
|
||||
8. 点击"Approve"批准配对,用户移入"Authorized Users"列表
|
||||
9. 授权后 Token 输入框和 Test 按钮变为 disabled(`tokenLocked` tooltip:"请先关闭渠道并删除所有授权用户后才能修改配置")
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- Token 为空点击 Test:warning toast "Please enter a bot token"
|
||||
- 测试失败(Token 无效/网络错误):error toast 显示后端错误信息或"Connection failed"
|
||||
- 启用无 Token(Switch 直接打开):warning toast "Please enter a bot token first"
|
||||
- 批准配对失败:error toast
|
||||
- 拒绝配对:info toast "Pairing rejected"
|
||||
- 撤销用户失败:error toast
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Bot Token 输入框支持 Password 可见性切换
|
||||
- [ ] Test 按钮测试成功后自动启用插件
|
||||
- [ ] 启用后显示 4 步操作引导
|
||||
- [ ] 实时接收并显示配对请求
|
||||
- [ ] 可批准/拒绝配对请求
|
||||
- [ ] 授权用户列表显示用户名、平台、授权时间
|
||||
- [ ] 可撤销已授权用户
|
||||
- [ ] 有授权用户时 Token 和 Test 按钮禁用
|
||||
- [ ] 配对请求和授权用户列表支持手动刷新
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-14) Lark / 飞书渠道配置 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过配置飞书应用凭据连接 Lark/飞书,与 AionUi 进行 IM 对话。
|
||||
|
||||
**前置条件**:用户已在飞书开放平台创建应用并获取 App ID 和 App Secret
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 展开 Lark 卡片,显示配置面板
|
||||
2. **必填字段**:
|
||||
- App ID(Input,必填,红色 `*` 标记)
|
||||
- App Secret(Input.Password,必填,红色 `*` 标记)
|
||||
3. **可选字段**(默认折叠,点击"Show optional fields"展开):
|
||||
- Encrypt Key(Input.Password)
|
||||
- Verification Token(Input.Password)
|
||||
4. 右下角有蓝色链接"飞书开放平台文档",打开 `https://open.feishu.cn/document/develop-an-echo-bot/introduction`
|
||||
5. 点击"Test & Connect"按钮测试连接
|
||||
6. 测试成功:toast "Connected to Lark API!",自动启用插件(传入 appId + appSecret + encryptKey? + verificationToken?)
|
||||
7. 配对/授权用户管理流程与 Telegram 相同(见 F-WEBUI-13)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- App ID 或 App Secret 为空点击 Test:warning toast "Please enter App ID and App Secret",空字段标红(`touched` 状态)
|
||||
- 凭据无效:error toast 显示后端错误信息
|
||||
- 启用无凭据(Switch 直接打开):warning toast "Please configure Lark credentials first"
|
||||
- 连接状态变化:`pluginStatusChanged` 事件实时更新(Connected/Error/Connecting 带颜色编码徽标)
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] App ID 和 App Secret 为必填项,带红色 `*`
|
||||
- [ ] 可选字段默认折叠,可展开
|
||||
- [ ] Test & Connect 测试成功后自动启用
|
||||
- [ ] 飞书文档链接可正常打开
|
||||
- [ ] 空字段提交时表单标红
|
||||
- [ ] 配对/授权用户管理功能正常
|
||||
- [ ] 连接状态实时更新
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-15) DingTalk 渠道配置 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过配置钉钉应用凭据连接 DingTalk,与 AionUi 进行 IM 对话。
|
||||
|
||||
**前置条件**:用户已在钉钉开放平台创建应用并获取 Client ID 和 Client Secret
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 展开 DingTalk 卡片,显示配置面板
|
||||
2. **必填字段**:
|
||||
- Client ID(Input,必填,红色 `*` 标记)
|
||||
- Client Secret(Input.Password,必填,红色 `*` 标记)
|
||||
3. 右下角有蓝色链接"DingTalk setup guide",打开 GitHub Wiki 配置指南
|
||||
4. 点击"Test & Connect"按钮测试连接
|
||||
5. 测试成功后自动启用插件
|
||||
6. 配对/授权用户管理流程与 Telegram 相同(见 F-WEBUI-13)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 凭据为空:warning toast "Please configure DingTalk credentials first"
|
||||
- 测试/启用失败:error toast 显示错误信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Client ID 和 Client Secret 为必填项
|
||||
- [ ] Test & Connect 测试成功后自动启用
|
||||
- [ ] GitHub Wiki 链接可正常打开
|
||||
- [ ] 配对/授权用户管理功能正常
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-16) WeChat 渠道配置(扫码登录) [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过微信扫码登录连接 WeChat 渠道,无需手动输入复杂凭据。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 展开 WeChat 卡片,显示配置面板
|
||||
2. **初始状态(idle)**:显示"Scan to Login"按钮
|
||||
3. 用户点击按钮,进入二维码加载状态(`loading_qr`)
|
||||
4. **Electron 桌面端**:
|
||||
- 调用 `window.electronAPI.weixinLoginStart()` 启动登录
|
||||
- 通过 `weixinLoginOnQR` 事件接收 QR 码(base64 data URL)
|
||||
- 显示 `<img>` 标签渲染 QR 码(160x160px)
|
||||
5. **WebUI 浏览器端**:
|
||||
- 通过 `EventSource('/api/channel/weixin/login')` SSE 接收事件
|
||||
- 通过 `qr` 事件接收 QR 码数据(raw ticket string)
|
||||
- 使用 `QRCodeSVG` 组件渲染 QR 码(160x160px)
|
||||
6. 用户用微信扫描二维码
|
||||
7. 扫描后状态变为 `scanned`:显示 Spin loading + "Scanned, waiting for confirmation..."
|
||||
8. 确认后自动调用 `enableWeixinPlugin(accountId, botToken)` 启用渠道
|
||||
9. 状态变为 `connected`:显示绿色对钩 + "Connected" + "Disconnect"按钮
|
||||
10. 配对/授权用户管理流程与 Telegram 相同(见 F-WEBUI-13)
|
||||
|
||||
**登录状态机**:
|
||||
|
||||
```
|
||||
idle → loading_qr → showing_qr → scanned → connected
|
||||
↑ ↓ ↓ ↓
|
||||
←─── (error/expired) ←───┴──────────┘
|
||||
```
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- QR 码过期/too many 尝试:warning toast "QR code expired, please try again",回到 idle
|
||||
- EventSource error 事件(WebUI):error toast "WeChat login failed",回到 idle
|
||||
- Electron IPC 错误(非 Aborted):error toast "WeChat login failed"
|
||||
- 用户取消(Aborted):静默回到 idle,不显示错误
|
||||
- enablePlugin 失败:error toast,回到 idle
|
||||
- Disconnect 失败:error toast
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] Electron 端通过 IPC 接收 QR 码(base64 img)
|
||||
- [ ] WebUI 端通过 EventSource SSE 接收 QR 码(QRCodeSVG 渲染)
|
||||
- [ ] 扫码后显示"等待确认"loading 状态
|
||||
- [ ] 登录成功后显示 Connected 状态 + Disconnect 按钮
|
||||
- [ ] QR 码过期有明确提示,可重新发起
|
||||
- [ ] 断开连接(Disconnect)可正常禁用渠道
|
||||
- [ ] 组件卸载时 EventSource 正确关闭(无连接泄漏)
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-17) WeCom 渠道配置(WebSocket 长连接) [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过配置企业微信机器人 ID 和密钥连接 WeCom 渠道,使用 WebSocket 长连接模式无需回调 URL。
|
||||
|
||||
**前置条件**:用户已在企业微信管理后台创建应用并获取 Bot ID 和 Secret
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 展开 WeCom 卡片,显示配置面板
|
||||
2. 顶部橙色提示横幅说明:WeCom 使用 WebSocket 长连接模式,无需配置回调 URL
|
||||
3. **必填字段**:
|
||||
- Bot ID(Input,必填,红色 `*` 标记,有授权用户时 disabled)
|
||||
- Secret(Input.Password,必填,红色 `*` 标记,有授权用户时 disabled)
|
||||
4. 右下角有蓝色链接"企业微信开发文档",打开 `https://developer.work.weixin.qq.com/document/path/101463`
|
||||
5. 点击"Save & Enable"按钮(区别于其他渠道的 "Test & Connect")
|
||||
6. 保存并启用成功:toast "WeCom channel enabled",刷新状态
|
||||
7. 配对/授权用户管理流程与 Telegram 相同(见 F-WEBUI-13)
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- Bot ID 或 Secret 为空:warning toast "Please enter Bot ID and Secret",字段标红
|
||||
- 启用无凭据(Switch 直接打开):warning toast "Please save Token and EncodingAESKey first"
|
||||
- 保存/启用失败:error toast 显示错误信息
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 顶部显示 WebSocket 长连接模式说明横幅
|
||||
- [ ] Bot ID 和 Secret 为必填项
|
||||
- [ ] Save & Enable 按钮一步保存并启用
|
||||
- [ ] 企业微信文档链接可正常打开
|
||||
- [ ] 有授权用户时凭据输入框禁用
|
||||
- [ ] 配对/授权用户管理功能正常
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-18) 渠道 Agent 选择 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望为每个 IM 渠道独立选择使用的 Agent,以便不同渠道使用不同的 AI 后端。
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 每个渠道配置面板内有"Agent"选择行
|
||||
2. 点击 Dropdown 按钮展开 Agent 列表(来自 `acpConversation.getAvailableAgents.invoke()`,过滤掉 preset Agent)
|
||||
3. 默认选中"Gemini CLI"(`{ backend: 'gemini' }`)
|
||||
4. 选择新 Agent 后:
|
||||
- 本地状态更新
|
||||
- `ConfigStorage.set('assistant.{platform}.agent', agent)` 持久化
|
||||
- `channel.syncChannelSettings.invoke({ platform, agent })` 同步到后端
|
||||
- toast "Agent switched successfully"
|
||||
5. 页面重新打开时从 ConfigStorage 恢复之前的选择
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- Agent 列表加载失败:默认显示 "Gemini CLI" 作为唯一选项
|
||||
- 持久化失败:error toast "Failed to save"
|
||||
- syncChannelSettings 失败:仅 console.warn,不影响本地选择
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 每个渠道有独立的 Agent Dropdown
|
||||
- [ ] Agent 列表从后端加载,过滤 preset
|
||||
- [ ] 切换 Agent 有成功 toast
|
||||
- [ ] 选择持久化,重新打开页面恢复
|
||||
- [ ] Agent 列表为空时显示 "Gemini CLI" 兜底
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-19) 渠道默认模型选择 [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望为每个 IM 渠道独立设置默认使用的 AI 模型,以便控制不同渠道的回复质量和速度。
|
||||
|
||||
**前置条件**:当前 Agent 为 Gemini 兼容类型(`backend === 'gemini'` 或 `'aionrs'`)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 每个渠道配置面板内有"Default Model"选择行
|
||||
2. 使用 `GeminiModelSelector` 组件渲染模型下拉选择
|
||||
3. 选择模型后:
|
||||
- `ConfigStorage.set('assistant.{platform}.defaultModel', { id, useModel })` 持久化
|
||||
- `channel.syncChannelSettings.invoke({ platform, agent, model })` 同步到后端
|
||||
- toast "Model switched successfully"
|
||||
4. 页面重新打开时自动恢复保存的模型(`useChannelModelSelection` hook,最多重试 5 次匹配 provider)
|
||||
|
||||
**条件渲染**:
|
||||
|
||||
- Agent 为 Gemini/aionrs 类型:正常显示模型选择器
|
||||
- Agent 为其他类型(如 Custom Agent):显示灰色文案"Automatically follow the model when CLI is running",选择器禁用
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 保存的 Provider ID 已失效(provider 被删除):重试最多 5 次后放弃,使用默认模型
|
||||
- 模型保存失败:error toast "Failed to save model"
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 每个渠道有独立的模型选择
|
||||
- [ ] 仅 Gemini 兼容 Agent 显示模型选择器
|
||||
- [ ] 非 Gemini Agent 显示禁用状态 + 提示文案
|
||||
- [ ] 模型选择持久化,重新打开页面恢复
|
||||
- [ ] Provider 失效时有重试机制(最多 5 次)
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-20) 渠道配对与用户授权(通用流程) [已实现]
|
||||
|
||||
**用户故事**:作为用户,我希望通过配对码机制控制哪些 IM 用户可以与我的 AionUi 交互,保证安全性。
|
||||
|
||||
**适用渠道**:Telegram、Lark、DingTalk、WeChat、WeCom(全部 5 个已实现渠道共享此流程)
|
||||
|
||||
**正常流程**(用户视角):
|
||||
|
||||
1. 渠道启用且已连接后,IM 用户向 Bot 发送消息
|
||||
2. 首次消息触发配对请求,AionUi 通过 `channel.pairingRequested.on()` 实时推送到设置页面
|
||||
3. "Pending Pairing Requests"区域显示配对卡片:
|
||||
- 用户显示名(`displayName`,未知则显示"Unknown User")
|
||||
- 配对码(`code`)+ 复制按钮
|
||||
- 剩余时间(`expiresAt`,单位 min)
|
||||
- "Approve"按钮(主色调)+ "Reject"按钮(红色危险态)
|
||||
4. 点击 Approve:`channel.approvePairing.invoke({ code })` → 成功 toast → 用户移入"Authorized Users"
|
||||
5. `channel.userAuthorized.on()` 实时推送,自动从 pending 列表移除并添加到 authorized 列表
|
||||
6. "Authorized Users"区域显示:
|
||||
- 用户显示名
|
||||
- 平台类型 + 授权时间
|
||||
- "Revoke access"删除按钮(红色危险态)
|
||||
7. 点击 Revoke:`channel.revokeUser.invoke({ userId })` → 成功 toast → 用户从列表移除
|
||||
|
||||
**凭据锁定机制**:当 `authorizedUsers.length > 0` 时,凭据输入框和测试按钮变为 disabled,tooltip 提示"请先关闭渠道并删除所有授权用户后才能修改配置"。此规则适用于 Telegram(Token)、Lark(AppID/Secret)、DingTalk(ClientID/Secret)、WeCom(BotID/Secret)。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 加载配对/授权列表失败:仅日志,空列表
|
||||
- 批准配对失败:error toast 显示后端错误
|
||||
- 拒绝配对失败:error toast
|
||||
- 撤销用户失败:error toast
|
||||
- 实时事件去重:已存在相同 code/id 的不重复添加
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 配对请求实时显示(无需手动刷新)
|
||||
- [ ] 可批准/拒绝配对请求
|
||||
- [ ] 批准后用户实时出现在授权列表
|
||||
- [ ] 可撤销已授权用户
|
||||
- [ ] 有授权用户时凭据/测试按钮禁用 + tooltip 提示
|
||||
- [ ] 配对请求显示剩余时间
|
||||
- [ ] 配对请求/授权列表均有手动刷新按钮
|
||||
- [ ] 空列表显示 Empty 占位组件
|
||||
|
||||
---
|
||||
|
||||
## (F-WEBUI-21) 扩展渠道支持 [已实现]
|
||||
|
||||
**用户故事**:作为扩展开发者,我希望通过扩展 manifest 注册自定义 IM 渠道,使用动态表单配置凭据。
|
||||
|
||||
**正常流程**(用户视角/开发者视角):
|
||||
|
||||
1. 扩展通过 `extensionMeta` 声明 `credentialFields` 和 `configFields`,定义表单字段
|
||||
2. 页面加载时,扩展渠道从 `getPluginStatus` 返回的非内置类型中识别
|
||||
3. 扩展渠道卡片在 5 个内置渠道之后、coming_soon 渠道之前显示
|
||||
4. 卡片 header 显示蓝色 "ext" 徽标(`isExtension: true`)
|
||||
5. 展开后根据 `extensionMeta` 的字段 schema 动态渲染表单
|
||||
|
||||
**支持的字段类型**:
|
||||
|
||||
| 类型 | 渲染组件 | 说明 |
|
||||
| ---------- | ---------------------- | ----------------------------- |
|
||||
| `text` | `Input` | 普通文本输入 |
|
||||
| `password` | `Input[type=password]` | 密码输入 |
|
||||
| `select` | `Select` | 下拉选择,options 来自 schema |
|
||||
| `number` | `InputNumber` | 数值输入 |
|
||||
| `boolean` | `Switch` | 开关切换 |
|
||||
|
||||
**启用前校验**:遍历 `credentialFields` 中 `required: true` 的字段,若有未填写的(空字符串或 undefined),warning toast 提示"Please fill required field: {fieldLabel}"
|
||||
|
||||
**特殊处理 — `ext-wecom-bot` 扩展**:当扩展类型为 `ext-wecom-bot` 时,显示橙色提示横幅说明回调 URL(本机/局域网/公网),包含 WebUI 状态(`localUrl`/`networkUrl`)。
|
||||
|
||||
**异常情况**:
|
||||
|
||||
- 扩展无配置字段:显示 `extensionMeta.description` 或 "No extra configuration required."
|
||||
- 启用缺少必填字段:warning toast,不提交
|
||||
- 启用/禁用失败:error toast
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 扩展渠道在内置渠道之后显示
|
||||
- [ ] 卡片显示蓝色 "ext" 徽标
|
||||
- [ ] 5 种字段类型正确渲染
|
||||
- [ ] 必填字段校验生效
|
||||
- [ ] 字段默认值从 `field.default` 初始化
|
||||
- [ ] ext-wecom-bot 显示回调 URL 信息横幅
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:IPC 通信链路(Channels Tab)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ ChannelModalContent.tsx (Channels Tab): │
|
||||
│ 渠道状态: │
|
||||
│ ├─ channel.getPluginStatus.invoke() [加载全部渠道状态] │
|
||||
│ └─ channel.pluginStatusChanged.on() [实时监听状态变更] │
|
||||
│ 渠道启停: │
|
||||
│ ├─ channel.enablePlugin.invoke({ pluginId, config }) [启用] │
|
||||
│ ├─ channel.disablePlugin.invoke({ pluginId }) [禁用] │
|
||||
│ └─ channel.testPlugin.invoke({ pluginId, token, extraConfig? }) │
|
||||
│ 配对授权: │
|
||||
│ ├─ channel.getPendingPairings.invoke() [加载待配对] │
|
||||
│ ├─ channel.getAuthorizedUsers.invoke() [加载已授权] │
|
||||
│ ├─ channel.approvePairing.invoke({ code }) [批准] │
|
||||
│ ├─ channel.rejectPairing.invoke({ code }) [拒绝] │
|
||||
│ ├─ channel.revokeUser.invoke({ userId }) [撤销] │
|
||||
│ ├─ channel.pairingRequested.on() [实时配对请求] │
|
||||
│ └─ channel.userAuthorized.on() [实时授权通知] │
|
||||
│ Agent/模型: │
|
||||
│ ├─ acpConversation.getAvailableAgents.invoke() [加载 Agent 列表] │
|
||||
│ ├─ channel.syncChannelSettings.invoke({ platform, agent, model }) │
|
||||
│ └─ ConfigStorage.get/set('assistant.{platform}.agent/defaultModel')│
|
||||
│ WeChat 登录: │
|
||||
│ ├─ electronAPI.weixinLoginStart() [Electron IPC] │
|
||||
│ ├─ electronAPI.weixinLoginOnQR/OnScanned/OnDone [IPC 事件] │
|
||||
│ └─ EventSource('/api/channel/weixin/login') [WebUI SSE] │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:Toast 通知汇总
|
||||
|
||||
| 触发操作 | Toast 类型 | i18n Key |
|
||||
| ------------------------------- | ---------- | -------------------------------------------------------------------------- |
|
||||
| 渠道插件启用成功 | success | `settings.assistant.pluginEnabled` / `settings.{platform}.pluginEnabled` |
|
||||
| 渠道插件禁用成功 | success | `settings.assistant.pluginDisabled` / `settings.{platform}.pluginDisabled` |
|
||||
| 渠道插件启用/禁用失败 | error | `settings.assistant.enableFailed` / `settings.{platform}.enableFailed` |
|
||||
| 连接测试成功 | success | `settings.assistant.connectionSuccess` / `settings.lark.connectionSuccess` |
|
||||
| 连接测试失败 | error | `settings.assistant.connectionFailed` / 后端错误信息 |
|
||||
| Token 为空测试 | warning | `settings.assistant.tokenRequired` |
|
||||
| 凭据为空(Lark/DingTalk/WeCom) | warning | `settings.{platform}.credentialsRequired` |
|
||||
| 配对批准成功 | success | `settings.assistant.pairingApproved` |
|
||||
| 配对拒绝 | info | `settings.assistant.pairingRejected` |
|
||||
| 用户撤销成功 | success | `settings.assistant.userRevoked` |
|
||||
| Agent 切换成功 | success | `settings.assistant.agentSwitched` |
|
||||
| 模型切换成功 | success | `settings.assistant.modelSwitched` |
|
||||
| 模型保存失败 | error | `settings.assistant.modelSaveFailed` |
|
||||
| WeChat 登录过期 | warning | `settings.weixin.loginExpired` |
|
||||
| WeChat 登录失败 | error | `settings.weixin.loginError` |
|
||||
| WeCom 保存启用成功 | success | `settings.wecom.pluginEnabled` |
|
||||
|
||||
---
|
||||
|
||||
## 附录 C:已知局限汇总
|
||||
|
||||
| # | 功能点 | 局限描述 | 来源 |
|
||||
| --- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| 1 | F-WEBUI-13 | Telegram Token 锁定机制依赖 `authorizedUsers.length > 0`,如果后端授权数据丢失但 Token 实际在用中,用户可能误修改 Token 导致断连 | R2 初稿 |
|
||||
| 2 | F-WEBUI-16 | WeChat 扫码登录的 EventSource(WebUI 模式)在组件卸载时关闭,但 Electron IPC 模式中 `weixinLoginStart` 是 await Promise,组件卸载不会取消该 Promise | R2 初稿 |
|
||||
| 3 | F-WEBUI-18 | Agent 列表通过 `acpConversation.getAvailableAgents.invoke()` 加载,每个渠道独立调用,无缓存共享(5 个渠道 = 5 次相同 IPC 调用) | R2 初稿 |
|
||||
| 4 | F-WEBUI-19 | `useChannelModelSelection` 恢复保存的 provider 时最多重试 5 次,provider 被删除后静默降级为默认模型,无用户提示 | R2 初稿 |
|
||||
| 5 | F-WEBUI-20 | 配对请求的 `expiresAt` 由后端生成,UI 显示剩余时间为 `Math.ceil((expiresAt - Date.now()) / 60000)`,无自动倒计时刷新(显示值仅在组件渲染时计算) | R2 初稿 |
|
||||
| 6 | F-WEBUI-17 | WeCom "Save & Enable" 按钮 warning 提示 "Please save Token and EncodingAESKey first" 与实际字段名 (Bot ID / Secret) 不一致 | R2 初稿 |
|
||||
| 7 | F-WEBUI-21 | 扩展渠道字段值存储在组件 state(`extensionFieldValues`),页面离开后丢失;已启用的扩展字段值不从后端恢复显示 | R2 初稿 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user