# 跨端录音状态同步：需求与接口交接

更新：2026-09-19。本文是供后端、手机端、手机 Web 和桌面端共同对接的需求与接口草案，不代表接口已经实现或上线。

## 一、核心需求：任何一端开始录音，其他端都能及时知道

同一账号在任意一台设备上开启录音后，其他已登录的端无需手动刷新，就能看到：

1. **是否正在录音**，而不是仅知道“录音开关已打开”或“有文件正在上传”。
2. **哪台设备、哪个端正在录音**，例如“我的 iPhone · 手机端”或“MacBook · 桌面端”；多台设备同时录音时分别展示。
3. **这次已经录了多久**，例如 `12:36`，显示期间持续走时。
4. 录音停止、暂停或中断后，其他端同步更新，不能继续显示为正在录音。

典型场景：在手机开始全天候录音 → 电脑第一栏出现录音中的呼吸提示 → 查看可见“我的 iPhone · 全天候录音中 · 12:36”；手机停止采集 → 电脑及时取消该设备的录音中提示。电脑开始录音、手机查看，同样成立。

### 范围与显示口径

| 场景 | 要求 |
| --- | --- |
| 手机手动长录音 | 真实采集开始后同步，不等停止、不等音频上传 |
| 手机 Web 入口的全天候录音 | 必须覆盖；实际由手机原生运行时采集，由它上报，不把 Web 页面和手机算成两台录音设备 |
| 桌面直接录音、实际具备采集能力的 Web 端 | 使用同一套后端合同上报和接收状态 |
| 另一端新打开、重新登录或恢复前台 | 立即查询当前状态，不能只等下一次开始事件 |
| 页面切换、手机关闭录音控制网页 | 只要实际采集还在继续，录音状态就应继续；不能依赖某个页面一直打开 |
| 同账号多设备、多窗口或不同 Web 端口 | 都从同一账号的云端状态获取信息；没有采集的窗口只消费状态，不重复上报 |
| 静音录音 | 麦克风正在采集即属于录音，与是否检测到人声、是否完成转写无关 |

“立即”的建议验收目标：正常联网且接收端在线可运行时，从实际开始/停止到其他端更新 **不超过 2 秒**。这是待各端共同确认并实测的目标，不是现有能力承诺；不能只靠十几秒一次的轮询实现。

断网、系统挂起或进程被终止时无法承诺即时同步，必须明确显示“状态未同步/状态待确认”，不能把失联直接当作停止，也不能永远保留确定正在录音的提示。

本需求只同步录音状态元信息，不要求实时上传声音，不调整现有音频保存、上传或转写流程，也不包含远程启停、远程接管或全账号录音互斥。

## 二、当前缺什么：后端需要提供统一的录音状态接口与通知

**现在桌面知道本窗口是否录音，但缺少供所有端共同上报、查询和订阅的完整后端合同。** 其他端不能仅靠本地红点、手机 Web 桥接或已上传音频列表知道谁正在录音。

| 必需能力 | 当前缺口 | 后端需交付的内容 |
| --- | --- | --- |
| 开始录音登记 | 缺少独立于音频上传会话的录音实例登记 | 登记账号下的采集设备、端类型、录音模式、开始时间与实例标识 |
| 持续录音与状态更新 | 现有短期保活没有完整的设备、状态和时长合同 | 心跳续期、累计采集时长更新、暂停/中断/恢复上报及失联判断 |
| 停止录音 | 缺少明确的结束与其他端清除链路 | 停止登记、最终时长、幂等重试，拒绝迟到消息重新激活已结束录音 |
| 当前录音列表查询 | 缺少同账号所有录音设备的统一快照 | 返回各设备状态、已录时长、更新时间、有效期与服务端时间 |
| 实时变更通知 | 缺少已确认的跨端开始/停止/中断/失联通知合同 | 按账号推送状态变化，支持断线后的状态校准 |

