调试服务器接口与 gomobile 集成前的准备工作

December 14, 2025
5 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

背景

  • 目标:未来通过 gomobile / FFI 把 Go 核心嵌入到移动端/桌面应用中,减少“先启动核心进程,再连接 HTTP 服务”的操作成本。
  • 短中期现实:UI 仍然通过 HTTP 调用本地/局域网的 Go 核心。
  • 痛点:
    • 客户端很难“快速判断”当前核心和 S3 的状态:是否在线、桶是否可用、当前桶里有没有已经初始化的加密库。
    • 初次启动时,我这边看到的是一片空白,很难知道“下一步应该是初始化密码、解锁现有库还是处理冲突”。
    • 逻辑分散在多处,特别是 Home 页面的启动流程、离线处理、自动解锁逻辑,之前比较绕。

本轮改动的目标:

  1. 在服务端增加/梳理“调试友好”的接口:/health/bootstrap 等。
  2. 在客户端围绕这些接口,重写启动与引导的复杂逻辑,使得:
    • 无论将来是 HTTP 还是 gomobile,核心能力的语义是一致的。
    • 逻辑集中到少数几个函数里,更容易在切换实现时整体替换。

服务端:调试友好的状态接口

1. /health:快速健康检查

  • 定义位置:core/internal/api/server.go:114-120,171-220
  • 路由注册:
    • setupRoutes 中作为无需认证的公共接口:
      • s.router.GET("/health", s.healthCheck)
  • 行为:
    • 为当前请求上下文包装 5 秒超时:
      • ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
    • 调用 s.s3.Ping(ctx) 来检测 S3 连通性:
      • Ping 底层实现是 ListBuckets
        • 位置:core/internal/storage/s3.go:130-159
    • 若 S3 检查失败:
      • 返回 503 ServiceUnavailable
      • body:{"status":"error","error":"...详细错误..."}
    • 若成功:
      • 返回 200 OK
      • body:{"status":"ok","timestamp":<当前秒级时间戳>}

设计意图:

  • 提供一个极轻量的“核心是否在线且能访问 S3”的探针。
  • 为客户端的以下场景服务:
    • 应用启动时的初始在线/离线判定。
    • 后台定时心跳,随时更新 UI 的“离线”提示。
    • 将来 gomobile 嵌入时,即便不再通过 HTTP,也可以用同样的语义:healthCheck() 表示“核心处于可工作状态”。

2. /bootstrap:引导初始化/解锁流程

  • 定义位置:core/internal/api/server.go:114-120,171-201
  • 路由注册:
    • 同样是无需认证接口:
      • s.router.GET("/bootstrap", s.bootstrapStatus)
  • 返回结构:
    • BootstrapStatusResponsecore/internal/api/server.go:30-37
      • Status string "json:\"status\""
      • Bucket string "json:\"bucket\""
      • ObjectCount int "json:\"objectCount\""
  • 内部逻辑:
    1. 列出当前桶根目录下的所有对象:
      • objects, err := s.s3.ListObjects(ctx, "")
    2. 统计对象数量:
      • count := len(objects)
    3. 填充响应体基础字段:
      • Bucket: s.s3.Bucket()
      • ObjectCount: count
    4. 根据状态分三类:
      • empty
        • 对象数为 0。
        • 说明当前桶是全新的,尚未初始化加密库。
      • ready
        • 对象数 > 0,且存在 .e2eepan/salt
          • s.s3.GetObjectInfo(ctx, ".e2eepan/salt") 成功。
        • 说明这个桶中已经有一个按约定初始化好的加密库,可以直接解锁。
      • conflict
        • 对象数 > 0,但没有找到 .e2eepan/salt
        • 推断:
          • 这个桶中有其他内容,但不是本应用的加密库,或者结构被破坏。
          • 需要手动决策(换桶、清空、或手动迁移)。

这个接口是整个“初次启动引导”和“调试当前桶状态”的关键,对 gomobile 也非常重要:

  • 无论核心是独立服务还是嵌入式库,只要暴露同样语义的 bootstrapStatus,UI 就能复用当前的引导逻辑。
  • 对调试来说,在浏览器或 curl 中访问 /bootstrap 就可以一眼看出:
    • 当前桶名。
    • 对象个数。
    • 是否已经初始化加密库/是否存在冲突。

3. 元数据同步调试:/api/v1/metadata/sync

  • 位置:core/internal/api/server.go:903-943
  • 原有职责:
    • 接收客户端上传的完整元数据索引(MetadataIndex),并覆盖存储。
    • 这是一个“重同步”接口。
  • 为了排查问题,增加了简单的调试输出:
    • 使用 c.GetRawData() 读取原始请求体,并打印:
      • fmt.Printf("syncMetadata received: %s\n", string(body))
    • JSON 解析失败时,也打印具体错误:
      • fmt.Printf("syncMetadata parse error: %v\n", err)
  • 虽然这算不上正式的“调试接口”,但对于开发阶段排查元数据结构错误和 gomobile 适配时验证序列化格式很有帮助。

