Files
tergent-android/docs/TERGENT-UI-SPEC.md
2026-05-23 03:26:48 +00:00

1068 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tergent Android — UI 规范文档 v1.0
> 版本: 1.0 | 状态: 草稿 | 作者: 茂之核
> 基于: TERGENT-DESIGN.md + 界面布局参考 (tergent-ui-screens.html)
> 目标: Pixso 高保真设计输入 + 开发实现手册
---
## 目录
1. [设计语言](#1-设计语言)
2. [页面流程](#2-页面流程)
3. [9 个页面详细设计](#3-9-个页面详细设计)
4. [推荐第三方库](#4-推荐第三方库)
5. [改造优先级细化](#5-改造优先级细化)
6. [附录](#6-附录)
---
## 1. 设计语言
### 1.1 配色方案
Tergent 配色不与 Material Design 3 强绑定,采用更简洁、国内用户熟悉的风格。参考微信、飞书、即刻的用色逻辑,以中性蓝为主色,减少渐变和复杂阴影。
| Token | 色值 (Light) | 色值 (Dark) | 用途 |
|-------|-------------|-------------|------|
| `tergent_primary` | `#1C6BFF` | `#5B8CFF` | 主色:按钮、Tab 选中、链接、品牌强调 |
| `tergent_primary_dark` | `#1551CC` | `#3A6FE0` | 主色深色变体:按压态、标题强调 |
| `tergent_primary_bg` | `#EDF2FF` | `#1A2744` | 主色背景:选中态、轻高亮 |
| `tergent_accent` | `#06D6A0` | `#3AE0B5` | 辅助色:成功、在线、正面反馈 |
| `tergent_warning` | `#F59E0B` | `#FBBF24` | 警告色:连接不稳定、通知 |
| `tergent_danger` | `#EF4444` | `#F87171` | 危险色:错误、断开、删除 |
| `tergent_bg` | `#FFFFFF` | `#111827` | 页面背景 |
| `tergent_surface` | `#F8FAFC` | `#1F2937` | 卡片/列表背景 |
| `tergent_surface_raised` | `#FFFFFF` | `#374151` | 弹窗/Sheet 背景 |
| `tergent_border` | `#E5E7EB` | `#374151` | 分割线、边框 |
| `tergent_text_primary` | `#111827` | `#F9FAFB` | 主要文字 |
| `tergent_text_secondary` | `#6B7280` | `#9CA3AF` | 次要文字 |
| `tergent_text_tertiary` | `#9CA3AF` | `#6B7280` | 提示文字 |
| `tergent_nav_bar` | `#FFFFFF` | `#111827` | 底部导航栏背景 |
| `tergent_chat_bubble_self` | `#1C6BFF` | `#2563EB` | 自己发出的聊天气泡 |
| `tergent_chat_bubble_other` | `#F3F4F6` | `#2D3748` | 对方聊天气泡 |
| `tergent_success_green` | `#10B981` | `#34D399` | 在线指示、成功状态 |
| `tergent_input_bg` | `#F3F4F6` | `#1F2937` | 输入框背景 |
**设计原则:**
- 主色用纯蓝 `#1C6BFF`,避免紫/渐变(原 OpenClaw 风格),更接近国内主流 IM 应用
- 辅助色用绿松石 `#06D6A0` 做成功/在线态,清爽不刺眼
- 界面使用白色/浅灰背景,卡片使用圆角矩形,不依赖大块色彩区域
- 深色模式全程同步,取色逻辑为 Light 反转明度 + 降低饱和度
### 1.2 中文字体
| 层级 | 字体 | Fallback | 用途 |
|------|------|----------|------|
| 应用全局 | `"PingFang SC"` | `"Microsoft YaHei"`, `system-ui`, `sans-serif` | 所有 UI 文字 |
| 聊天内容 | `"PingFang SC"` | `"Microsoft YaHei"`, `-apple-system` | 消息正文 |
| 等宽文字 | `"JetBrains Mono"` | `"Cascadia Code"`, `monospace` | 日志、代码块、诊断信息 |
| 品牌字体 | `"PingFang SC"` | — | Logo、标题大字 |
**行高规范:**
- 正文 14px/16px: 1.5 倍行高
- 小字 11px/12px: 1.4 倍行高
- 标题 16px/18px: 1.3 倍行高
### 1.3 图标风格
- **风格**:线性描边图标,2px 描边,圆角端点 (round cap),填充色为当前文本色
- **来源**`Remix Icon`(国内开源图标库,飞书也在用)+ 自定义品牌图标
- **Compose 兼容**:使用 `androidx.compose.material.icons.Icons.Default` 兜底,Remix 图标以 `ImageVector` 形式集成
- **尺寸规范**
- 底部 Tab: 24×24dp
- 列表前导图标: 20×20dp
- 按钮图标: 18×18dp
- 空状态大图标: 48×48dp
- **颜色**:跟随 `tergent_text_secondary`,选中态变为主色 `tergent_primary`
### 1.4 间距 / 圆角 / 阴影
**间距体系(8 点网格):**
| Token | dp | 使用场景 |
|-------|----|---------|
| spacing_xs | 4 | 图标间距、小间隙 |
| spacing_sm | 8 | 列表项内边距 |
| spacing_md | 12 | 组件间间距 |
| spacing_lg | 16 | 卡片内边距、外边距 |
| spacing_xl | 24 | 段落间距、区块间距 |
| spacing_2xl | 32 | 大区块间距 |
**圆角体系:**
| Token | dp | 使用场景 |
|-------|----|---------|
| radius_sm | 4 | 标签、下拉项 |
| radius_md | 8 | 卡片、输入框、按钮 |
| radius_lg | 12 | 弹窗、聊天气泡、外露卡片 |
| radius_xl | 16 | 顶部/底部 Sheet |
| radius_full | 999 | 圆形按钮、头像、语音 Orb |
**阴影:**
| Token | 值 (Light) | 值 (Dark) | 使用场景 |
|-------|------------|-----------|----------|
| shadow_sm | `0 1px 2px rgba(0,0,0,0.06)` | `0 1px 2px rgba(0,0,0,0.4)` | 列表项、小卡片 |
| shadow_md | `0 4px 6px -1px rgba(0,0,0,0.1)` | `0 4px 6px rgba(0,0,0,0.5)` | 卡片、底部导航栏 |
| shadow_lg | `0 10px 15px -3px rgba(0,0,0,0.1)` | `0 10px 15px rgba(0,0,0,0.5)` | 弹窗、BottomSheet |
**Compose 实现参考:**
- 使用 `Modifier.shadow(elevation, shape, clip = true)` 实现 Android elevation shadow
- Light 模式下 elevation: sm=1dp, md=4dp, lg=8dp
- Dark 模式下不依赖系统 drop shadow,使用 `AmbientElevation` 配合 `surfaceTonalElevation`
---
## 2. 页面流程
### 2.1 用户全流程 (ASCII)
```
┌──────────────┐ 首次启动? ┌──────────────────┐
│ 启动 App │ ───────────────▶ │ Onboarding 引导 │
│ (Splash) │ │ (扫码/手动配对) │
└──────┬───────┘ └────────┬─────────┘
│ 已有配对 │ 配对完成
▼ ▼
┌───────────────────────────────────────────────────┐
│ 主界面 (PostOnboardingTabs) │
│ │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │ 连接 │ 聊天 │ 语音 │ 屏幕 │ 设置 │ │
│ │ Tab 0 │ Tab 1 │ Tab 2 │ Tab 3 │ Tab 4 │ │
│ └───┬────┴───┬────┴───┬────┴───┬────┴───┬────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │连接 │ │聊天 │ │语音 │ │屏幕 │ │设置页 │ │
│ │状态 │ │对话 │ │对讲 │ │WebView│ │(多子页) │ │
│ │&网关 │ │流 │ │ │ │canvas │ │ │ │
│ │管理 │ │ │ │ │ │ │ │ │ │
│ └──┬───┘ └──┬────┘ └──────┘ └───────┘ └─────┬────┘ │
│ │ │ │ │
│ │ │ ┌───────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 设备管理页 │ │ 通知页 │ │ 连接诊断 │ │
│ │ (子路由) │ │ (子路由) │ │ (子路由) │ │
│ └──────────────┘ └──────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────┘
```
### 2.2 底部 Tab 默认排序
底部导航栏共 5 个 Tab,初期固定(不可编辑):
| 序号 | 名称 | 图标 (Remix) | 页面 | 优先级 |
|------|------|-------------|------|--------|
| 0 | 连接 | `ri-link` | ConnectScreen | P0 |
| 1 | 聊天 | `ri-chat-3-line` | ChatScreen | P0 |
| 2 | 语音 | `ri-mic-line` | VoiceScreen | P1 |
| 3 | 屏幕 | `ri-smartphone-line` | CanvasScreen | P1 |
| 4 | 设置 | `ri-settings-3-line` | SettingsScreen (多子页) | P0 |
**排序逻辑:**
1. 连接状态是首要信息 → 放最左边,一眼能看到连接状况
2. 聊天是核心使用场景 → 紧随其后,用户主要交互在此
3. 语音和屏幕是特色功能 → 中间位置,容易触及
4. 设置靠右提供访问入口 → 最右侧,符合 Android 操作习惯
### 2.3 页面之间的导航关系
| 从 → 到 | 触发方式 | 备注 |
|---------|---------|------|
| Splash → Onboarding | 首次启动 / 无配对记录 | 自动跳转 |
| Splash → 主界面 | 已有有效配对记录 | 跳过引导 |
| Onboarding → 主界面 | 配对成功 | 自动跳转 |
| 连接页 → 设备管理 | 点击"已配对网关"卡片 | Navigation push |
| 连接页 → 诊断页 | 点击"连接诊断"按钮 | Navigation push |
| 连接页 → 手动添加 | 点击"+ 手动添加网关" | Dialog / BottomSheet |
| 设置 → 通知页 | 点击"通知转发"行 | Navigation push |
| 设置 → 连接设置 | 点击"连接设置"行 | Navigation push |
| 聊天 → 设置 Tab | 点击导航栏第五个 Tab | Tab 切换 |
| 所有页面 → 其他 Tab | 点击对应 Tab | Tab 切换 |
---
## 3. 9 个页面详细设计
### 3.1 引导页 (Onboarding) — P0
#### 页面用途
首次安装后的配对引导流程。用户通过扫码(网关二维码)或手动输入方式连接到 Tergent 网关。
#### 用户场景
- 用户刚安装 Tergent,打开即看到此页
- 在电脑/服务器上启动 Tergent 网关后,用手机扫描配对二维码
- 扫码失败(光线差/损坏)时,可手动输入网关地址
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ │
│ ┌────────────────┐ │
│ │ Tergent │ │ ← Logo 60×60dp, 圆角16dp
│ │ 品牌图标 │ │ 渐变背景
│ └────────────────┘ │
│ │
│ Tergent │ ← 应用名 20sp, Bold
│ │
│ 连接你的设备,随时掌控 │ ← 副标题 13sp, tertiary
│ │
│ │
│ ┌──────────────────┐ │
│ │ │ │ ← QR 码区域 200×200dp
│ │ 「扫码框」 │ │ 虚线边框, 圆角8dp
│ │ │ │ 相机实时扫描
│ └──────────────────┘ │
│ │
│ 扫码配对 或 手动输入 │ ← 12sp, tertiary
│ │
│ ┌────────────────────┐ │
│ │ 开始连接 → │ │ ← 主色按钮, 圆角24dp
│ └────────────────────┘ │ padding 10×28dp
│ │
│ v0.1.0 alpha │ ← 11sp, tertiary
│ │
└──────────────────────────────┘
```
#### 组件明细
| 组件 | Compose 类型 | 规格 |
|------|-------------|------|
| Logo | `Image` (painterResource) | 60×60dp, 圆角 16dp, 品牌渐变 |
| 应用名 | `Text` | 20sp, FontWeight.Bold, `tergent_text_primary` |
| 副标题 | `Text` | 13sp, `tergent_text_secondary` |
| QR 扫描区 | `Box` + CameraX preview | 200×200dp, 虚线 BorderStroke, 圆角 8dp |
| 手动输入链接 | `TextButton` | "手动输入", 12sp, 主色/下划线 |
| 开始连接按钮 | `Button` | 主色填充, 圆角 24dp, 白色文字 14sp SemiBold |
| 版本号 | `Text` | 11sp, `tergent_text_tertiary` |
#### 交互说明
| 操作 | 行为 |
|------|------|
| 摄像头自动扫描 QR | 自动识别二维码中的 gateway connect info |
| 点击 QR 区域 | 手动触发一次扫码 |
| 点击"手动输入" | 弹出 `AlertDialog`:输入网关地址:端口 |
| 点击"开始连接" | 触发 `GatewaySession.connect()`, 按钮显示 Loading |
| 连接成功 | 自动跳转到主界面 |
| 连接失败 | Snackbar 提示具体错误原因 |
#### 需要 API 获取的数据
- 无需 API(配对过程本地完成,GatewaySession.connect() 处理认证)
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 默认 | QR 扫描区虚线框 + 扫码提示文字 |
| 加载中 | 按钮变 `CircularProgressIndicator(4dp)` + "连接中…" |
| 错误 | Snackbar 显示具体错误原因 |
| 权限拒绝 | 回退为手动输入模式 |
| 相机不可用 | 自动降级为纯手动输入页面 |
---
### 3.2 连接页 (Connect) — P0
#### 页面用途
展示与 Tergent 网关的连接状态、已配对设备信息、网络概况。
#### 用户场景
- 日常检查:打开 App 首页即见连接状态
- 连接断开时:显示断开原因 + 自动重连计数
- 需要切换网关:点击"切换网关"更换目标
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ 连接 [⋮]│ ← TopAppBar, 右侧菜单
├──────────────────────────────┤
│ │
│ ┌── [●] 已连接 · hdtime ──┐│ ← 连接状态条
│ │ 网关: gateway.hdtime ││ 绿/黄/红 + 文字
│ └──────────────────────────┘│ 圆角6dp, 12sp
│ │
│ 已配对设备: 茂之钳 (main) │ ← 13sp, secondary
│ │
│ ┌──────────────────────────┐│
│ │ 连接信息 ││ ← 卡片, 圆角8dp
│ │ 网关地址 hdtime.space ││ border #E5E7EB
│ │ 端口 443 (WSS) ││ 标签12sp + 值12sp
│ │ 连接状态 ● 在线 ││
│ └──────────────────────────┘│
│ │
│ ┌──────────┐ ┌────────────┐│
│ │ 切换网关 │ │ 重新配对 ││ ← Outlined + Filled btn
│ └──────────┘ └────────────┘│
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 连接状态 | `NodeRuntime.connectionState` (StateFlow) |
| 网关端点 | `NodeRuntime.gatewayEndpoint` |
| Ping 延迟 | `NodeRuntime.lastPingMs` |
| 已注册设备 | `NodeRuntime.registeredDevices` |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 在线 | 绿色 `#DCFCE7` + "已连接" |
| 连接中 | 黄色 `#FEF3C7` + "正在连接…" + 脉冲动画 |
| 重连中 | 黄色/灰色 + "第 N 次重连中…" |
| 断开 | 红色 `#FEE2E2` + "已断开" + "点击诊断" |
| 无网络 | 灰色 + "无网络连接" |
---
### 3.3 聊天页 (Chat) — P0
#### 页面用途
与 Tergent AI Agent 文字对话的核心界面。支持流式回复、多轮对话、Markdown、图片、会话管理。
#### 用户场景
- 日常问答:提问后获取流式回复
- 任务执行:发指令让 Agent 执行操作
- 历史回溯:翻阅历史对话
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ [●] 茂之钳 [⋮] [+]│ ← 头像+名称+菜单+新建
│ 在线 │
├──────────────────────────────┤
│ │
│ ┌────────────────────────┐ │
│ │ 你好!有什么需要帮忙的? │ │ ← 对方气泡 (左对齐)
│ └────────────────────────┘ │ 浅灰底, 圆角12dp
│ │
│ ┌────────────────┐ │
│ │ 帮我查一下天气 │ │ ← 自己气泡 (右对齐)
│ │ 14:30 │ │ 蓝色底, 圆角12dp
│ └────────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 正在输入... │ │ ← 打字指示器
│ └──────────────┘ │ animate 3 dots
│ │
│ ─── Tergent is working ─│ ← 状态提示
│ │
├──────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ [输入消息...] [➤]│ │ ← 输入区, 圆角16dp
│ └────────────────────────┘ │
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 | 用途 |
|------|------|------|
| 消息列表 | `ChatController.messages` (StateFlow) | LazyColumn 数据 |
| 会话列表 | `ChatController.sessions` | 切换 session |
| 连接状态 | `NodeRuntime.connectionState` | 输入框可用性 |
| 流式回复 | operatorSession chat.send 回调 | 实时更新气泡 |
| 历史消息 | 缓存 / 网关 pull | 分页加载 |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 空对话 | 居中占位: 48dp 图标 + "开始对话吧" |
| 输入中 | 输入框正常, 发送按钮高亮 |
| 发送中 | 气泡右下角 CircularProgressIndicator(4dp) |
| 发送失败 | 气泡右上角红色 ⚠️, 点击重发 |
| 流式加载 | typing indicator |
| 连接断开 | 输入框禁用 + 顶部 banner |
---
### 3.4 设置页 (Settings) — P0
#### 页面用途
应用全局设置入口。个人信息 + 功能开关 + 子页面路由。
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ 设置 │ ← TopAppBar
├──────────────────────────────┤
│ │
│ ┌─── 个人信息卡片 ────────┐ │
│ │ [●] Tergent Node │ │ ← 40dp 渐变头像
│ │ v0.1.0 │ │ 14sp + 11sp
│ └─────────────────────────┘ │
│ │
│ ┌── 开关行 ──────────────┐ │
│ │ 🔔 通知转发 [🔘] │ │
│ │ 📷 相机权限 [🔘] │ │
│ │ 📍 定位权限 [🔘] │ │
│ │ 🤖 语音唤醒 [🔘] │ │
│ │ 🌙 深色模式 [🔘] │ │
│ │ 📡 连接设置 › │ │
│ │ 📋 通知历史 › │ │
│ └────────────────────────┘ │
│ │
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 持久化设置 | SecurePrefs / DataStore |
| 权限状态 | ContextCompat.checkSelfPermission() |
| 前台服务状态 | TergentForegroundService.isRunning |
| 连接统计 | GatewaySession.getStats() |
**子路由页面:** 连接设置(P0) / 通知历史(P2) / 关于(P2)
---
### 3.5 语音页 (Voice) — P1
#### 页面用途
与 Tergent 进行语音对话。支持点击录音 + TTS 语音回复。
#### 用户场景
- 开车/双手占用时语音提问
- 快速语音查询,无需打字
- 连续语音对话
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ 语音 │ ← TopAppBar
├──────────────────────────────┤
│ │
│ 点击按钮开始说话 │ ← 13sp, tertiary
│ │
│ ┌───────┐ │
│ │ ● │ │ ← 语音 Orb
│ │ Orb │ │ 72×72dp
│ └───────┘ │ 渐变 background
│ │
│ ▎▌█▌▎ ▎▌█▌▎ │ ← 声波可视化 (录制时)
│ │
│ "帮我查一下今天的日程" │ ← 识别文本 12sp italic
│ │
│ ┌────────────────────────┐ │
│ │ Tergent: 今天有2个会议 │ │ ← 回复气泡
│ └────────────────────────┘ │ 浅灰bg, 圆角8dp
│ │
│ 长按说话 / 点击开始 │ ← 11sp
│ │
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 麦克风数据 | MicCaptureManager (AudioRecord PCM) |
| PTT 控制 | TalkModeManager (PttStart/Stop/Cancel) |
| TTS 播放 | TalkSpeakClient |
| 语音识别 | gateway transcription 回调 |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 空闲 | Orb 静态, "点击按钮开始说话" |
| 录音中 | Orb 呼吸动画 + 声波动画 + "正在听…" |
| 识别中 | Orb 微动 + "识别中…" |
| 识别完成 | 显示识别文本 |
| TTS 播放中 | Orb 微动 + 气泡逐步显示 |
| 连接断开 | Orb 灰色 + "需要网络连接" |
| 权限拒绝 | Orb 灰色 + "请允许麦克风权限" |
---
### 3.6 画布页 (Canvas) — P1
#### 页面用途
Agent 在 WebView 上展示交互式 HTML/CSS 内容。支持仪表盘、图形、表单等。
#### 用户场景
- Agent 展示实时数据仪表盘
- 交互式表单填写
- 富媒体内容呈现
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ 屏幕 [⊞][↻]│ ← TopAppBar
├──────────────────────────────┤
│ [📷截图] [🔍缩放] [↻刷新] │ ← 操作标签
│ │
│ ┌────────────────────────┐ │
│ │ │ │
│ │ WebView 内容区域 │ │ ← Agent 渲染的 HTML
│ │ │ │ 可交互点击/输入
│ │ │ │
│ └────────────────────────┘ │
│ │
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 页面导航 | CanvasController.navigate() |
| JavaScript eval | CanvasController.eval() |
| 截图 | CanvasController.snapshotBase64() |
| A2UI 消息 | A2UIHandler.push() / pushJSONL() |
| URL 白名单 | CanvasActionTrust |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 空白 | 占位文字 "等待 Agent 展示内容" |
| 加载中 | 浅灰 bg + Center CircularProgressIndicator |
| 已加载 | 正常 WebView 内容 |
| A2UI 就绪 | 可接收 push/pushJSONL 实时更新 |
| 加载失败 | Error state: "内容加载失败" + 重试按钮 |
---
### 3.7 节点管理页 (Devices) — P1
#### 页面用途
已配对网关列表 + 发现的网关列表。管理多网关场景。
#### 用户场景
- 查看已配对的网关和在线状态
- 发现局域网内的其他 Tergent 网关
- 手动添加不在同一网段的网关
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ [←] 设备 │ ← TopAppBar 带返回
├──────────────────────────────┤
│ │
│ 已配对网关 │ ← Section header 12sp Bold
│ │
│ [●] hdtime.space │ ← 设备行
│ 茂之钳 · 在线 │ 绿/灰 dot + 名称 + 状态
│ ● 18789│ 右侧 ping/port
│ │
│ 发现的网关 │ ← Section header
│ │
│ [○] Local Gateway │ ← 未配对设备
│ 192.168.1.100:18789 │ 灰色 dot
│ │
│ + 手动添加网关 │ ← Dashed border button
│ │
├──────────────────────────────┤
│ 🔗 💬 🎤 🖥 ⚙️ │
│ 连接 聊天 语音 屏幕 设置 │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 已配对设备 | NodeRuntime.registeredDevices |
| 发现结果 | GatewayDiscovery (mDNS + 广域网) |
| 设备状态 | NodeRuntime.nodeCapabilities |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 已配对且在线 | 绿色 dot + 设备名 + "在线" |
| 已配对但离线 | 灰色 dot + "离线" |
| 发现中 | 底部 shimmer 加载 |
| 未发现任何设备 | "未发现其他网关" 空状态 |
| 已配对列表为空 | "暂无已配对的网关" + 引导去 onboarding |
---
### 3.8 通知历史页 (Notifications) — P2
#### 页面用途
展示设备上被转发到网关的通知列表。用户可查看转发历史、开启/关闭转发。
#### 用户场景
- 查看手机上的通知历史
- 确认通知转发是否正常工作
- 关闭某个应用的通知转发
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ [←] 通知 │ ← TopAppBar 带返回
├──────────────────────────────┤
│ │
│ 通知转发 [🔘] │ ← 全局开关
│ │
│ ──── 微信 ──────────────── │
│ 收到了一条消息 │ ← 通知卡片
│ 刚刚 │ 12sp + 11sp time
│ │
│ ──── Slack ─────────────── │
│ #general: 新消息 │
│ 2分钟前 │
│ │
│ ──── 系统 ──────────────── │
│ 电池电量低 │
│ 10分钟前 │
│ │
└──────────────────────────────┘
```
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| 通知列表 | DeviceNotificationListenerService 缓存 |
| 转发开关 | SecurePrefs |
| 应用列表 | NotificationListenerService.getActiveNotifications() |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 转发开启 + 有通知 | 正常列表展示 |
| 转发关闭 | 列表清空 + "通知转发已关闭" 提示 |
| 无通知历史 | 空状态 "暂无通知记录" |
| 通知服务未授权 | 引导跳转系统设置授权 |
---
### 3.9 连接诊断页 (Diagnostics) — P2
#### 页面用途
网络连接诊断工具。展示延迟、TLS、WebSocket 详细状态。
#### 用户场景
- 连接不稳定时排查问题
- 向运维提供诊断信息
#### 核心组件清单
```
┌──────────────────────────────┐
│ StatusBar │
│ [时间] [📶 🔋] │
├──────────────────────────────┤
│ [←] 诊断 │ ← TopAppBar 带返回
├──────────────────────────────┤
│ │
│ ┌── 连接不稳定 ──────────┐ │
│ │ [●] 延迟: 342ms │ │ ← 状态卡, 黄色警告
│ │ 重连次数: 3 │ │ dot 颜色随状态变
│ │ 信号质量: 82% │ │
│ └────────────────────────┘ │
│ │
│ [🔄 运行测试] │ ← 操作按钮
│ │
│ ┌──── ping gateway... ────┐ │
│ │ 64 bytes: 342ms │ │ ← 日志输出
│ │ TLS handshake... │ │ 等宽字体 10sp
│ │ OK (TOFU) │ │ 仿真终端样式
│ │ WebSocket upgrade... │ │
│ │ OK │
│ └────────────────────────┘ │
│ │
└──────────────────────────────┘
#### 需要 API 获取的数据
| 数据 | 来源 |
|------|------|
| Ping 延迟 | GatewaySession.ping() |
| TLS 状态 | GatewayTls 指纹校验结果 |
| WebSocket 状态 | GatewaySession 连接状态 |
| 重连次数 | GatewaySession.reconnectAttempts |
| 信号质量 | 综合评分 (ping+重连+rtt) |
#### 状态设计
| 状态 | UI 表现 |
|------|---------|
| 连接正常 | 绿色状态卡 + "连接正常" |
| 连接不稳定 | 黄色状态卡 + "连接不稳定" + 延迟/重连次数 |
| 连接断开 | 红色状态卡 + "已断开" |
| 测试运行中 | 日志区域动态追加输出行 + "正在测试…" |
| 测试完成 | 日志区域显示完整结果 |
---
## 4. 推荐第三方库
### 4.1 图片加载
| 库 | 推荐 | 理由 |
|----|------|------|
| **Coil** (v2.x) | ⭐ 首选 | Kotlin 原生 + Coroutines + Compose 集成 (`AsyncImage`)、包体小 (~1500 methods vs Glide ~3000) |
| Glide | 备选 | Compose 支持需额外 `landscapist-glide`,国内有更成熟的中文社区 |
**建议:** Coil。Tergent 不是高频图片应用,Coil 的轻量和 Kotlin 优雅更重要。
```kotlin
// Coil 用法
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = null,
modifier = Modifier.clip(CircleShape)
)
```
### 4.2 网络请求
| 库 | 推荐 | 理由 |
|----|------|------|
| **OkHttp** (v4.x) | ✅ 已有 | 项目已在用 |
| Retrofit | 可选,但非必需 | Tergent 不直接调用 REST API,通过 WebSocket + JSON RPC 通信 |
| **kotlinx.serialization** | ✅ 已有 | 已集成,用于 Gateway 协议序列化 |
### 4.3 状态管理
| 方案 | 推荐 | 理由 |
|------|------|------|
| **StateFlow + ViewModel** | ✅ 已有 | Jetpack 官方方案, 与 Compose `collectAsStateWithLifecycle` 结合 |
| **Compose Navigation** | ✅ 推荐 | 使用 `NavHost` + `NavController` 管理页面路由 |
| Hilt / Koin | P2 考虑 | 初期可以不引入,手动 DI 就够了 |
### 4.4 国内服务 SDK(可选)
| 功能 | 推荐 SDK | P | 理由 |
|------|---------|---|------|
| 推送通知 | 小米推送 / 华为推送 / 统一推送联盟 | P2 | 国内 Android 无 FCM,需厂商通道保活 |
| 扫码 | ZXing + CameraX | P0 | 二维码扫描,常见成熟方案 |
| 登录 | 微信登录 SDK / 飞书登录 SDK | P2 | 如果未来需要用户系统 |
| 地图 | 高德地图 SDK / 百度地图 SDK | P2 | 如果未来需要位置展示 |
| WebRTC | Google WebRTC | P2 | 如果未来需要 P2P 语音/视频 |
| Bug 上报 | Bugly (腾讯) / Sentry | P1 | 国内推荐 Bugly,免费且稳定 |
### 4.5 其他实用库
| 库 | 用途 | P |
|----|------|---|
| `androidx.compose.material3:material3` | Material3 组件 (核心依赖) | P0 |
| `androidx.navigation:navigation-compose` | Compose 导航 | P0 |
| `com.google.accompanist:accompanist-systemuicontroller` | 状态栏/导航栏沉浸式 | P0 |
| `io.coil-kt:coil-compose` | 图片加载 | P1 |
| `com.google.mlkit:barcode-scanning` | QR 扫码 (ML Kit) | P0 |
| `com.tencent.bugly:crashreport` | Bugly 崩溃上报 | P1 |
| `org.commonmark:commonmark` | Markdown 渲染 (已有) | P0 |
---
## 5. 改造优先级细化
### 5.1 优先级定义
| 级别 | 含义 | 交付标准 |
|------|------|---------|
| **P0** | 必须完成,否则不可用 | 完整的 UI + 功能 + 错误处理 |
| **P1** | 核心功能,应尽早交付 | 可用但不一定完整 |
| **P2** | 体验优化,锦上添花 | 可推迟到后续版本 |
### 5.2 页面工时估算
| 页面 | P | 预估工时 | 可复用 OpenClaw 组件 | 说明 |
|------|---|---------|---------------------|------|
| Onboarding | P0 | 2 天 | `GatewayDiscovery`, `GatewaySession.connect()` | 全新 UI,扫码逻辑可复用后端,UI 重写 |
| Connect | P0 | 2 天 | `NodeRuntime.connectionState`, `GatewayEndpoint` | 全新 UI,数据绑定改到新 VM |
| Chat | P0 | 3 天 | `ChatController`, `ChatModels`, `ChatMarkdown` | 核心消息 UI 重写,输入框/气泡/流式效果全部重做 |
| Settings | P0 | 2 天 | `SecurePrefs`, `PermissionRequester` | 列表式 UI,开关联动,各子页面路由 |
| Voice | P1 | 2 天 | `MicCaptureManager`, `TalkModeManager`, `TalkSpeakClient` | Orb + 声波动画,UI 非复杂,数据绑定为主 |
| Canvas | P1 | 1 天 | `CanvasController`, `A2UIHandler`, `CanvasActionTrust` | WebView 包装,UI 变动小,主要为品牌替换 |
| Devices | P1 | 1.5 天 | `GatewayDiscovery`, `ConnectionManager` | 列表 UI + 发现功能,可复用后端逻辑 |
| Notifications | P2 | 1 天 | `DeviceNotificationListenerService` | 简单列表 UI,数据来自 service 缓存 |
| Diagnostics | P2 | 1 天 | `GatewayTls`, `GatewaySession` | 调试工具,终端风格 log 输出 |
| **总计** | — | **15.5 天** | — | 不含第 0 阶段改名和架构重构 |
### 5.3 阶段工时分配
| 阶段 | 内容 | 工时 |
|------|------|------|
| 阶段 0: 基础设施改名 | 包名/品牌/服务类型/资源替换 | 1-2 天 |
| 阶段 1: 架构重构 | NodeRuntime 拆分、ViewModel 重写 | 3-5 天 |
| 阶段 2: UI 重制 (P0) | Onboarding + Connect + Chat + Settings | 9 天 |
| 阶段 3: 功能完善 (P1) | Voice + Canvas + Devices | 4.5 天 |
| 阶段 4: 打磨 (P2) | Notifications + Diagnostics + 动画/深色 | 3 天 |
| **Total** | — | **~20.5 天** |
### 5.4 可复用的 OpenClaw 组件清单
**✅ 直接可用(仅改包名):**
| 组件 | 文件 | 说明 |
|------|------|------|
| GatewaySession | `gateway/GatewaySession.kt` | WebSocket 会话,协议不变 |
| InvokeDispatcher | `node/InvokeDispatcher.kt` | 命令路由逻辑不变 |
| InvokeCommandRegistry | `node/InvokeCommandRegistry.kt` | 命令注册表不变 |
| 所有 Handler (×12) | `node/*Handler.kt` | 硬件/系统功能代码无品牌关联 |
| ChatController | `chat/ChatController.kt` | 聊天数据模型和控制逻辑不变 |
| CanvasController | `node/CanvasController.kt` | WebView 抽象无品牌关联 |
| A2UIHandler | `node/A2UIHandler.kt` | A2UI 协议处理不变 |
| CameraCaptureManager | `node/CameraCaptureManager.kt` | CameraX 纯硬件操作 |
| LocationCaptureManager | `node/LocationCaptureManager.kt` | 定位纯硬件操作 |
| MicCaptureManager | `voice/MicCaptureManager.kt` | 麦克风纯硬件操作 |
| SecurePrefs | `SecurePrefs.kt` | 加密存储抽象 |
| PermissionRequester | `PermissionRequester.kt` | 权限请求框架 |
| GatewayTls | `gateway/GatewayTls.kt` | TLS 指纹校验逻辑不变 |
| DeviceIdentityStore | `gateway/DeviceIdentityStore.kt` | 密钥管理不变 |
**🔄 需要修改后复用:**
| 组件 | 文件 | 修改内容 |
|------|------|---------|
| GatewayDiscovery | `gateway/GatewayDiscovery.kt` | 服务类型 `_openclaw-gw``_tergent-gw` |
| ConnectionManager | `node/ConnectionManager.kt` | UserAgent 字符串替换 |
| DeviceNotificationListenerService | `node/DeviceNotificationListenerService.kt` | Brand name 替换 |
| TalkSpeakClient | `voice/TalkSpeakClient.kt` | 改包名即可 |
**❌ 重写或删除:**
| 组件 | 处理 |
|------|------|
| `NodeRuntime.kt` | 拆分为 `TergentRuntime` + `SessionManager` + `RefreshCoordinator` + `VoiceCoordinator` |
| `MainViewModel.kt` | 重写为 `TergentViewModel` |
| `NodeApp.kt` | 重写为 `TergentApp` |
| `NodeForegroundService.kt` | 重写为 `TergentForegroundService` |
| 所有 `ui/*Screen.kt` | 全新 UI |
| `ui/design/*` | 全新主题系统 |
| `canvasScaffold.html` | 替换品牌标识 |
| `ShellScreen.kt` | 删除 |
| `DreamingSettingsScreen.kt` | 删除/推迟 |
| `TalkOrbOverlay.kt` | 删除 |
---
## 6. 附录
### 6.1 主题代码参考 (Compose)
```kotlin
// TergentTheme.kt
private val TergentLightColors = lightColorScheme(
primary = Color(0xFF1C6BFF),
onPrimary = Color.White,
primaryContainer = Color(0xFFEDF2FF),
onPrimaryContainer = Color(0xFF001B3D),
secondary = Color(0xFF06D6A0),
onSecondary = Color.White,
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF111827),
surface = Color(0xFFF8FAFC),
onSurface = Color(0xFF111827),
surfaceVariant = Color(0xFFF3F4F6),
onSurfaceVariant = Color(0xFF6B7280),
outline = Color(0xFFE5E7EB),
error = Color(0xFFEF4444),
onError = Color.White,
)
private val TergentDarkColors = darkColorScheme(
primary = Color(0xFF5B8CFF),
onPrimary = Color(0xFF001B3D),
primaryContainer = Color(0xFF1A2744),
onPrimaryContainer = Color(0xFFD6E3FF),
secondary = Color(0xFF3AE0B5),
onSecondary = Color(0xFF003827),
background = Color(0xFF111827),
onBackground = Color(0xFFF9FAFB),
surface = Color(0xFF1F2937),
onSurface = Color(0xFFF9FAFB),
surfaceVariant = Color(0xFF2D3748),
onSurfaceVariant = Color(0xFF9CA3AF),
outline = Color(0xFF374151),
error = Color(0xFFF87171),
onError = Color(0xFF601410),
)
@Composable
fun TergentTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) TergentDarkColors else TergentLightColors
MaterialTheme(
colorScheme = colorScheme,
typography = TergentTypography,
shapes = TergentShapes,
content = content
)
}
```
### 6.2 字体配置
```kotlin
// Typography
val TergentTypography = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = 36.sp,
fontFamily = FontFamily(
Font(R.font.pingfang_sc),
Font(R.font.microsoft_yahei),
Font.SansSerif
)
),
titleLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
lineHeight = 26.sp,
// same fontFamily
),
titleMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 22.sp,
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 21.sp,
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 18.sp,
),
labelSmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 14.sp,
)
)
```
### 6.3 圆角 Shapes
```kotlin
val TergentShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp),
)
```
### 6.4 颜色命名对照
| 设计 Token | Compose Color |
|-----------|--------------|
| `tergent_primary` | `MaterialTheme.colorScheme.primary` |
| `tergent_primary_bg` | `MaterialTheme.colorScheme.primaryContainer` |
| `tergent_accent` | `MaterialTheme.colorScheme.secondary` |
| `tergent_bg` | `MaterialTheme.colorScheme.background` |
| `tergent_surface` | `MaterialTheme.colorScheme.surface` |
| `tergent_border` | `MaterialTheme.colorScheme.outline` |
| `tergent_text_primary` | `MaterialTheme.colorScheme.onBackground` |
| `tergent_text_secondary` | `MaterialTheme.colorScheme.onSurfaceVariant` |
| `tergent_danger` | `MaterialTheme.colorScheme.error` |
### 6.5 文件结构建议 (ui/ 目录)
```
ui/
├── TergentTheme.kt # 主题: 颜色 + 字体 + Shapes
├── RootScreen.kt # 根路由 (Onboarding vs MainTabs)
├── OnboardingFlow.kt # 引导流程
├── MainTabs.kt # 底部 Tab 导航
├── screens/
│ ├── ConnectScreen.kt # 连接页
│ ├── ChatScreen.kt # 聊天页
│ ├── VoiceScreen.kt # 语音页
│ ├── CanvasScreen.kt # 画布页
│ ├── SettingsScreen.kt # 设置页 (主列表)
│ ├── DevicesScreen.kt # 节点管理
│ ├── NotificationsScreen.kt # 通知历史
│ └── DiagnosticsScreen.kt # 连接诊断
├── components/
│ ├── StatusBar.kt # 连接状态指示器
│ ├── ConnectionCard.kt # 连接信息卡片
│ ├── VoiceOrb.kt # 语音按钮 Orb
│ ├── VoiceWave.kt # 声波动画
│ ├── ChatBubble.kt # 聊天气泡
│ ├── ChatInput.kt # 输入框
│ ├── SettingsRow.kt # 设置行封装
│ └── EmptyState.kt # 空状态占位
├── navigation/
│ └── NavGraph.kt # NavHost 路由定义
└── viewmodels/
├── TergentViewModel.kt # 主 VM
├── ChatViewModel.kt # 聊天 VM
└── SettingsViewModel.kt # 设置 VM
```
---
*本文档由茂之核编写,茂之钳审阅。版本 v1.0,2026-05-23。*