已核验的 `/api/v1/audio/active-session` 只接收 `session_id`，按账号与 session 写入有效期 60 秒、值为 `1` 的缓存。它没有上述设备列表、时长、结束和实时通知合同，**不能直接作为完整实现交给各端接入**。可以由后端评估扩展复用，但必须补齐表中能力。

`get-session-ls`、`one-day-trans` 等已有接口服务于音频历史与转写，不能用“最近有上传”推断“当前还在录音”。录音时间轴的历史覆盖范围是另一类需求，也不能替代实时状态。

以上缺口依据目前已核验的源码，非生产接口全量审计；如已有未纳入核验的服务，请后端提供合同后对照补齐。

## 三、建议接口合同：各端按同一份定义实现

以下路由和事件名称均为**建议名称，待后端确认，不是当前可调用接口**。后端可以合并写接口或复用已有实时通道，但需要完整保留这些能力，并交付正式请求、响应、错误码与通知定义。

### 3.1 接口清单

统一建议前缀：`/api/v1/audio/recording-presence`；所有接口必须鉴权。

| 建议接口/事件 | 何时调用 | 关键输入与结果 |
| --- | --- | --- |
| `POST /start` | 麦克风实际开始采集成功 | 上报录音 ID、采集设备/运行实例、端类型、模式、真实开始时间、累计时长、递增序号；返回服务端确认状态与有效期 |
| `POST /update` | 采集期间心跳，以及暂停、中断、恢复等变化时立即调用 | 上报录音 ID、序号、真实状态、累计采集时长；续期并返回确认状态 |
| `POST /stop` | 实际停止采集后立即调用，不等保存/上传完成 | 上报录音 ID、序号、最终采集时长、结束原因；确认结束并通知其他端 |
| `POST /list` | 登录、首次打开、恢复前台、重连或状态校准 | 不传任意用户 ID；查询当前登录账号的设备录音列表、服务端时间及快照版本 |
| `recording.presence.changed` | 开始、停止、状态变化或服务端判定失联 | 服务端主动通知该账号的其他端；携带变更条目、账号级版本及服务端时间 |

写接口由真正采集音频的运行时调用；查询和订阅由所有需要展示状态的端接入。仅提供 HTTP 写接口而没有查询和通知，不能完成本需求。

### 3.2 统一数据字段

| 字段 | 含义与约束 |
| --- | --- |
| `recording_id` | 客户端生成的单次录音唯一 ID；重试、暂停恢复和全天候自动切片不更换，明确停止后重新录音才更换；不依赖最终音频 session ID |
| `device_id` / `device_name` | 采集设备标识及可读名称，用来区分两台手机或两台电脑；设备归属由服务端校验，名称仅用于展示 |
| `runtime_id` | 本次采集运行实例标识，用于区分同设备多个运行环境；观看状态的页面不能冒充另一个采集实例 |
| `client_type` / `platform` | 如 `mobile`、`desktop`、`web` 与 `ios`、`android`、`macos`、`windows`；手机内嵌 Web 控制的录音仍归属手机 |
| `recording_mode` | `manual` 或 `all_day`，展示直接录音/全天候录音的区别 |
| `state` | `recording`、`paused`、`interrupted`、`error`、`stopped`；表示真实采集阶段，不是用户开关或上传阶段；不要求没有暂停能力的端新增暂停功能 |
| `elapsed_ms` | 采集运行时累计的有效录音时长；静音采集计入，暂停、中断、等待上传不计入 |
| `started_at` | 本次实际开始时间，UTC 毫秒；不是本次切片开始时间，也不能单独用于计算累计有效时长 |
| `seq` | 单次录音内递增的上报序号，重试沿用原序号，避免乱序覆盖新状态 |
| `revision` | 服务端按账号、环境维护的状态版本，用于快照与事件校准；与客户端 `seq` 区分 |
| `last_confirmed_at` / `expires_at` | 服务端最后确认时间与本条状态有效期，UTC 毫秒；由服务端计算，不由客户端任意延长 |
| `duration_anchor_at` | 服务端时间坐标上的时长参考点，与响应中的 `elapsed_ms` 配对，供其他端平滑走时 |
| `freshness` | `fresh` 或 `stale`；区分“确认在录音”和“上次在录音但现在失联”，不能把 stale 自动改成 stopped |
| `server_now` | 响应/事件生成时的服务端时间，UTC 毫秒，用于接收端校准本机时钟偏差 |

