bug: 手机端 unknown presence 被当作离线,远程历史读取被静默拒绝(加载更早消息无效且无任何提示)
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 问题描述 / What happened
**实际行为**:手机端打开一个远程任务时,「加载更早消息」无效、历史只显示最近一小段;屏幕上**既没有错误提示、也没有连接横幅**,看起来像"这个任务本来就没有更早历史"。
**期望行为**:只有在被控端**明确不可用**(presence 显式 `false`)时才停止远程读取;状态**未知**时应照常发起读取(失败则走既有错误提示)。未知不等于离线。
**根因**:presence 是三态(`true` / `false` / unknown),但手机端会话页把 unknown 当成了不可用,且拒绝路径全程静默。
同一文件内已是两套相反口径:
```ts
// apps/mobile/app/sessions/[sessionId].tsx:1033
const remoteHistoryAvailable = status === 'online' && getPresenceAvailability(deviceId) === true;
// apps/mobile/app/sessions/[sessionId].tsx:1034-1036
// Unknown presence while connecting is loading, not evidence of a lost computer.
const showCachedHistoryNotice = status !== 'connecting'
&& (status !== 'online' || getPresenceAvailability(deviceId) === false);
```
`:1036` 明确注释「未知不是离线证据」(所以不弹横幅),`:1033` 却要求显式 `true`,把未知当作不可用——于是既不读取、也不提示。
拒绝链路(全部静默返回,不置 loading / error):
- `apps/mobile/src/session/remoteHistoryView.ts:12` — `view.setNetworkAvailable(networkAvailable)`,`networkAvailable` 即上面的 `remoteHistoryAvailable`
- `packages/maker-shared/src/historyViewController.ts:60` — `isActive = () => this.active && this.networkAvailable`
- `:127` — `readPage` 首行 `if (!this.isActive() || ...) return;` → 不发请求、不置 `loading`、不置 `error`
- `:117`(`invalidate`)、`:213`(`loadDetails`)同样静默返回
- `apps/mobile/app/sessions/[sessionId].tsx:3258` — `if (!remoteHistoryAvailable) { setLoading(false); return; }`
- `:4610` — `if (!remoteHistoryAvailable) return;`(点「加载更早消息」直接吞掉)
**unknown 会长期存在**,不是瞬时窗口:`apps/mobile/src/device-link/presenceRecovery.ts:300` 的 `resetPresenceAvailabilityForConnection` 在每次重连都清空 availability(全部退回 unknown);presence 是增量广播、服务端不重放全量;唯一补全手段是 `apps/mobile/src/device-link/DeviceLinkContext.tsx:900` 的 `void readDeviceList().catch(() => undefined)` —— 失败被吞、无重试、无退避。
**与全仓其他位置的约定不一致**(其余都是「默认可用,只有显式 false 才拦」):
- `packages/maker-shared/src/deviceResponsiveness.ts:65-67` — `isPresenceValueEligible` → `return available !== false;`(其注释明确「missing value means this generation has not received a presence edge yet」「must not send into an explicitly unavailable peer」,即未知允许探测)
- `apps/mobile/src/device-link/DeviceLinkContext.tsx:452 / 1323 / 1372` — transport 自家门都用 `=== false`
- `apps/desktop/src/renderer/lib/makerChatStore.ts:3917 / 8763 / 10872` — `setNetworkAvailable(!isRemoteDeviceMarkedDisconnected(deviceId))`,只有 `connectionStatus === 'disconnected'` 才拦
即:全仓只有 `[sessionId].tsx:1033` 使用 `=== true`。
## 环境 / Environment
- Cindy 版本或 commit: 被控端为 Desktop 已安装构建(sourceCommit `db19a86b`);源码核对基于 origin/main `5adbdd80d363ce907ee08c06e04c0f5c24c27ce1`(2026-09-14)
- 平台与版本: 被控端 Windows Desktop;控制端 Android 手机端(手机端具体构建号未记录)
- 安装方式: Desktop 安装包;手机端经 device-link 连接同一账号
## 复现步骤 / Steps to reproduce
1. 桌面端与被控端同账号登录,被控端开启远程控制(`remoteControlEnabled`)。
2. 手机端连接该被控端,打开一个消息较多的远程任务。
3. 向上滚动 / 点「加载更早消息」。
4. 观察:更早历史不加载;同时屏幕没有错误提示、也没有「仅显示缓存」类横幅。
5. 对照:在桌面端打开同一任务,向上滚动可正常逐页加载更早历史(同一被控端、同一会话)。
更易命中的触发条件:弱网/链路抖动下让手机端重连一次,使该设备 presence 停在 unknown(本机同一被控端当天出现 34 次 link-open / 26 次 presence-offline、4 次响应性熔断开启、32 次 DEVICE_UNRESPONSIVE、38 次 relay-error)。
## 日志与截图 / Logs & screenshots
**手机端日志(当前未取得)**:本机没有手机端日志包,因此上述结论来自代码路径复现 + 被控端链路证据,不是手机日志原文。需要手机端日志确认两点:① 该设备 presence 长期为 unknown(表现为 `remoteHistoryAvailable=false` 而 `showCachedHistoryNotice=false`,即无横幅但加载无效);② 该时刻被控端未收到任何 `local-db:messages:view`。
**被控端(Windows host)日志**:对应时刻**没有**任何 `blocked non-allowlisted channel`,也**没有** history 扫描预算报错 —— 说明不是通道/allowlist 或大消息问题。
**代码路径复现**(真实 `HistoryViewController` + `isPresenceValueEligible`):
```text
presence=true strictGate=true sharedGate=true -> ready=true items=1 error=null pageCalls=1
presence=false strictGate=false sharedGate=false -> ready=false items=0 error=null pageCalls=0
presence=null strictGate=false sharedGate=true -> ready=false items=0 error=null pageCalls=0
```
第三行为本缺陷:`error=null`、`loading=false`、`items=0`、**未发出任何请求**,调用方无法与「本就没有更早历史」区分。
**被控端读取健康性对照**(用于排除数据侧):用被控端真实读取实现 `createHistoryViewReader` 连真实库跑该会话,13 轮上翻、246 个 item 全部返回、单页 0.008–0.142 MiB(帧上限 2 MiB)、无 `UNSUPPORTED_CAPABILITY`。
## 修复建议
1. `[sessionId].tsx:1033` 改用共享三态判据(`isPresenceValueEligible`),与 `:1036` 横幅口径、transport、桌面端一致;真离线时 transport 仍会以 `DEVICE_OFFLINE` 快速失败,不会变成盲发。
2. `:3258` 区分「显式不可用」与「未知」,未知照常尝试读取。
3. 静默路径补可见状态,避免「加载更早消息」无声失效。
4. `DeviceLinkContext.tsx:900` 的 roster 补读失败后加退避重试,或在重连后对该设备做一次具名探测以定论。
5. 测试:`packages/maker-shared/src/__tests__/historyViewOffline.test.ts` 目前只覆盖 `false`/`true`,补一条 unknown ≠ false 的断言;`apps/mobile/src/__tests__/sessionMainLayerDesktopFirst.test.ts:108` 只断言「用到了 `getPresenceAvailability(deviceId)`」、未断言比较语义,因此锁不住本缺陷。
Contributor guide
Research direction
Start with apps/mobile/app/sessions/[sessionId].tsx and trace remoteHistoryAvailable into apps/mobile/src/session/remoteHistoryView.ts and packages/maker-shared/src/historyViewController.ts. Run packages/maker-shared/src/__tests__/historyViewOffline.test.ts and apps/mobile/src/__tests__/sessionMainLayerDesktopFirst.test.ts, then verify unknown presence is not treated as explicit offline and the relevant loading path no longer fails silently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100