45 KiB
45 KiB
Tergent Android — UI 规范文档 v1.0
版本: 1.0 | 状态: 草稿 | 作者: 茂之核
基于: TERGENT-DESIGN.md + 界面布局参考 (tergent-ui-screens.html)
目标: Pixso 高保真设计输入 + 开发实现手册
目录
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 |
排序逻辑:
- 连接状态是首要信息 → 放最左边,一眼能看到连接状况
- 聊天是核心使用场景 → 紧随其后,用户主要交互在此
- 语音和屏幕是特色功能 → 中间位置,容易触及
- 设置靠右提供访问入口 → 最右侧,符合 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)
// 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 字体配置
// 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
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。