账号从认证上下文取得，禁止信任任意 `user_id`。设备身份必须与该登录会话绑定；若现有认证不能区分采集设备，后端需一并补充设备登记/绑定约定，不能只信任前端填写的设备 ID。只有原采集设备/实例可以修改自己的录音记录。

`list` 返回当前未结束的录音，包括仍保留的失联条目；成功且为空才表示当前没有可展示条目。服务异常不能伪装成空列表。停止事件需带已结束条目，让接收端移除活动提示。失联条目的保留时限需写入正式合同。

### 3.3 时长、心跳与事件规则

- 开始、停止、暂停、中断和恢复立即上报并推送，不能等下一个心跳。建议初始心跳间隔 10 秒、有效期 30 秒，最终以手机后台及桌面休眠实测共同确定；这两个间隔不是开始/停止通知的延迟。
- 服务端确认时把累计采集时长锚定到服务端时间，并返回参考点。其他端仅在 `recording + fresh` 时随时间递增，收到新样本后校正；暂停、中断或有效期届满即停止外推。
- 时长以采集运行时的单调计时累计值为准，不用“当前时间－开关开启时间”。网络传输会产生小幅展示误差，心跳用于校准；设备墙上时钟不准不能导致负数或异常跳时。
- 服务端负责续期和失效判定，并为过期状态提供查询结果与通知。接收端同时检查有效期，即使通知断开也不能无限显示“确认正在录音”。
- 开始和结束重试必须幂等；过时序号不得覆盖新状态。结束是终态，结束先到、开始后到或迟到心跳均不能复活同一录音 ID。
- 断网时本地继续录音并显示“状态未同步”；恢复后上报当前真实状态，不逐条重放过时的“正在录音”心跳。离线期间已结束的录音补报结束。
- 首次进入和重连采用“订阅 + 带版本快照/补偿”的一致性流程，避免查询与订阅之间漏事件。实时通道可复用现有账号通道；若不支持事件重放，发现版本缺口应重新查询快照。
- 实时通道不可用时可有界轮询兜底，但必须标明同步延迟，不能把降级轮询当作已经满足 2 秒目标。

## 四、各端分别需要做什么

```text
手机手动录音 ──────┐
手机 Web → 原生采集 ├─ 开始 / 更新 / 停止 → 后端账号录音状态
桌面或 Web 实际采集 ┘                        │
                                   列表查询 + 实时通知
                                            ↓
                              手机 / 桌面 / Web 统一展示
                              哪台设备 · 录音状态 · 已录多久
```

| 负责方 | 交付内容 |
| --- | --- |
| 后端 | 提供写入、查询、通知合同与可联调环境；实现账号/设备鉴权、版本与幂等、有效期、失联通知及服务端时间；明确错误码和重连同步方式 |
| 手机原生/Flutter | 手动长录音、全天候录音都从真实采集生命周期上报；后台录音时保持可靠状态续期，停止采集即报结束；同时接入其他设备状态的查询和展示 |
| 手机 Web | 全天候控制页继续通过原生桥接控制采集；不重复登记录音，不拿 `enabled=true` 当正在采集；涉及跨设备展示时消费同一后端合同，原生续期不能依赖 WebView 存活 |
| 桌面插件/Web | 本机采集状态即时展示并上报；订阅同账号其他设备状态，在全局录音入口显示活动提示，可查看设备、模式、时长；新窗口/新端口同样查询云端状态 |