4. 监听地址:为移动端访问准备

  • Server.Run 定义:core/internal/api/server.go:1067-1079
    • 采用:addr := fmt.Sprintf("0.0.0.0:%d", s.cfg.Server.Port)
    • 所以无论是桌面还是 Android 设备,只要在同一局域网,都可以通过 IP+端口访问。
  • 对 gomobile 而言:
    • 在“过渡期”可以仍然跑一个本地 HTTP 核心,让移动端通过 Wi-Fi 调试。
    • 长期看则可以把这部分逻辑提炼为“路由表”和“服务实现”,HTTP/Native 两个入口共同复用。

客户端:围绕调试接口重构启动与引导逻辑

1. 健康检查链路:从 HTTP 到 AppState

  • ApiClient.healthCheckclient/lib/core/api/api_client.dart:70-107
    • GET /health,使用较短的超时:
      • sendTimeout / receiveTimeout / connectionTimeout 均为 3 秒。
    • 所有异常都视为 false(离线)。
  • AppState._checkInitialNetworkStatusclient/lib/core/state/app_state.dart:196-219
    • 应用启动时调用:
      • 稍微等待 100ms,确保 ApiClient 初始化完成。
      • 调用 api.healthCheck()
      • 根据结果设置 _isOfflinenotifyListeners()
      • 若在线:立即加载元数据 _loadMetadata()
      • 若离线:尽量加载本地数据库缓存 _loadFromLocalDb()
  • AppState 构造函数中的心跳定时器,client/lib/core/state/app_state.dart:88-133
    • 每 15 秒执行一次:
      • 再次调用 api.healthCheck()
      • 若离线状态有变化,更新 _isOffline 并通知 UI。
  • 辅助接口:
    • checkConnection():显式检查一次连接并更新 _isOfflineclient/lib/core/state/app_state.dart:346-352)。
    • setOffline() / canWrite() / offlineError 等封装了离线模式下的行为。

这一整条链路把 /health 的语义映射为:

  • “核心是否处于可工作的在线状态”,并被整个 UI 统一使用。
  • 将来如果改用 gomobile,只要在新的实现里保持 healthCheck() 语义不变,AppState 和 UI 可以不改。

2. 引导逻辑重构:Home 页面的 _checkBootstrapStatus

  • 入口:HomePage.initStateclient/lib/ui/home_page.dart:49-65
    • 初始化后,在 addPostFrameCallback 中依次做:
      • appState.addListener(_onStateChanged);
      • appState.refreshFiles();(乐观加载本地文件并后台同步)。
      • _checkBootstrapStatus();(只在首次且未解锁时运行)。
  • 引导核心逻辑:_checkBootstrapStatusclient/lib/ui/home_page.dart:88-140
    • 防重入:
      • 已经检查过 (_bootstrapChecked) 或正在检查 (_bootstrapChecking) 时直接返回。
      • appState.isOffline 或已解锁,直接返回。
    • 调用服务端 /bootstrap
      • final result = await appState.api.getBootstrapStatus();
      • 对应 ApiClient.getBootstrapStatusclient/lib/core/api/api_client.dart:109-117
    • 失败处理:
      • 若调用失败或无数据:
        • 重置 _bootstrapChecking / _bootstrapChecked / _vaultStatus / _conflictBucket / _conflictCount
        • 返回,UI 退回到普通“空态”。
    • 成功时解析:
      • statusempty / ready / conflict 中的一种。
      • bucket:当前桶名。
      • objectCount:对象数。
    • 特殊优化:ready + 已保存密码时自动解锁:
      • status == 'ready'
        • 通过 appState.getSavedPassword() 获取本地保存的密码。
        • 若存在,尝试 appState.unlock(saved)
        • 成功则:
          • 清除 _vaultStatus 和冲突信息。
          • 标记 _bootstrapChecked = true,不再显示引导区域。
    • 其他情况:
      • _vaultStatus / _conflictBucket / _conflictCount 写入状态,用于后续 UI 渲染。

这段逻辑把 /bootstrap 的三种状态映射为 UI 行为:

  • ready
    • 如果密码已记住,自动尝试解锁。
    • 自动解锁失败或无密码,则展示“解锁”界面。
  • empty
    • 展示“初始化密码”界面。
  • conflict
    • 提示当前桶存在冲突(已有非本应用内容),引导处理。

