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

45 KiB
Raw Permalink Blame History

Tergent Android — UI 规范文档 v1.0

版本: 1.0 | 状态: 草稿 | 作者: 茂之核
基于: TERGENT-DESIGN.md + 界面布局参考 (tergent-ui-screens.html)
目标: Pixso 高保真设计输入 + 开发实现手册


目录

  1. 设计语言
  2. 页面流程
  3. 9 个页面详细设计
  4. 推荐第三方库
  5. 改造优先级细化
  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)

// 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。