diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index 1eec39e..9acfc1f 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -2197,3 +2197,80 @@ infinity maximum height constraints, which is disallowed. - HEAD = **`3d8bc61`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`(后端在途 + 我的登记与四问)。 - 模拟器装的**是最新包**(含 W58~W62);App 已重启并连回**真后端**;`adb reverse`:`tcp:80 → 10942`(真后端)、`tcp:10942 → 10942`。 - 假后端 18080 进程**仍在跑**(带语音夹具;在后台任务里,到点会被系统收)。 + +## ★★ 锚点补记四十一 · 通知横幅 + 免打扰开关 + W63~W66 四批落地(2026-10-07 下午) + +### 1. 主人报的问题:「我在其他 APP 时没看到横幅」 + +**根因**:「她主动说的话」那条渠道的重要级是 **`IMPORTANCE_DEFAULT`(3)** —— 而**横幅(heads-up)要求 `HIGH`(4)**。代码注释里当初是**刻意**选 DEFAULT 的(「HIGH 会弹横幅盖住用户正在做的事,而这句话并没有急到那个程度」)⇒ 主人明确要横幅,即**口径变更**。 + +**修法(关键约束)**:🔴 **渠道的重要级一旦创建就改不了** —— 只有**用户**能在系统设置里改,App 改不动。所以光改常量对已建过渠道的设备**无效**,必须**换一个新渠道 id**: +- `PROACTIVE_CHANNEL_ID` = **`proactive_v2`**(`IMPORTANCE_HIGH`); +- 新增 `LEGACY_PROACTIVE_CHANNEL_ID = "proactive"`,建渠道时 `deleteNotificationChannel` 掉(重要级改不动,留着只会让系统设置里多一条永不出现的条目); +- `.setPriority(PRIORITY_HIGH)`(管 Android 7.x 及更早,那边还没渠道)。 + +**真机验收(`emulator-5554` + 假后端)**:注入一条 `proactive_message`,屏幕顶部弹出横幅「昔涟 · 现在 / 横幅验收:她主动说了一句」;`dumpsys notification` 显示 `channel=proactive_v2 importance=4` ✓ + +### 2. 主人要的「免打扰」开关(W66,`0e2df11` → 合并 `3b9936e`) + +需求原话:「**开启时弹通知不弹横幅,关闭时两者都弹,默认都弹**」。 + +**为什么只能是「两条渠道 + 切 id」**:同上 —— 重要级改不了 ⇒ 运行时无法开关横幅。**不能**用 `setSilent(true)`(那是完全静音,语义不是「只关横幅」)。 + +| | 弹横幅那档(`quiet=false`,**默认**) | 免打扰那档(`quiet=true`) | +|---|---|---| +| 渠道 id | `proactive_v2` | `proactive_quiet` | +| 重要级 | `IMPORTANCE_HIGH`(4) ⇒ 横幅 | `IMPORTANCE_DEFAULT`(3) ⇒ 提示音、**不弹横幅** | +| 渠道名 | 她主动说的话 | 她主动说的话(免打扰:只在通知栏) | + +- 判据纯函数 `proactiveChannelSpec(quiet)`(id/名/重要级/描述**成对给全**,防「换了 id 没换重要级」)+ `proactiveChannelId(quiet)`;`quiet` **刻意不给默认值**(让编译器拦住「忘了传」)。 +- **惰性创建**:只 ensure「这一次要用的那一条」(两条都建 ⇒ 从没开过免打扰的用户会平白多一条没见过的渠道);**另一条不删**(用户可能对它调过设置);只删那条再也发不到的 legacy `proactive`。 +- 设置项存进既有的 `AppPrefs`(键 `proactive_quiet`,**默认 false**),UI 在设置页新增的「**通知**」段(`PreferenceCard` 外包 `SwitchPreference`),文案写明「**不是静音**」。发通知那一刻现读热副本 ⇒ **开关一改,下一条就生效**(不必重启)。 +- ⚠️ 如实登记的毫秒级窗口:冷启动首读盘未回时读到默认 `false`,代价封顶是「多看到一次横幅」,不丢通知。 +- 全量 **123 suites / 1481 tests**(+2 suite / +8 tests)。 + +**真机验收**:设置页点开该开关(副文案从「**关(默认)**:她主…」变成「**已开:她主动找你说话只在通知栏提…**」);回后台再注入一条 ⇒ **屏幕顶部没有横幅**(截图:桌面干净),`dumpsys` 显示那条走的是 `channel=proactive_quiet importance=3`,通知栏里条目仍在、有提示音 ✓ + +### 3. W63:解析器去二次方(`0e834fc` → 合并 `3b9936e`) + +审查只归因了一处,子代理**找出两处**(都在 `domain/parse/ChatContentParser.kt`): +1. `hasContentAfter` 每个落单 `
` 现扫到串尾(约占 23s); +2. **`tokenize` 里 `P_BLOCK_RE.findAll(raw)` 的懒惰回溯**(约占 8.2s)—— 只修第 1 处时,20000 个落单 `
` 仍是 **8308ms**。 + +**它还推翻了我给的建议方案**:我建议「记下最后一个有效内容的下标」,但答案**对起点不单调**(`"
"` 上 `hasContentAfter(0)=false` 而 `(1)=true` —— 起点落在标签内部也算有正文)⇒ 改成**逐位置答案表**(`ContentAfterQuery`,惰性建表,常规消息一个字节都不建)+ 对 `tokenize` 做「先切掉最后一个 `
` 之后」的前缀优化。 + +**实测(JVM)**:`parse("".repeat(N))`:**2000 → 5ms**(原 854)、**8000 → 12ms**(原 4254)、**20000 → 17ms**(原 26405)。`ChatContentParserTest` 套件从 31.5s 降到 0.15s。全量 **121 suites / 1473 tests**(+2)。 +**留了一处同类平方(如实登记、未修)**:`"
"`(`
`、末尾只有一个 `
`)仍走 `P_BLOCK_RE` 最坏路径,**改前改后一样**(20000 → 7.2s)。治它要手写线性匹配,会牵动气泡切分核心语义,超出「纯性能、语义不变」的范围 ⇒ **要不要另开切片等主人定**。 + +### 4. W64:分享回落链(`4fe5509` → 合并 `3b9936e`) + +修「只有 `EXTRA_SUBJECT`/`EXTRA_TITLE` 的分享**静默**」:**回落链** `EXTRA_TEXT` → `EXTRA_SUBJECT` → `EXTRA_TITLE`(取第一个非空白);三者都空/读不到 ⇒ 走**既有**「读不进来」路径(不再静默);「连值都没有」(误触分享)照旧静默。 +- 来源如实登记(`SharedContent.textSource`),用户看到一句「**这段文字来自对方的分享标题(对方没有提供正文)**」—— 走认领那一刻的**柔和横幅**,**绝不进正文**(有单测钉住 `shareConfirmBody` 逐字等于正文)。 +- 防御式读取**自己核过 SDK 源码**:`BaseBundle.getCharSequence` 自己 catch `ClassCastException` 回 null;会抛的是内部的 `unparcel()` → `BadParcelableException` ⇒ 用「读原始值 + `as?` + `runCatching`」两条都挡。 +- **子代理自己在设备上跑过** 4 条 fixture(只有 SUBJECT / 只有 TITLE / `--ei TEXT 42`(有值但类型读不出,今天=毫无反应)/ TEXT+SUBJECT 同时给 ⇒ TEXT 赢且不出提示),并截图确认。 +- 全量 **123 suites / 1494 tests**(+2 suite / +23 tests)。 +- ⚠️ 两条可选改进(**等主人点头**):① 把来源提示画进**面板内部**(Miuix `WindowDialog` 有 `summary` 参数,现调用点没传 ⇒ 加一行即可);② 让「读不进来」那句在**面板一开**就说(现在是发送时才说)。 + +### 5. W65:分享预览与气泡一致(`cdd58fa` → 合并 `3b9936e`) + +让预览**走同一套 `parse()`**(多段成块、`` 的分享 → 预览里**看不到尖括号**且与气泡字一致;② `
甲
乙
丙
` → 预览**三块分开**;③ `