3. 内联引导 UI: _buildVaultInline 及其分支

  • 入口:_buildFilesBodyclient/lib/ui/home_page.dart:615-654
    • 决定是否在空列表时展示“加密库引导区”:
      • showVaultInline = !state.isOffline && !state.isUnlocked && _vaultStatus != null;
    • files.isEmpty 且需要引导:
      • 显示 _buildVaultInline(state)
    • 否则显示普通空态或文件列表。
  • 引导区渲染:_buildVaultInlineclient/lib/ui/home_page.dart:684-705
    • _bootstrapChecking == true
      • 显示“正在检测存储状态…”圈圈。
    • _vaultStatus == 'ready'
      • 调用 _buildVaultUnlockInline()
        • 内联解锁密码输入框 + “解锁”按钮。
        • 按钮触发 _handleVaultUnlock()client/lib/ui/home_page.dart:142-164)。
    • _vaultStatus == 'empty'
      • 调用 _buildVaultInitInline()
        • 两个密码框(密码与确认)+ “初始化”按钮。
        • 按钮触发 _handleVaultInit()client/lib/ui/home_page.dart:166-201)。
    • _vaultStatus == 'conflict'
      • 调用 _buildVaultConflictInline()
        • 文案提示当前桶中已有其他内容,并显示桶名/对象数,供手动处理。
    • 其他情况:
      • 回退到 _buildEmptyPlaceholder(state)

这套 UI 逻辑本质上就是 /bootstrap 的前端投影:

  • 把底层“桶中状态”翻译成“该给操作建议”。
  • 同样的结构将来可以复用到 gomobile 模式,只要 native 实现能给出相同的状态信息。

4. 调试核心地址:隐藏的内核地址设置弹窗

  • 入口:设置页里的“关于”区域,client/lib/ui/settings_page.dart:711-775
    • 连续点击“关于 E2E Pan”5 次,触发隐藏菜单:
      • 调用 _showCoreAddressDialog()
  • main.dart 中的默认核心地址,client/lib/main.dart:12-24
    • 默认使用:http://127.0.0.1:18520
    • SharedPreferences 中存在 core_base_url 则覆盖默认。
  • _showCoreAddressDialog 实现:client/lib/ui/settings_page.dart:303-359
    • 文本框预填当前/保存的核心 URL。
    • 可以:
      • 恢复默认(删除 core_base_url)。
      • 设置新的 URL(写入 core_base_url)。
    • 修改后提示“重启应用后生效”。

这相当于是一个“调试服务器地址”的隐藏开关,有两个用途:

  • 当前阶段:
    • 方便在 PC 和手机间切换不同核心实例(本机、局域网其他机器等)。
    • 结合 /health/bootstrap,可以快速验证不同核心的状态。
  • 将来 gomobile:
    • 在完全 native 化之前,可以在测试版中维持 HTTP+gomobile 双通道,通过这个入口自由切换。
    • 也可以作为“开发模式”的配置项保留,用于回退到 HTTP 调试路径。

与 gomobile 的关系:为什么要先把这些逻辑理顺

  1. 统一“状态查询”语义

    • 现在通过 /health/bootstrap 两个接口,把“核心可用性”和“存储桶/加密库状态”抽象成稳定的语义。
    • gomobile 实现时,只要提供同样语义的函数(例如 HealthCheck() / BootstrapStatus()),UI 层不需要重新设计。
  2. 集中而非分散的复杂逻辑

    • 启动、离线处理、自动解锁、冲突提示等复杂逻辑都集中在:
      • AppState._checkInitialNetworkStatus / refreshFiles / forceRefresh
      • HomePage._checkBootstrapStatus + _buildVaultInline 相关分支。
    • 未来如果切换到 NativeCore,只需要在少数几个“获取状态”的入口替换实现,而不必全局搜一堆“零散 if/else”。
  3. 更好的调试体验

    • 即便暂时还没接入 gomobile,现在的 HTTP 模式已经非常易调试:
      • 浏览器访问 /health / /bootstrap 即可看到核心状态。
      • 通过隐藏的“内核地址”设置,可以随时切换到不同核心实例。
    • 这些调试能力在 gomobile 集成阶段也会非常重要:
      • 可以对比“HTTP 核心”和“Native 核心”的行为是否一致。
      • 便于在出现问题时快速回退到 HTTP 模式做 A/B 验证。
  4. 为 NativeCoreService 预留清晰的接口层

    • ICoreService / HttpCoreService / NativeCoreService 的分层(client/lib/core/services/*.dart)已经在架构上说明了:
      • HTTP 和 Native 只是实现差异。
      • 上层真正关心的是“健康检查、初始化、解锁、元数据、文件操作、流式播放”的领域能力。
    • 本轮围绕 /health/bootstrap 所做的规范化,为未来把这套能力迁移到 gomobile 提供了很好的锚点。

小结

  • 在服务端,我们补齐/整理了适合调试和引导的状态接口:
    • /health:快速判断核心和 S3 是否可用。
    • /bootstrap:描述当前桶与加密库状态,为 UI 引导提供依据。
    • 元数据同步时增加原始请求体的调试输出,便于排查序列化问题。
  • 在客户端,我们围绕这些接口重写了启动和引导的复杂逻辑:
    • AppState 统一负责健康检查、在线/离线判定和数据加载策略。
    • Home 页内联的“初始化/解锁/冲突”引导区完全由 /bootstrap 驱动。
    • 通过隐藏的“内核地址设置”弹窗,增强了调试不同核心实例的能力。
  • 这些工作一方面让当前 HTTP 架构的开发体验更好,另一方面也为 gomobile/NativeCore 的未来集成打好了地基:
    核心能力的语义先收敛,再谈实现的替换。