AskaEth
|
53c3c5c8ee
|
docs(05): 锚点补记三十九 —— 后端回执二十五/二十六要点(.end 加 resultSummary 已批准、proactive 在 data 里、帧 id 同源、channel_persisted 可能带 proactive、markup 畸形确认是 bug 且已修但未部署)+ 我们去重口径复核没踩坑 + W59 工具卡结果区落地 + 真机验收边界(这批只有单测证据)
|
2026-10-07 10:15:13 +08:00 |
|
AskaEth
|
e0bdaa3537
|
merge: W59 —— 工具卡结果区(tool_execution.end 的可选 resultSummary;缺席/空串一律不画,与老服务端逐字节一致)
|
2026-10-07 10:14:30 +08:00 |
|
AskaEth
|
316cd27049
|
feat(chat): 工具卡接上 resultSummary 结果区(缺席时与今天逐字节一致)
后端新契约切片给 `tool_execution.end` 的 `data` 补了一个**可选** `resultSummary`
(与回看面 `toolCalls[].resultSummary` 同一份预算:已脱敏、已截断 ≤500 字符)。
它的语义是「拿到就带、没拿到就缺席」——失败 / 取消 / 超时 / 未落图时**整个键缺席**,
且契约明说**别把缺席读成「没有输出」**。在那之前工具卡只能显示工具名与状态。
改了什么:
- wire:`WsEvent.ToolExecutionEnd` 新增 `resultSummary: String? = null`;共用的
`ToolExecutionData` 跟着加这个字段(只由 `.end` 读,`.start` / `.update` 的领域事件
仍然没有它)。缺键 ⇒ `null`,空串 ⇒ `""`,都在解码层照原样保留、不编结论。
- 领域:`ToolCallState` 新增 `resultSummary`;`applyToolExecution` 多一个同参数
(非空白时顶替、缺席 / 空白时保留原值——三帧里只有 `.end` 带它,一次落不到不该抹掉旧的);
VM 的 `onToolExecution` 与 `ToolExecutionEnd` 分支透传。
- 判据:新增纯函数 `toolResultTextOf(tool)`:摘要非空白 ⇒ 用它;否则回落 `output`
(今天唯一的来源,口径不变);两者都没有 ⇒ `null`。
- UI:`ToolCard` 折叠区里「结果」那一块(参数之后)改由 `toolResultTextOf` 决定要不要画。
`null` 时**连标签与它上方那道 Spacer 都不画** ⇒ 摘要缺席时这一屏与后端补这个键之前
逐字节相同。空串同理(服务端承诺不发空串,但真发了也绝不渲染出一个空的结果区块)。
摘要只展示:不再截断第二次、不解析、不当 markdown;正文沿用与参数同一个 `FoldCardBody`
(`SelectionContainer` + 不设 `maxLines`),长文本整段可见,没有新增任何滚动 / 限高 modifier。
为什么这么做:一句话的判据、纯函数、单测可钉;渲染层只做「画 / 不画」这一个决定,
于是「老服务端(键恒缺席)」与「新服务端(键在场)」共用同一套 UI,不用等部署。
测试:新增 14 条(`WsEventToolExecutionTest` +5、`ToolExecutionCorrelationTest` +9),
覆盖 ①键在场 ⇒ 解出该值(含 500 字符原样透传)②键缺席 ⇒ `null` 且不报错
③空串 ⇒ 解出 `""` 且判据按「不画」处理 ④`toolResultTextOf` 在缺席 / 空串 / 有值三档下的输出
(含摘要优先于 `output`、结果落卡、缺席不抹旧值、落不到卡时不影响任何卡)。
`assembleDebug` 通过;全量单测 114 suites / 1388 tests / 0 failures / 0 errors(基线 1374)。
|
2026-10-07 10:14:10 +08:00 |
|
AskaEth
|
c40a752bcf
|
docs(05): 锚点补记三十八 —— 分享面板 P0 崩溃(两次:Dp.Infinity 当哨兵值 / 展开态只去上限没去 verticalScroll)已修并真机验收(收起 153px↔展开 255px,同一 pid 无崩溃);三条教训(单测抓不到渲染层崩溃、子代理论证再详细也不能替代真机点按、verticalScroll 的约束来源要看谁在外面);顺带推翻 W56『首帧回流看不见』的结论
|
2026-10-07 09:51:59 +08:00 |
|
AskaEth
|
df7b504fa2
|
fix(chat): 分享面板展开态也会崩 —— 只去掉上限是不够的,必须连 verticalScroll 一起去掉
补上一个修复的漏:上一笔只把展开态的 heightIn 上限去掉,没去掉 verticalScroll。而外层
ShareConfirmPreviews 自己就是一个 verticalScroll,它给内容的是**无界高度** ⇒ 这一层的 scroll
在无界高度下被测量 ⇒ 抛同一个 IllegalStateException。真机上点「展开」的那一下崩了第二次
(pid 2077,01:48:53,reason=4 APP CRASH)。
改:展开态把这一层**整个**去掉(上限 + 滚动一起),只留外层预览区那一层滚 —— 这也正是 W56
原本的设计意图('展开态只留外层那一层')。shareConfirmWindowCap 去掉了 expanded 维度(展开态
不再走它),测试同步改写并补了一条「两种状态在链上都有有限约束」。
真机验收(emulator-5554 + 真后端):收起 153px(3 行)+『展开』→ 点开展开 255px +『收起』→
点回收起 153px,**三次操作同一个 pid 4063,没有任何新崩溃记录**。
|
2026-10-07 09:51:44 +08:00 |
|
AskaEth
|
6d4f2cdbae
|
fix(chat): 分享确认面板一打开就崩 —— 正文那一层绝不能再拿到 Dp.Infinity
真机崩溃栈:heightIn(max = Dp.Infinity) 等于不施加约束,verticalScroll 在无界高度下被测量时
checkScrollableContainerConstraints 直接抛 IllegalStateException(ScrollNode.measure ←
SizeNode.measure ← DialogLayout.internalOnMeasure),分享进来、面板刚打开就闪退。
改法:这一层的高度上限只经纯函数 shareConfirmWindowCap() 一个出口 —— 首帧用
SHARE_PREVIEW_MAX_HEIGHT(150dp) 兜底(有限值),展开态这一层不设上限(外层 240dp 仍是有限约束),
并且不再用 Dp.Infinity 当「还没量到」的哨兵值(改用 null)。新增 4 条单测钉住
「任何输入组合都不会产出非有限上限」。
|
2026-10-07 09:45:34 +08:00 |
|
AskaEth
|
af9c63d267
|
docs(05): 锚点三十七补第 5 节 —— 收尾:假后端任务到点、tcp:80 切回真后端、App 已重连并拉到真实会话;并如实记录「换后端目标但地址字符串不变 ⇒ 本地消息残留」这个观察(已核实真后端没有那条测试消息,且正常换地址必经登出会清)
|
2026-10-07 03:57:03 +08:00 |
|
AskaEth
|
c8f19f9c84
|
docs(05): 锚点补记三十七 —— 长跑第3次定案(30min 有效通过;2h 失败是模拟器 system_server 崩溃连带杀进程,五条证据)+ Android 14 划掉会终止 dataSync 前台服务(验收造场景改用返回键 finish Activity)+ W57 端到端验收通过(含未做改前对照的如实说明)
|
2026-10-07 00:18:51 +08:00 |
|
AskaEth
|
f9555fba85
|
docs(05): 锚点补记三十六 —— W57 通知消费点提到进程级(根因链:连接是进程级/订阅挂 Activity 级 VM/replay=0 丢帧)+ 长跑第3次基线(开关确认是开的,推翻上节那条猜想)+ W57 设备验收步骤与副作用
|
2026-10-06 22:23:09 +08:00 |
|
AskaEth
|
900d191b8d
|
merge: W57 —— 把「她主动说的话」的通知消费点提到进程级(新增 ProactiveFrameIntake;修 Activity 被划掉后收不到帧/不弹通知)
|
2026-10-06 22:22:26 +08:00 |
|
AskaEth
|
0e03eba6a4
|
feat(notify): W57 —— 「她主动说的话 ⇒ 弹通知」的帧消费点提到进程级
改了什么
- 新增 app/ProactiveFrameIntake:订 ChatSocket.events,把两类帧(proactive_message、
message_persisted{proactive:true})折成四个入参交给 ProactiveNotifier。纯函数
proactiveFrameOf 管「哪些帧算她主动说的话、每个入参取自帧上的哪个键」。构造即订阅,
刻意不提供 start/stop。
- AppContainer:新增 proactiveFrameIntake(与 chatSocket 同一层,进程级)。
- SessionsViewModel:删掉 onEvent 里那两处转交 proactiveNotifier 的调用,本类不再判断
通知的任何一件事(未读、「谁在跑」原样不动)。
- AppPresence / ProactiveNotifier 的 KDoc 跟着改:谁在读「在不在前台」、谁在喂帧。
为什么
改前这份订阅挂在 SessionsViewModel 的 viewModelScope 上。连接本身是进程级的
(ChatSocket 自带 scope,只有 close() 取消它,而 SessionsViewModel.onCleared 刻意不关),
所以从最近任务把 Activity 划掉之后连接照旧收帧——但 events 是 replay=0 的 SharedFlow,
没有订阅者的 tryEmit 当场丢掉:她的话进了连接、然后没了,通知永远不弹。这是 W53N 登记
的那一格缺口。
边界
- 判据与去重一个字节没动(仍在 ProactiveNotifier 的纯逻辑里)。
- 连接所有权没动:仍然只在登录态那一处 connect/close。
- 不是「新起一条连接」,只是把帧的消费点从 Activity 级提到进程级;VM 里那两处转交是
删而不是留(留两处等于靠去重去遮一次重复消费,且「谁在喂通知」会有两个答案)。
- deliver 里接住落地异常:这条订阅不会再被重建,抛出去就是进程崩溃 + 通知整个哑掉。
验收
assembleDebug 通过;testDebugUnitTest 全量 113 suites / 1370 tests / 0 failures /
0 errors(基线 112/1361,新增的 9 条全在 ProactiveFrameIntakeTest)。
|
2026-10-06 22:21:26 +08:00 |
|
AskaEth
|
6de2f7656a
|
docs(05): 锚点三十五补第 7 节 —— 保活长跑第2次复查结果作废(装包必然杀进程 / tcp:80 已指向真后端使注入 delivered=0 / 复查时进程本就不在),并写明重做需要的前提与代价
|
2026-10-06 22:14:13 +08:00 |
|
AskaEth
|
5f7489d70e
|
docs(05): 锚点补记三十五 —— 模拟器已切真后端(含全角冒号坑与「未知错误」根因);主人报的标签解析 bug 定位到后端畸形 markup <p><p>正文</p> 并已容错修复 + 真机验收;W54A/C/D + W55 + W56 五支合并推送(112/1361 绿);模拟器卡的原因诊断
|
2026-10-06 21:44:07 +08:00 |
|
AskaEth
|
a60e3abc12
|
merge: W56 —— 分享面板预览加「展开/收起」(长正文可读全文;展开态上限 240dp;短正文不显示控件)
|
2026-10-06 21:43:30 +08:00 |
|
AskaEth
|
14955ef6c0
|
feat(chat): 分享确认面板的正文预览可以展开 / 收起
问题:W54D 把正文那一窗改成「三行 + 内部滚动」之后「读不到整段正文」是解决了,但一次只看得见
三行。主人拍板「可以滚动 可以展开」——两个都要,所以这一轮补上后半。
改法(只动 ShareConfirmPanel.kt 与它的测试,ChatScreen / ChatViewModel 与接线一字未碰):
- 正文卡片里加一枚「展开 / 收起」(`Text` + `clickable`,不用 `TextButton`:按钮自带高度会把
这一行撑高一截)。它挂在「分享的文字」那一行的右端,不挂在正文末尾——展开后正文可能好几屏,
末尾那枚「收起」要滚到底才找得到。
- 收起态一个字没变:三行窗(SHARE_PREVIEW_TEXT_LINES = 3)+ 内部滚动。
- 展开态:正文那一窗撤掉自己的上限(`heightIn(max = Dp.Infinity)`,正是首帧那个状态),整段交给
外层预览区滚;外层上限由 150dp 抬到 240dp(SHARE_PREVIEW_EXPANDED_MAX_HEIGHT)。展开态**只留
这一层**滚动是刻意的:两层都设上限时手势先被内层吃掉、内层滚到底才轮到外层,两层上界还互相
牵制,最容易出的错就是「明明展开了还剩半截读不到」。
- 控件**只在正文排下来超过三行时出现**。判据是行数 > SHARE_PREVIEW_TEXT_LINES,不是
`hasVisualOverflow`:这一窗的高度不受限(`verticalScroll` 给子项的就是无上限约束),`Text` 自己
永远报「没有溢出」,拿它当判据会在最长的那种正文上漏掉控件;行数则与上限无关,首帧就可用。
- 展开态为什么是 240dp:这个上限管的是**整个预览区**,下面还压着描述输入框(最多 4 行)与两枚
按钮,面板贴着屏幕底、键盘一弹可用高度只剩半屏上下。150 → 240 在默认字号档上把正文窗从三行抬到
十行上下;再往上(例如整屏的六成)先被顶掉的会是按钮。⚠️ 这个数**没在真机上量过**,是按这笔账
取的保守值,观感由主人验收,要调只改这一个常量。
- 两条判据都做成纯函数(`sharePreviewTextOverflowsWindow` / `sharePreviewMaxHeight`),调用点只读
结论,单测钉的就是它们。
上一批留下的「首帧回流会不会看得出来」疑点,复核结论:**留着,且确认它看不见**(理由已写进
`ShareConfirmBody` 的 KDoc):① 首帧那一下撑不动任何别的东西——正文块是预览区里的最后一个元素,
可视像素只由外层那层独立的上限裁剪决定,上面图片/文件的位置一分不动;② 展开态本来就没有这一层
上限,不存在「量完再回落」;而展开 / 收起那一帧的尺寸变化是用户动作的**目的**,不是回流。真正多
出来的是控件本身(第二帧才出现,因为行数是排完才知道的),它与标签同用 `footnote2`、行高不变,
也顶不动任何东西。组合期 `rememberTextMeasurer` 那条路要外包 `BoxWithConstraints` 拿宽度、还要
自己复现字号与字体缩放,等于把排版做两遍——为一个看不见的跳动换掉结构不划算,不做。
已知取舍(如实记下,没修):转屏(Activity 重建)后展开态回到收起。它现在活在
`remember(preview.text)` 里,跨转屏要活下来只有两条路——进 `ChatUiState`(W54C 给「已取走的分享」
与描述走的就是那条路,但那是 ChatScreen / ChatViewModel 的地盘,本批红线不许碰),或
`rememberSaveable`(本仓一次都没用过的新写法,且弹层窗口带不带 saveable registry 没在真机上验过)。
两条都没走,留一条 ⚠️ 在代码里 + 留给主人定。
测试:新增 3 条(纯函数判据)——「什么时候该出现展开控件」(0/1/3 行不给、4 行起给,并接一份
40 行的真分享走一遍)、「展开态给多高」(150 / 240 两个字面量 + 展开严格更高 + 不超过收起态的
两倍)、「两句标签不同且非空」。展开与滚动本身单测够不着(本模块没有 Compose UI 测试的依赖),
如实说明;既有断言一条未放松。
验证:`./gradlew.bat assembleDebug testDebugUnitTest --tests "*ShareConfirm*"` → BUILD SUCCESSFUL
(ShareConfirmTest 15 条 = 既有 12 + 新增 3、ShareConfirmDraftTest 3 条,0 失败 / 0 错误);
全量 `./gradlew.bat testDebugUnitTest` → BUILD SUCCESSFUL,112 suite / 1361 tests / 0 失败。
|
2026-10-06 21:43:12 +08:00 |
|
AskaEth
|
9ff2de3a1d
|
merge: W55 —— 解析器剥掉落单标签(修后端畸形 <p><p>正文</p> 导致标签字面显示在气泡里)
|
2026-10-06 21:26:34 +08:00 |
|
AskaEth
|
f3c1121887
|
merge: W54D —— 分享确认面板正文预览改成三行窗口 + 内部滚动(发送前能读到整段正文)
|
2026-10-06 21:26:33 +08:00 |
|
AskaEth
|
a861881ec4
|
merge: W54C —— 转屏不再静默丢掉已取走的分享(面板状态与描述移进 ChatUiState,take 仍只发生一次)
|
2026-10-06 21:26:29 +08:00 |
|
AskaEth
|
b3ded77c23
|
merge: W54A —— 通知一事一条(通知 id / PendingIntent requestCode 按消息稳定派生,修她连说几句时后到覆盖先到)
|
2026-10-06 21:26:26 +08:00 |
|
AskaEth
|
464e91621e
|
fix(parse): 容错落单的段落标签,绝不把 <p> 当正文渲染
真机实测:后端产出的某条消息第三段是 `<p><p>那现在…?</p>`(两个开标签、一个闭标签)。
P_BLOCK_RE 是非贪婪的,先把外层 `<p>` 配到那个 `</p>`,内层那个裸 `<p>` 就成了「块内容」;
而 appendAnnotations 只认 `<action>` / `<narration>` / 星号,不认裸 `<p>`,于是它作为普通文本
落进气泡正文,屏幕上直接显示 `<p>那现在…`。
改了什么:新增 stripLooseTags,在切出 `<p>` 块之后按区剥掉「落单标签」——
- 消息里已有完整 `<p>…</p>` 块时,块内块外多出来的每个 `<p>` / `</p>` 一律剥掉,块外没被
ROLE_RE 认领的 `<action>` / `</action>` 同样剥掉;
- 一个完整块都没有时,维持既有「未闭合 `<p>` 整段回落字面文本」的契约(编辑框靠它保证不
静默改字),只再剥两类连「被截断的结构」都算不上的东西:落单的 `</p>` 与后面什么都没有的空开标签。
判据只认完整匹配已知标签词表的形状,`1 < 2` / `<3` / `a<b` / `<p2>` / `<P>` / `<p`(残缺) 一律原样保留。
为什么选择「剥」而不是「原样显示」:这些是服务端产出的标记、不是她说的话;渲染成正文等于往
气泡里塞一串用户既看不懂、也改不掉的字符,而剥掉只是少显示几个本就不该出现的字符,正文一个字
都不动。这只是客户端的容错,不代表服务端这样发是对的。
流式:stablePrefixEnd 不动。扣留点取「第一个没闭合的开标签」,而前缀里的每个开标签必定在同一个
前缀内配到闭标签,所以「是不是落单」在它被产出的那一刻就已定死,中间态剥掉、收口也不用改口;
放宽扣留(把重复的 `<p><p>` 当外层冗余)反而会让 `<p>甲<p>乙` 在中间态把 `<p>甲` 当字面文本吐上屏,
等 `</p>` 到了再跳成 `甲乙`。新增测试钉住这条:逐字符喂实测畸形串时,任何一帧都不许漏出标签,
且已显示的正文只许增不许改口。
测试:assembleDebug 通过;ChatContentParserTest 55 项全绿(新增 17 项);全量 110 suites /
1349 tests / 0 failures / 0 errors(主仓同 HEAD 基线 110 suites / 1332 tests,本次 +17 项)。
|
2026-10-06 21:26:07 +08:00 |
|
AskaEth
|
ec89d828d1
|
fix(chat): 分享确认面板的正文预览改成三行窗口 + 内部滚动
问题:正文那一窗此前是 maxLines = 3 + 省略号,超过三行的分享正文在按「发送」之前根本读不到。
面板里再没有第二处放着这段正文(输入框只剩「用户附加的描述」,初值恒为空),截断就等于
「要发出去的那段正文看不见」——而这正是用户点「发送」时唯一要确认的东西。
改法:正文那一窗高度恒为三行、内部可滚,外层预览区那 150dp 的上限原样不动,所以面板不会被
长正文撑高(图片 / 文件芯片 / 按钮的位置都不变)。滚到底后余量交给外层 scroll,嵌套滚动本身
就有,不用额外接线。
上限为什么不是写死的 dp:Miuix 的 footnote1 只给了 13sp 的字号,lineHeight 是 Unspecified,
行高由系统字体的 ascent/descent 决定、随「大字体」设置一起缩放。写死 dp 在默认字号档上勉强
对得上,到大字号档就会矮一截、把末行的字切一半。所以改为量:从 TextLayoutResult 上取第三行的
底边当上限,量到之前先按无上限排一次(heightIn(max = Dp.Infinity) 在确认过 Compose 的
Density.roundToPx 对 Infinity 的处理后是安全的,即「没有上限」)。
SHARE_PREVIEW_TEXT_LINES 由 private 改 internal,只为让单测能把「这一窗是三行」钉成字面量
(本模块没有 Compose UI 测试的依赖,滚动行为单测够不着)。
测试:新增两条——正文远超三行时纯函数层仍原样交付**完整**正文(一个字不少、不加省略号),
以及三行上限的字面量;既有断言一条未放松。
|
2026-10-06 21:11:30 +08:00 |
|
AskaEth
|
1142bf5ed1
|
fix(share): 转屏不再静默丢掉「已取走、还没确认」的那份分享 —— 面板状态移进 ChatViewModel
分享进来的内容被 `ShareIntake.take()` 取走之后没有归还路径,而它此前只挂在一格
`remember(sessionId)` 上:Activity 因转屏 / 深浅色切换重建时那次组合整份丢掉,那份分享
**既不在待发队列里、也不在界面上**(用户什么都没看到,也没有任何提示);用户已经输入到
一半的那句描述同样丢。
改法:把「已取走、正等确认的那份分享 + 用户补的描述」移进 `ChatUiState.shareConfirm`
(`ShareConfirmDraft`),那一格由 VM 持有,而 VM 活在 `ChatSessionSlots` 的 `ViewModelStore`
里、由 AppContainer 撑着 —— 比组合与 Activity 都活得久,所以重建之后面板照旧长出来,
描述也跟着回来(描述与正文绑在同一个值里,两者同生共死)。
`take()` 仍然只发生一次:认领那一步收进 `ChatViewModel.claimSharedContent`,闸门是纯函数
`shareConfirmClaim`,且**在 `take()` 之前** —— 手上已经有那份内容时一个字节都不动队列
(既不取新的一条,也不会把用户正看着的那份顶掉)。重建后的新一帧照样会走到判据这里,
而那时手上已经有东西(它活在 VM 里)⇒ 不会再取第二次。判据、出队、落地之间没有挂起点,
所以也不存在「取走了却没记住」的窗口。
顺带把这一格计入 `holdsUnsentWork`:这份内容已经没有退路,那一格被回收就等于把它删掉。
`ShareLanding` / `ShareIntake.take()` 里「取走之后直接发」的旧说法一并改成「交给确认面板」。
新增 ShareConfirmDraftTest:钉住「重建后的第二帧不再调 take、队列里那条也不许被取走」、
「内容与描述原样还在」、「队列为空时不造一个空面板」。
|
2026-10-06 21:09:15 +08:00 |
|
AskaEth
|
53339b369a
|
feat(notify): 她主动说的话改成「一事一条」——通知号按消息派生,后到的不再顶掉先到的
改前所有她主动说的话共用一个固定的通知 id,而同 id 的 `notify` 是**替换**:她连说几句时后来
的那条把先来的顶掉,用户拿起手机只看到最后一句,前面几句一声没响就没了(这正是 W50 判据下游
的落地形状)。
改成按消息派生:
- `ProactiveNotify.kt` 新增纯函数 `proactiveNotificationId(key)`:把**去重键**(`proactiveNotifyKey`
的输出)折成通知号。刻意与去重键同源——去重说「这是同一条」、派生说「这是同一条 ⇒ 同一个号」,
两处口径一旦分成两套,就会出现「认得出是同一条、却排出两条通知」的形状漂移(实时帧与落库帧
两条路都会到,这条尤其要命)。
- 两个坑都真的堵上:① 号必须 > 0,而 `hashCode()` 可以是负的、甚至正好是 `Int.MIN_VALUE`
(`abs(Int.MIN_VALUE)` 在 int 域里仍是它自己)⇒ **先展宽到 `Long` 再取绝对值**,不引任何新依赖;
② 不撞既有 id:全仓只有保活那条常驻通知(id = 1)与它 `PendingIntent` 的 request code = 0 是
占用者,派生的号从 1000 起步、宽度 2×10⁹,落点与它们**结构性**不相交。
- `ProactiveNotifier.post` 的签名带上这个号(`post: (text: String, notificationId: Int) -> Unit`),
`AppContainer` 的装配 lambda 同步改;`ProactiveNotification.kt` 删掉恒定的
`PROACTIVE_NOTIFICATION_ID` / `PROACTIVE_OPEN_REQUEST_CODE`,通知 id 与 `PendingIntent` 的
request code **一起**随消息走(只换 id 不换 request code,点后到的那条通知会串到先到那条的
`PendingIntent` 上)。
- 撞号仍会发生(`String.hashCode()` 只有 32 位):后果与改前一样是「后来者替换先来者」,已在
`proactiveNotificationId` 的 KDoc 里如实写清概率,以及为什么接受这一侧。**没有**放松任何既有断言。
测试:新增 `ProactiveNotificationIdTest`(同一条派生两次同号 / 两条不同消息不同号 / 任何输入恒 > 0,
最后一条用 hashCode 正好是 `Int.MIN_VALUE` 的字符串 `polygenelubricants` 钉住前提);另在
`ProactiveNotifierTest` 加一条端到端的号断言(两条路都到的第一条只发一次且号不变、第二条是另一个
号)。`ProactiveNotificationShapeTest` 里那条「不与保活那条相撞」改成扫一遍真实形状的键。
验证:`./gradlew.bat assembleDebug` → BUILD SUCCESSFUL;`./gradlew.bat testDebugUnitTest` →
BUILD SUCCESSFUL,111 suite / 1336 tests / 0 失败(基线 110 suite / 1332 tests,本批净增 4 条)。
|
2026-10-06 21:08:23 +08:00 |
|
AskaEth
|
e5e61e131d
|
docs(05): 锚点补记三十四 —— 压缩后续接(W54 三支并行在飞:通知一事一条/转屏丢分享/预览可滚动;第二批进程级订阅待开)+ 主人三条拍板(预览要滚动+展开、提示词已贴等回执、推送待研究)+ 环境核对
|
2026-10-06 21:02:43 +08:00 |
|
AskaEth
|
33c320219a
|
docs(05): 锚点补记三十三 —— 续接锚点(仓库现状/本轮八项已完成及其真机证据/保活长跑在跑/待主人拍板四条/环境与两个会踩空的地方/两条教训)
|
2026-10-06 20:59:32 +08:00 |
|
AskaEth
|
827f0e61a4
|
docs(05): 锚点三十二补第 6 节 —— 分享面板语义改对并上机验过(预览留正文 / 输入框空+占位符 / 正文+空行+描述 拼接端到端验过)+ 保活长跑因装包重起(新基线 20:13:35 / pid 25584)
另记两条:① 预览正文仍只有 3 行 ⇒ 发送前读不到全文,属观感取舍待主人定;② adb am start 的 --es 值会被空格拆开(我那次 URL 就是这么丢的,不是 App 吞的)。
|
2026-10-06 20:13:52 +08:00 |
|
AskaEth
|
30c53451dc
|
merge: W53X3 —— 分享面板的输入框改成「用户附加的描述」(默认空 + 占位符),正文留在预览区;发送时把分享正文与描述拼起来(空行分隔)
|
2026-10-06 20:10:03 +08:00 |
|
AskaEth
|
514901c12d
|
fix(share): 确认面板的输入框改成「用户附加的描述」——不再预填分享正文,发送时由正文+描述拼成
W53X 的第一版把那个输入框当成了分享正文的可编辑副本(`shareConfirmDraft(share)` 直接返回分享
正文、由 ChatScreen 预填进去),在真机上就渲染成「同一句话排两遍」:预览的「分享的文字」块一遍、
输入框里又一遍,而输入框里那句还看着像一段只读展示。主人澄清语义:那个框是**用户要附加的描述**
——「这次要发什么」归预览,「你想补一句什么」才归这个框,两件事被合成一件才是根因。
改成:
- 输入框初值**恒为空**(`shareCaption` 在取走一份分享时显式归零),占位符改成「加一句描述…」
(与输入栏那句「和昔涟说点什么…」同一个 `useLabelAsPlaceholder` 写法);预览区一个字没动
(图片缩略图 / 文件 chip / 分享正文都留着,那才是这次要发出去的东西)。
- 新增纯判据 `shareConfirmBody(share, caption)` = 分享正文 + 用户描述:两段都在时用**一个空行**
分隔,只有一段就只用那一段,都空就是空串(两侧空白按 `ShareRules.extract` / `sendShared`
的口径归一,中间一个字不动,也不加任何来源标记——那是项目红线)。
- 删掉 `shareConfirmDraft`(名字说「草稿/正文」、行为是「把正文当输入框初值」,留着就是骗人);
`shareConfirmCanSend(share, caption)` 改成**直接问 `shareConfirmBody`** 非空、或另有图片/文件,
于是「发送」可点与否与 `sendShared` 开头那条早退同源;描述为空时不再误把「纯文本分享」判成不可发。
- ChatScreen 的发送口由 `share.copy(text = shareDraft)` 改成
`share.copy(text = shareConfirmBody(share, shareCaption))`;接线语义(`take()` 只发生一次、
面板渲染位置、WindowDialog 选型)一字未动。
测试:`ShareConfirmTest` 跟着改到新语义,拼接规则**穷举**钉住(都有 / 只有正文 / 只有描述 / 都空,
分隔符写成字面量 `"\n\n"`),并钉住「都空且无文件 ⇒ 不能发」与「可点 ≙ 拼出来的正文非空或另有
附件」(同源性)。10 条测试全绿。
验证:`./gradlew.bat --console=plain assembleDebug testDebugUnitTest` → BUILD SUCCESSFUL,
110 suite / 1332 tests / 0 失败(基线 110 / 1330,本批净增 2 条)。
|
2026-10-06 20:09:35 +08:00 |
|
AskaEth
|
59012f711b
|
docs(05): 更正分享面板的语义 —— 输入框是「用户附加的描述」而不是分享正文的复刻
主人澄清:那个输入框(我这边先误判成「预览多余」)应当是用户附加描述的输入框。正确形态:
预览留着分享正文(它才是要发的内容)、输入框默认留空 + 占位符、发送时「分享正文 + 用户描述」拼接
(两段都在时空行分隔)、都空且无文件时按钮不可点。我先前那条「删预览正文」的指令方向反了,已更正,
W53X2 回来即按更正执行。
|
2026-10-06 20:06:00 +08:00 |
|
AskaEth
|
863ac93b25
|
docs(05): 锚点补记三十二 —— 续接锚点(主人两条新指令:复验分享面板「正文两次」、开跑保活长时间后台)+ 环境状态 + 两条各自照抄即可的跑法
|
2026-10-06 20:03:56 +08:00 |
|
AskaEth
|
d8e44c6692
|
docs(05): 锚点补记三十一 —— W53 四项落地(后台通知/退出确认框/保活文案与入口/分享确认面板)+ 真机验收全过
- 三支各自的机制与判据要点,含两条「子代理纠正任务书」的记录(Miuix 0.9.3 没有 extra.* 包;
proactive 判据后端放在 data 里而不是帧壳顶层——我读后端源码核过)+ 我自己补的那处
(AppPresence 补同源 StateFlow 当 key,修「去系统设置授权后回来服务没起」)。
- 真机验收逐条证据(后台通知的形状与点它回主会话、退出确认框、保活文案与「去系统设置」、
分享面板确实没被直接发出、INTERNET 来源是源码)。
- 仍未验/已知缺口如实列出(两条路去重只有单测、通知 id 覆盖、Activity 销毁后收不到帧、
转屏丢已取走的分享、S4 对照组没跑干净)。
|
2026-10-06 19:00:03 +08:00 |
|
AskaEth
|
62f552f87f
|
merge: W53X —— 分享进来先弹确认面板(预览图片/文件 + 可附文字 + 发送/取消),不再一到就发
|
2026-10-06 18:55:36 +08:00 |
|
AskaEth
|
77fbedc350
|
fix(keepalive): 权限「去系统设置里授权后回来」这一档现在真能把服务起起来(补 W53S 登记的那一格)
问题(W53S 标出、我核过代码确认):CyreneApp 里那个起停前台服务的 LaunchedEffect 的 key 只有
`keepAliveEnabled` 与登录态,而通知权限是**现读**的 ⇒ 用户在系统设置里授权后回到 App 时它不会重跑,
设置页已经显示「已开」而服务其实没起(要等下次启动或再动一次开关)。
修法:给 AppPresence 补一条**与既有 `appInForeground` 同源**的 StateFlow(`onStart/onStop` 里
从同一个集合重算,布尔 getter 语义一字未动),让那个 effect 收它当 key —— 「回到前台」正是
「从系统设置返回」必经、且必然变化的那一位。没有新增任何依赖。
|
2026-10-06 18:55:20 +08:00 |
|
AskaEth
|
0a43cedea9
|
docs(share): 补上「为什么是 WindowDialog 而不是 OverlayDialog」的硬理由
查了 Miuix 0.9.3 的源码(下载 sources jar 核对,不是猜):`OverlayDialog` 靠
`MiuixPopupUtils.DialogLayout` 把自己登记进 `LocalDialogStates` / `LocalRootDialogStates`,
而这两个 `staticCompositionLocalOf` **只有 Miuix 的 `Scaffold` 会 provide**
(`basic/Scaffold.kt` 的 Surface 里那层 CompositionLocalProvider)。
本面板的调用点在 `ChatScreen` 那个 `Scaffold` 的**外面**(与附件预览 / 编辑 / 删除三个弹层
同一处)⇒ 换用 `OverlayDialog` 会拿到一个没人读的默认空列表:**不报错、也不显示**,
分享进来会变成「什么都没发生」。这条写进 KDoc,免得后人只看到 W53S 用了 `OverlayDialog`
就顺手把这里改过去(两处调用点的处境不同:那个确认框长在设置页的 Scaffold 里)。
只改注释。验收:./gradlew.bat --console=plain assembleDebug testDebugUnitTest → BUILD SUCCESSFUL,
102 suite / 1278 tests / 0 失败。
|
2026-10-06 18:54:43 +08:00 |
|
AskaEth
|
b06688f1db
|
feat(share): 分享进来先确认再发 —— 新增确认面板
主人 2026-10-06 拍板:分享进来的内容不再「一落地就发」,要像其他 IM 一样先给一个
确认面板。此前 ChatScreen 的分享落地 effect 取走内容后**直接**调 sendShared,
用户没有任何确认机会;现在它把取走的那一份放进 Compose 状态,面板靠它渲染。
新增 `ui/chat/ShareConfirmPanel.kt`(Miuix 自己的 `WindowDialog`,不是 Material3):
- 预览:图片缩略图(与待发草稿预览同形)+ 文件芯片(显示名 / 体积)+ 分享正文(三行,
全文在下面那个输入框里,可编辑);
- 输入框:初值就是分享进来的正文本身,**一个字符都不加**(不拼来源标记——那是红线);
- 「取消」= 本次分享作废(不发送、也不塞回待发队列);「发送」才走既有 sendShared。
为什么这么接线(都在 ChatScreen 那一小段里):
- **取走只发生一次**:`ShareIntake.take()` 只在 `shareConfirm == null` 时被调用,而
`shareConfirm == null` 同时是这个 effect 的键 ⇒ 面板一关就再跑一遍判据、把面板开着时
攒下的下一条分享接上(不加这个键,那一条会一直躺在队列里等连接状态碰巧再变一次);
- 面板开着时新来的分享**原地留在队列里等**,不覆盖用户正看着的那一份;
- 告知按**真正发出去的那一份**算(`share.copy(text = 输入框内容)`):正文可能刚被用户改过,
拿改之前那份去算会说出「正文较长、本想作为文件发送」这种与事实不符的话。
未动:shareLanding / shareNoticeFor / shareWaitingNotice 的语义,sendShared 里
「超长正文 → 文件」那条裁决,ShareIntake 的提取与暂存,W49/W50/W51 的判据。
判据抽成纯函数并单测(`ShareConfirmTest`,8 条):预览该显示哪几样(图片数 / 文件 / 正文是否
非空)、输入框初值、以及「发送」可点 ≙ 真的会有东西发出去(与 sendShared 开头那条早退同一条)。
验收:./gradlew.bat --console=plain assembleDebug testDebugUnitTest → BUILD SUCCESSFUL,
102 suite / 1278 tests / 0 失败(基线 101 / 1270)。
|
2026-10-06 18:53:41 +08:00 |
|
AskaEth
|
6505512c0a
|
merge: W53S —— 退出登录加 Miuix 确认对话框;保活那行补「24 小时累计 6 小时」上限事实 + 权限被拒时的「去系统设置」入口
|
2026-10-06 18:53:34 +08:00 |
|
AskaEth
|
bbd02fc279
|
feat(settings): 退出登录加二次确认;保活那行补系统上限并给「去系统设置」入口
两件事都在这一个切片里,都只碰 ui/settings/**。
一、「退出登录」的确认框
- 现状:W52C 把它改成整行可点的 BasicComponent(红色标题 + Remove 图标),而它紧挨着
「版本」那一行,两条长得几乎一样 ⇒ 误触代价是一次真登出(AuthManager.logout 会吊销
服务端 refresh 并把本机凭据整份清空,没有撤销)。W52C 自己把这条风险标了出来。
- 改成:点条目只立一个本页的 remember 状态(不进任何 VM),弹出的确认框里
「取消 / 点框外 / 系统返回键」三个出口都只是关掉框,一个字都不改;只有确认那一路
才调 onLogout —— 行为链路与加框之前完全一致。
- 控件用的是 Miuix 自己的 OverlayDialog(top.yukonga.miuix.kmp.overlay),由本页 Scaffold
自带的 MiuixPopupHost 承载:标题 / 说明 / 内容分槽,正是为这种事准备的。
(注意 Miuix 0.9.x 并没有 extra.* 包,对话框只在这两个包里,别照旧版 import 去猜。)
- 说明文案如实带上代价:退出后本机登录信息整份清空,服务器地址也要重填。
二、「后台保持连接」那行
- 补事实:dataSync 型前台服务在 targetSdk 37 下,24 小时内累计最多 6 小时会被系统回调
onTimeout(KeepAliveService 到点自停以免 ANR)⇒「全天候保活」做不到。原先这句只写在
KeepAliveService 的代码注释里,用户看不到;现在写进开关下面那行字,且落在「关 / 开」
两档——只写在「已开」那一档等于默认所有人都已经开过一遍,而默认态恰恰是关的。
- 补入口:权限把功能挡住的两档(刚被拒 / 开关开着但权限被撤)原先只说「到系统设置里允许」,
用户得自己摸进去找。现在这两档下面多一个「去系统设置」按钮,直接跳到本 App 的通知设置页
(ACTION_APP_NOTIFICATION_SETTINGS + EXTRA_APP_PACKAGE;厂商砍了这个页面就退到应用详情页,
两次都失败就什么都不做,绝不崩)。按钮出现的条件与那两句文案一一对应,两处不会分叉。
- 顺手复核:点了按钮从系统设置回来时,由 ActivityResult 回调现读一次真实授权,把过期的
「刚刚被拒」清掉 —— 比挂生命周期钩子直接,也不需要它。
- 测试:KeepAliveSummaryTest 原有的 4 条断言一条没动(新增事实挂在「关 / 开」两档,正好
不吃「已开」与「起不来」那两条);新增两条 —— ① 关 / 开两档都必须写明 24 小时窗口与
6 小时上限;② 权限挡住的那两档都必须把用户指向系统设置(与按钮的判据同源)。
验收:./gradlew.bat --console=plain assembleDebug testDebugUnitTest
BUILD SUCCESSFUL;101 suite / 1272 tests / 0 失败(基线 101/1270)。
|
2026-10-06 18:52:31 +08:00 |
|
AskaEth
|
53c7494389
|
merge: W53N —— 她主动说的话在后台/不在聊天页时发系统通知(只这一档;读回 proactive 判据 + 按 id 去重 + 点通知切主会话)
|
2026-10-06 18:52:20 +08:00 |
|
AskaEth
|
611bc5f58a
|
feat(W53N): 她主动说的话在后台/不在聊天页时发一条系统通知(只这一档,普通消息不弹)
改了什么:
1. WsEvent:把落库帧上被丢掉的 `proactive` 判据**读回来**(新增 MessagePersisted.proactive)。
此前 PersistedData 只解 message,而生产 Json 是 ignoreUnknownKeys=true ⇒ 这个键既不解也不报错,
「她主动说的话走了 IM 桥那条落库路」在客户端完全不可见。两层落点都认(data.proactive 与帧壳
顶层 proactive 取或,契约只说了「帧顶」);值用**宽容读法**(只认字面 true),形状不认识时按
「不是 proactive」——判错成真会给普通消息弹通知,判错成假只是漏一次提醒,方向必须偏不打扰。
2. core/notify(新):判据 + 去重 + 落地
- shouldNotifyProactive(appInForeground, viewingTargetSession):语义只有一条——**这条消息此刻
会不会正好被用户看到**。四档组合都写进 KDoc(含「不在前台、聊天页却还挂着」那一档)。
- RecentProactiveNotices:进程级、容量 64、按插入序淘汰。记的是「**到达过**」而不是「弹过」,
因为「用户正看着」那一次也算这条已经到达(他看过了,另一条路再补一帧时不该为一句看过的话弹)。
- ProactiveNotifier:两条路共用一处;顺序是 归属 → 有没有字 → 记账/查重 → 判时机。
- proactiveNotifyKey:落库行 id > IM 消息号 > 时间字面量 > 正文(服务端一个键都不给时的兜底)。
实时帧的 data.id 与落库帧的 message.id 是**同一个值**,去重就靠它。
- proactiveNotificationText:复用权威解析器 domain.parse 剥标记(不再写第二份剥标签正则)、
折成一行、超 120 字截断;截断不劈开代理对(emoji)。
3. 前台判定不引新依赖:MainActivity 的 onStart/onStop 按**实例**登记到一个进程级集合
(配置变更时新实例的 onStart 先于旧实例的 onStop,布尔量在那里必然误判成「后台」);
聊天页那一半由 ChatScreen 的组合报告(DisposableEffect)。两者在 AppContainer 那层汇合。
4. 通知本体:自建渠道 `proactive`(DEFAULT,**要提醒人**,与 keep_alive 的 LOW 分开)、通知 id=2
(与保活那条 id=1 分开,同 id 是替换会把常驻通知顶掉)、标题「昔涟」+ 正文是她的原话、
setAutoCancel、PendingIntent.FLAG_IMMUTABLE(且 request code 与保活那条分开:两者的 Intent
filterEquals 相等而 PendingIntent 判据不含 extras,同 code 会复用先建的那个、丢掉这个 extra)。
通知权限现读(复用 core.keepalive.notificationsAllowed),没授权就不发。
5. 点通知 ⇒ 打开 App 并切回主会话:Intent extra → 进程级待落地意图(ProactiveTapIntake,通知可能
在 App 没起来 / 没登录时被点)→ ChatHomeScreen 落地成「关掉设置/管理/关于/抽屉 + select(web)」,
走的是用户点抽屉里那一行的同一条路,不引导航库。
为什么这样切:通知逻辑**绝不碰连接**(那条共享连接的所有权仍只在登录态那一处),只在帧**已经到达
之后**做判断;判断与去重全是纯函数/纯类,可单测;Android 落地只有一层薄薄的通知构造。
验证:W53N worktree(基线 f8ac95c)`assembleDebug testDebugUnitTest` 全绿 —— 109 suite / 1320
tests / 0 失败(基线 101 / 1270 / 0,新增 8 suite / 50 tests)。真机弹通知、点通知的落地未验(留给主人)。
|
2026-10-06 18:51:24 +08:00 |
|
AskaEth
|
659ab83f00
|
docs(05): 锚点三十补第 7 节 —— 新登记「分享确认面板」(主人 2026-10-06 提)
现状已核实:分享一进来就在 ChatScreen 的 ShareLanding.Send 支被立刻发出(ShareIntake.take() → sendShared),
用户没有任何确认机会。主人要的是像其他 IM 一样:能预览(图片/文件名与大小)、能附文字(预填分享自带的文本)、
由用户点发送才真发、可取消。已指派 W53X 且写明边界(不动 shareLanding/sendShared 语义、take 只许一次)。
|
2026-10-06 18:47:02 +08:00 |
|
AskaEth
|
ed0824f335
|
docs(05): 锚点三十补第 6 节 —— INTERNET 显式声明的验收证据 + 构建锁协议写死三条
锁协议:① 一行一个占用者;② 只删自己那行,绝不整文件删除(这批真踩到);③ 改/删前先读一遍。
|
2026-10-06 18:43:14 +08:00 |
|
AskaEth
|
f3d8d28cf7
|
fix(manifest): 显式声明 INTERNET(不再搭在 okhttp-android 的传递清单上)
原来 release 能联网是靠 com.squareup.okhttp3:okhttp-android:5.5.0 的清单把 INTERNET 合并进来的;
换掉/排除那个依赖,release 会静默失去网络权限(debug 侧 AGP 也注入同一份,所以本地怎么试都看不出来)。
现在显式写进源码。验收::app:processReleaseMainManifest 的 merger 报告里该权限已是
ADDED from app/src/main/AndroidManifest.xml(并 MERGED from okhttp-android)⇒ 来源是源码。
|
2026-10-06 18:43:13 +08:00 |
|
AskaEth
|
f8ac95ceed
|
docs(05): W52 待办变化 —— S4/S5/S9 移出待办,并标明仍未验/未做的三项
- S4 → W52A(真机未验,只有单测+走查);S5/S9 → W52B(真机通过);W52C/W52DE 三项真机通过。
- 仍在待办:保活→前台服务/通知/13+ 权限流 未验;INTERNET 显式声明未做;构建锁协议未写死;
主人待拍板的保活第 3 件(收到消息弹系统通知 + 接上 proactive 判据)。
- 另记:登出条目观感已实机确认(红字「退出登录」+ 箭头离开方框图标,一眼是登出)。
|
2026-10-06 16:15:02 +08:00 |
|
AskaEth
|
46855af2dd
|
docs(05): W52/E 复验补记 —— 「重试」不出现的根因是判据极性接反(已修,真机端到端通过)
根因:判据参数语义是「有没有东西」,调用点传了 isEmpty() ⇒ 唯一会求值的场景里结论恒假。
抓到方式:一次性诊断日志(三个输入全真而结论为假 ⇒ 只剩极性)。修在 0054a3a。
教训:判据单测全绿 ≠ 接线正确——极性/语义类错只有调用点会露,而调用点在 Compose、无 UI 测试装置 ⇒
只能靠真机验收。这是设备验收抓住单测抓不到的错的第二例。
|
2026-10-06 16:14:44 +08:00 |
|
AskaEth
|
0054a3a97a
|
fix(chat): 首屏失败的「重试」按钮永远不出现 —— 判据极性接反(W52/E 真机验收暴露)
改了什么:
- ChatScreen 里 shouldOfferRetryOnEmpty 的第三个实参从 `state.items.isEmpty()` 改成
`state.items.isNotEmpty()`。
为什么:判据是 `!loading && hasError && !hasItems`,参数语义是「列表里**有没有**东西」;调用点传的是
`isEmpty()`(极性相反),于是「列表为空」这个**唯一会求值**的场景里结论永远是 false ⇒ 按钮永远不出现。
判据单测用的是正确语义(不含调用点),所以全绿——这条只有真机才会露出来。
怎么定位的:先加一条一次性诊断日志把 UI 实际看到的三个输入打出来(`loading=false hasError=true
items=0 retry=false` —— 三个输入全真而结论为假 ⇒ 只可能是极性),核对判据本体后确认;定位完日志已删。
真机复验:断网冷启动 ⇒ 「消息没能加载出来」+「重试」;恢复网络后点它 ⇒ 发出
`GET /api/v1/sessions/web/messages` 且离线期间落库的那条出现在屏幕上;按钮随列表非空消失。
|
2026-10-06 16:14:27 +08:00 |
|
AskaEth
|
126a706422
|
docs(05): 锚点补记三十 —— W52 五项落地(已合并推送/101 suite 1270 tests 真跑绿)+ 真机验收结果
- 五项各自的机制与判据要点(S4 一格一份 ViewModelStore 与四条豁免、S5/S9 纯账本与既有 interrupt 出口、
登出条目、降级气泡屏蔽服务端动作、保活开关与 Start/Stop/Leave 判据)。
- 真机验收:设置页两行 / 抽屉「正在思考…」/「停止」发既有 interrupt 帧 / 降级气泡无删改入口 **通过**;
**首屏失败「重试」未通过**(断网冷启动下按钮没出现,根因未定位,写了下一步的诊断办法)。
- 保活开关→服务、S4 回收 未验(时间不够),如实标注。
- 三条教训:① 判据第一步要防「别的 App 抢前台」(gms 账号页把我们的 dump 全吃了);
② 更正 INTERNET 那条:release 的 INTERNET 来自 okhttp-android 传递依赖,建议显式声明;
③ 构建锁协议有洞(有人整文件删除导致别人的行没了)。
|
2026-10-06 16:10:37 +08:00 |
|
AskaEth
|
579eccdfd1
|
merge: W52K —— 「后台保持连接」开关 + 前台服务(默认关;开了才有常驻通知)
|
2026-10-06 16:02:42 +08:00 |
|
AskaEth
|
0243fb78d4
|
feat(设置): 新增「后台保持连接」开关与前台服务(默认关)
现状是一件保活都没做:Android 12+ 会把 cached 进程冻结,进程级共享的那条 WS 连接
(AppContainer.chatSocket)久了就断。这一批只解决「进程别被冻」这一件事,连接本身的生死
仍然归 SessionsViewModel(登录连、登出关),本服务不碰它、也不订阅任何帧。
- core/keepalive/KeepAliveService:dataSync 型前台服务,职责只有让进程活着。常驻一条 LOW
级静默通知(不响不震、setOnlyAlertOnce、点一下回 App),渠道只在服务真的起来时才建。
- core/keepalive/KeepAlivePolicy:起停判据抽成纯函数。shouldKeepAliveRun 三个条件缺一不可
(开关开 / 已登录 / 通知权限在);keepAliveAction 再补上「开关开着但登录态未定(自举中)
一律不动」——不补的话热启动会看到通知闪一下,而中间那段进程正好没人保护。判据刻意**不**查
「现在跑着没有」:那份进程内记忆会被「系统收掉服务却不走 onDestroy」写坏,于是该起的时候
一声不响地没起(保活最不能出的错);重复 start/stop 本身是无害的。
- CyreneApp:全 App 唯一一处按判据起停,开关或登录态一变就重判一次 ⇒「启动时开着且已登录就
在跑」「登出即停」「关开关即停」三件事不需要各自一条路径。
- 设置页新增一行(默认关),文案如实写清代价(常驻一条通知 + 多耗一点电)与当下真实状态;
Android 13+ 申请 POST_NOTIFICATIONS,被拒则开关弹回关闭并说明原因,绝不假装开着。
实现落在新文件 KeepAliveRow.kt:SettingsScreen.kt 只加一行调用(放在「关于」之前,不碰
最底下的登出块),避开 W52C 正在编辑那一段。
- KeepAliveService.onTimeout:targetSdk 37 下 dataSync 前台服务 24 小时内累计跑满 6 小时会被
系统回调,不自己停就 ANR 崩掉整个 App —— 照办 stopSelf,不绕(绕它要么换类型要么加心跳,
都超出本功能边界)。
- Manifest:FOREGROUND_SERVICE / FOREGROUND_SERVICE_DATA_SYNC / POST_NOTIFICATIONS + service
(exported=false、foregroundServiceType=dataSync)。
测试:新增 3 个文件 13 条 —— 默认关、三条件缺一不可、四入参全表一致、文案四档各不
相同且「跑不了时一个字都不提已开」、渠道重要级只许 LOW/MIN。基线 93 suite/1205 tests 全部保持
通过,本树 96 suite/1218 tests/0 失败。
|
2026-10-06 15:59:28 +08:00 |
|
AskaEth
|
d6b0c7cb20
|
merge: W52A —— 常驻 VM 保留最近 3 格 + LRU 回收(S4):一格会话一份 ViewModelStore
冲突仅一处(deleteSession 的 onSuccess 同一锚点):W52B 的 forgetBusy 与 W52A 的 reclaim 是同一件事
的两半,两侧都留 —— 一个让还活着的 VM 别再发注定 404 的补拉,一个把那一格的内存交出去。
|
2026-10-06 15:58:06 +08:00 |
|
AskaEth
|
37289391f8
|
merge: W52B —— 抽屉能看出哪个会话正在跑(S9)+ 行内「停止」入口(S5)
|
2026-10-06 15:56:23 +08:00 |
|