共同规则：本机采集事实不等后端成功才显示；自己的本机状态与云端回传按 `recording_id` 去重；多个录音实例分别展示。账号退出、切换或环境变化后取消旧订阅并清空旧账号状态，不提供跨设备远程停止按钮。

手机全天候录音的分片、封段、合并、上传与下一段采集可能并行。**自动切片不等于用户停止录音，上传中也不等于正在录音。** 全程 ID、计时和续期应由手机采集运行时维护，不由音频上传队列或控制网页维护。

## 五、联调验收与交付顺序

| 验收场景 | 通过标准 |
| --- | --- |
| 手机开始/停止，桌面已在线；反向再测 | 对端按约定时延更新，设备、端类型和时长正确；不等上传完成 |
| 手机 Web 开启全天候录音 | 桌面显示手机这一条录音；关闭 WebView、切片或封段不产生重复记录或错误停止 |
| 录音中另一个端登录/打开 | 快照立即给出已有设备状态及累计时长，不从零计时 |
| 两台设备同时录音，再停止一台 | 各自独立展示；另一台不被误清除 |
| 整段没有人声 | 仍显示录音中且时长增长，与转写结果无关 |
| 暂停、中断、恢复、权限拒绝或启动失败 | 与实际采集一致；非采集状态不显示录音呼吸，时长不误增 |
| 断网、系统挂起、进程退出或服务不可用 | 未获新确认时转为待确认；不得假报已停止或持续无限增长；本地音频保存不受影响 |
| 结束后到达旧心跳、重复请求、设备时间不准 | 不复活已结束录音、不重复登记，时长无异常跳变 |
| 账号切换或越权请求 | 不能查看或修改其他账号状态；不能由其他设备冒充原采集者写入 |

建议交付顺序：**先确认统一接口合同 → 后端提供可联调环境 → 手机（含全天候）与桌面按合同接入 → 双向、多设备及断网场景验收。** 不能只做桌面红点后将整个需求视为完成。

后端请优先反馈：上述能力是否复用已有接口；最终路由、字段、错误码与实时事件格式；设备身份绑定方式；心跳/失效/失联保留规则；是否可满足建议时延及联调环境。端侧据此实施，不自行猜测生产接口。

## 附录：现有实现与核验依据

以下是截至 2026-09-19 核验的源码情况，不代表手机安装包或生产部署已经具备相应能力。

- 后端 `jotmo-audio`，`origin/master: 5d2a14b`：`gin/api/audio.go` 中 `audioActiveSessionHandler` 仅写 60 秒 session/user 缓存；现有历史音频/转写接口不构成完整的跨端录音状态合同。
- 手机 `jotmo-frontend`，`origin/master: 32734114d`：`AudioLongRecordingManager` 先本地录音，远端音频 session 延后到停止后创建；因此录音状态 ID 必须独立于云端音频 session。
- 同版手机 `AudioAllDayRecordingRuntime` 负责全天候真实采集、约 2 分钟分片与最长 1 小时逻辑段轮换；桥接快照包括 `enabled`、`state`、`startedAt`、`elapsedMs` 等。iOS Live Activity 本机心跳不是云端状态心跳。
- 手机 Web `jotmo-frontend-web`，`origin/master: a53807c5`：`apps/jotmo-web-pages/src/app/app/audio/long-recording` 通过 `app.audio.allDayRecording.snapshot` / `setEnabled` 控制原生；收到 `web.audio.allDayRecording.state.changed` 后重新查询本机快照。页面目前有以 `enabled` 判断 recording 的路径，不能照搬为可信的跨端采集事实。
- 桌面 `DirectRecordingStore` 当前提供本窗口的真实录音状态、本地保存与停止后上传；第一栏提示尚不等于跨设备同步。Web Locks 只提供同源窗口约束，不承担跨设备状态分发。桌面“全天候录音”原入口是手机操作引导，不是远程采集开关。

本次仅重写交接文档；手机、后端与跨端状态接入仍待同事按确认后的合同实现，不修改已安装的 Arkme 客户端。
