AI 工作流2026-09-24

一天的工作:移动端阅读器的语音与图形收官、消息推送的身份采集通用化,和一次“整块不绘制”的根因定位

移动端 Markdown 阅读器从 UI 移植做到上架提交;消息推送身份采集通用化;发布流水线路径守卫根治;五仓库双平台同步发布;行业情报第十八轮。

今天有六条线并行:移动端 Markdown 阅读器的 Android 版从 UI 移植一路做到上架提交、一次图形渲染故障的根因定位、消息推送中心的全站身份采集能力通用化、内容发布流水线的路径守卫根治、开源仓库的双平台同步发布,以及例行的行业情报采集。工程量最集中的是移动端一线,约占全天一半以上的时间。

一、移动端 Markdown 阅读器:从 UI 移植到上架提交

1. 先给解析层立一条"同一份文档,两端同一结果"的基线

移植最怕的是"看起来对了"。所以我们先用一个取巧的办法把解析器的行为钉死:同一份解析器源码,一端由移动端原生运行时执行,另一端借助运行时的类型擦除能力直接跑同一份 TypeScript 源文件,喂同一份测试文档,比对结构化计数(总块数、标题数、表格数、图形数)。

结论是逐项一致。这一步的价值不在"验证通过",而在于此后任何一端的解析改动都能低成本回归——判据是数字,不是眼睛。

2. 一个"整块不绘制"的故障:从怀疑框架到怀疑两行样式

现象很反直觉:图形块在网页视图里纯白一片,但同页其他元素正常。

排查中最大的陷阱是过早归因。此前的结论一度指向"列表容器内嵌网页视图不出像素",于是我们做了一次一屏多组的对照实验:

  • 同一份诊断页直接挂在页面上 → 正常
  • 同一份诊断页放进列表容器 → 仍然正常
  • 同一份图形页放进列表容器 → 纯白

列表容器当场洗清。接着在图形页里注入三色基线——普通内容块正常、原生矢量图正常、把图形放进固定定位容器也正常——唯独处在普通文档流里的那个容器整块不绘制。

规律一出来,答案就只剩一个方向:容器被某种布局约束"挤"成了不绘制。翻到页面样式表,命中两行把文档高锁成视口高、并禁止溢出的声明。实际图形高度远超视口,溢出的内容不是被裁剪,而是整个普通流容器被跳过绘制。这两行的本意只是给"全屏手势模式"锁视口,却波及了内联场景。把限定条件收窄到全屏状态,问题立即消失。

顺手做了一层加固:关掉图形引擎默认的 HTML 标签承载模式,让标签退化为原生文本节点,减少对嵌套布局与合成路径的依赖。代价是标签不再支持富文本,可以接受。

教训:一屏内的多组对照实验,比逐层"猜—改—编译—装机"快一个数量级;而且能先证伪最贵的那个假设。

3. 内联高度:弃用"量取",改走"确定性计算 + 主动上报"

同一时刻还有两个方向相反的显示问题:某类图形显示不全(高度偏低),另一类底部大片留白(高度虚高)。

根因是同一个:原先靠"从渲染结果里回读高度",而回读值同时受内联样式与布局时机干扰,两个方向都会错。

改法是两件事:

  • 确定性计算:高度 = 视口可用宽与图形自然宽取小者 × 视图框高宽比。该公式与"最大宽度占满 + 高度自适应"的浏览器行为数学等价。
  • 页面主动上报:渲染完成后由页面把算出的高度通过控制台消息报给宿主,宿主截获即可。不引入任何脚本桥接接口,安全面为零。

再把内容包围盒收拢进视图框,溢出裁切与视图框富余留白一并治掉。

这里有一条特别有价值的证据:日志显示某图形真实高度 79,而旧代码的下限阈值恰好是 80——它被自己的下限拒掉了,于是沿用初始高度 260,底部留白 181 像素。数字对上了,问题就再无争议。下限放宽后两个症状同时消失。

4. 全屏查看层:三次踩坑换一条定位原则

给图形加全屏查看,第一版点不动。查下来是回调参数从上层传下来了,中间那一层调用时压根没往下传,而且全屏层组件还没写。

高度问题更折腾。全屏容器依次试了三版:

  1. 父级给百分比高 → 实测高度塌成只剩内边距;
  2. 改成视口单位高 → 高度仍断链,且溢出隐藏把图形整块裁没;
  3. 改用固定定位、四边贴零 → 高度与居中一次到位。

根因:这个页面不能给根元素设高度(设了就会触发前面那条"整块不绘制"),于是文档的百分比链本身是断的,任何依赖父级百分比的高度都解析成零。而固定定位的包含块是视口,不依赖父级百分比链。

一条通用原则就此存下来:当百分比链不可靠时,改用固定定位接管几何。

5. 语音朗读:为什么这一端不用"合成—播放分离"

另一端的实现是"合成音频数据 + 自持音频渲染器播放",因此能精确暂停。Android 的系统语音接口没有暂停能力,也没有音频数据出口。

于是改走逐句合成播放:合成回调一开始播即视为播放位(流式),直接驱动高亮,不需要字节级轮测;暂停 = 停止 + 记住当前句,继续 = 从当前句重播。

代价要诚实说明:本机引擎未实现"朗读范围回调",所以断点续播不可用,"继续"是从当前句开头重播。面板提示文案按引擎能力自动切换——能力不齐时,把差异显式暴露给用户,比假装一致要好。

6. 音频导出:原定的原生编码路线被实测推翻

原计划引入原生编码库做 MP3 导出,评估后发现要额外搭一整套原生构建工具链,代价偏高;而系统内置编码器只能出 AAC 系格式,出不了 MP3。

最终方案:复用与另一端同一份纯 JS 编码器,放在网页视图里跑。好处是零新增依赖、零新增权限,且编码器同源,参数与音质天然一致。

链路是:逐句合成到临时波形文件 → 解析容器定位音频数据 → 分块送进网页视图编码 → 流式写盘。几处细节值得记:

  • 在途分块上限做流控;
  • 输出格式以首句真实音频头锁定(白名单若干档),绝不硬编码采样率;
  • 取消导出时先冲刷尾部,收尾成完整帧流,产出可播放的短文件而不是丢弃数据。

验收用了四路交叉:逐帧扫描坏字节数、外部分析工具解码时长与电平、系统媒体库自解析时长、分块信息熵。有意思的是解码工具报的时长与逐帧计算结果毫秒级一致,两次全量导出的字节数完全相同。

7. 装机与自动化:三个把时间吃掉的坑

  • 系统安全弹窗的三段式:第一道风险提示、第二道"已了解此应用未经检测"复选框、第三道锁屏密码校验。此前反复失败的真因是第二道必须先勾复选框,不勾的话继续安装按钮永远无效。
  • 平台差异要读实测:某系统的导航栏内衬实测恒为零,所以"隐藏系统栏后底部避让归零"在两端结果一致但路径不同——这不是缺陷,别当 bug 修。
  • 坐标一律现取:界面状态一变(例如隐藏系统栏后整栏上移近一个状态栏高度),沿用旧坐标就点空。截图坐标与控件树坐标都要在每次状态切换后重新获取。全屏层内的按钮是网页内元素,控件树看不到,只能按截图点。

8. 一套"本地等价验证"替代方案

有一批改动依赖真机,但真机不一定随时在手。针对图形高度,我们做了一个本地等价验证工具:用无头浏览器加载同一份图形页,逐个图形比对"公式算出的高度"与"浏览器实际渲染高度"。

之所以能本地代真机,是因为该公式与浏览器原生布局行为数学等价——"上报值等于需求值"这个等价关系与平台无关。

覆盖了三组视口(含宽图压缩分支),全部通过;重点复核的两个图形正是反馈最多的那两个。此外还做了灵敏度自检:故意注入一条会压高的样式,工具必须报失败。不做自检的验证工具等于没有——"永远通过"的脚本只会给人假的安全感。

真机接入后补做了回归,用像素包围盒量化,把"双击后好像放大了一点"变成硬结论,并证明复位是精确回到原值。

9. 版本收口

本轮把图形页的修复同步回另一端,覆盖前逐行比对了"被删掉的是不是早已有意替换的旧算法",确认没有真实代码丢失;两侧唯一差异(标签承载模式)互相加了"勿误同步"注释。随后打包、走完应用市场的版本提交流程,进入预审。

二、消息推送中心:把"身份采集"从一次改造做成一份通用能力

这条线今天走了四步:现状评估 → 单点改造上线 → 全站采集可行性评估 → 落地实现与通用化。

关键实测

  • 官方接口的能力边界:获取用户资料的旧接口早已不再返回昵称与头像,实测确认多个字段全空;能拿到昵称头像的唯一官方途径是网页授权。这条结论推翻了"后台随时能拉昵称"的直觉。
  • 关注状态与用户资料是两条独立获取路径:资料需要用户授权(凭证在用户手里),关注状态后台随时可补。系统里有一个"关注状态"字段此前从未写入真实值,本轮通过另一条基础令牌接口补上了第一次真实数据。
  • 可达性黑洞:推送只校验系统内的启用状态,不校验平台侧是否仍关注。取关用户仍显示为可投递,投递失败只落日志、后台无展示无告警——静默失效。这是本轮最值得记录的一条:失败必须可见,否则系统会"看起来在跑"。

通用化

把一次性的改造抽象成可复用能力,做了三处抽象:

  1. 前端脚本属性驱动:接口地址、命名空间、模式、刷新周期、字段清单全部由页面属性提供,换站点零改代码。命名空间变更时自动迁移旧本地存储键,老访客不会被重新授权打扰。
  2. 字段白名单表驱动:以前新增一个档案字段要改三处(写入白名单、查询列、返回体),漏一处就出现"后台有列、界面空白、查不出原因"的假字段。现在只登记一行。标记为不外露的列,即使调用方显式索取也拿不到。
  3. 通用标记透传:跨两次请求的瞬时标记统一走一个通用字段,不必再改表结构。并做了防伪造——某个标记只在特定授权级别下才被接受,否则可以被构造成"把别人标成已拒绝"。

两条设计红线

  • 权限隔离:面向访客的接口只返回展示字段,绝不返回角色与状态;访客不签发登录凭证。
  • 身份与登录解耦:采集到的身份是一次签名令牌,不是登录态。它不能用来访问后台,也不能提权。

一条通用工程教训

失败也必须落库。 批量同步时,失败项如果只报错不写"最后尝试时间",它们会永远排在队首,每次重试都先撞同一批坏数据,队列再也推不动。修法是失败也写时间戳加错误码。这条适用于任何"批量重试队列"。

拒绝授权的处理也值得一说:用户点"拒绝"时,平台只回一个状态串、不回授权码,服务端因此不知道对方是谁。我们的做法是三级接力:拒绝 → 写一枚同域标记并跳回业务页带上降级参数 → 前端改用静默级别重新授权 → 静默授权成功时拿到身份 → 由通用标记触发"已拒绝"落库。

这里必须诚实标注边界:同域标记与本地存储都是本机的,换一台从未访问过的设备首访仍会被平台弹一次(弹窗是平台弹的,拦不住)。落库的价值在于后台可见、同域立即生效,以及为后续"先静默再查库"的两跳模式备好数据。

流程冗余审查

落地后专门做了一轮冗余审查。结论是主链路已接近最优(首次两跳加一次请求、二次访问零请求、带节流),不建议动架构;但揪出若干条真实冗余,其中两类特别典型:

  • 两个工具争抢同一个版本号:两个脚本都用内容摘要前缀做静态资源版本号,却一个用这类摘要、一个用那类摘要,且都改写同一行 → 互相覆盖、永不收敛。虽不致功能失效(链接始终带版本号),但版本号抖动、破坏幂等。修法:统一算法,并给其中一个加排除清单划清归属,使两个脚本可任意顺序执行。
  • 一个"整套假承诺"的字段:某字段有三处读取、零处写入——根因正是前面说的"拒绝时拿不到身份",写不进去。后果是前端那段分支成了死代码,"最多打扰一次"跨设备做不到。这条已被上面那套三级接力修掉。

另有一批"写了但线上全空"的字段:实测发现同一接口实际只返回昵称与头像两部分,其余字段平台根本没给。这类字段从"冗余"降级为空列——保留是明确决策(宁多存不丢),但必须写清楚"现在为空、原因已知"。

三、内容发布流水线:路径守卫根治

自研发布流水线里有两条自动发布钩子(文档类 / 页面类)。今天发现线上静态目录被挂进了一批"部署副本"——本该只发布内容产物,结果把工程目录下的镜像文件也发了出去。

根因不是"名单少了一条",而是三个结构性问题:

  1. 两条钩子各自维护一份重复的排除名单 → 必然漂移;
  2. 只做子串匹配 → 每冒出一个新工作目录就要改代码,而且会误伤(某个短片段会命中同名的其他目录);
  3. 同一份名单里两条路径写法不一致,而匹配前做了分隔符替换 → 其中一条永远命中不了(写了等于没写)。

处置:把判定提取成单一真源模块,两条钩子共用;规则从"碎片子串"升级为段 / 树 / 名称三类,每条显式声明对哪条通道生效;新增目录改配置不改代码;并做成 fail-closed——守卫加载失败就跳过发布("发错"不可撤销,"没发"可重试)。

另外做了跳过分级告知:属于"确实不该发"的级别会把原因回注给操作者,而工具目录、依赖目录这类则静默——否则每改一次技能文档就弹一堆警告,纯属噪声。

这里有一条特别值得记的教训:回放历史台账时,我们发现一批下划线开头的文档似乎"有链接",一度据此判定"下划线是该工具的工作稿命名法",还准备把规则收窄到另一条通道。核实磁盘后才发现,那些文件早已被删除(分片拼装完成后即删)。

台账不等于现行工作流。 定规则作用域必须核实磁盘现状,别把历史记录当事实。

顺带把这条从"约定"升级为"文件名说了算":以下划线开头的工作稿与分片,无论落在哪个目录都不会产出链接;正式稿照发(已有哨兵用例守住反向边界)。

验证:编译通过、自测用例全绿、钩子接线端到端通过(含真实交付物复用链接)、台账回放全量人工复核。

四、官网博客:一处"找不到"的文案

博客详情页显示了一个恒为零的阅读量,要求去掉。

定位过程中的小坑:页面里根本没有"阅读次数"这四个字,实际渲染串是" 次阅读"(前面拼数字)。按字面去搜会得出"不存在"的错误结论。改法很简单,两处元信息行都只保留分类标签与日期。

流程沿用既有标准链路:先与服务端逐文件比对确认一致(一致才动手)→ 本地改 → 刷新静态资源指纹 → 自检 → 服务端备份 → 上传 → 核对四个文件的摘要本地与线上完全一致 → 线上检索确认旧文案只存在于备份里 → 语法检查双通道通过。

五、开源仓库:五仓库双平台同步

把移动端阅读器的 Android 版新开一个仓库,同时把另外四个已有仓库更新到最新版本;五个仓库 × 两个平台共十项,全部用匿名方式核对远端分支指针与本地提交逐字符一致。

本轮最有价值的一个坑:

同一份打包脚本,作为"经典脚本"加载正常,作为"模块"加载就静默失效。

原因是该脚本末尾把结果挂在全局对象上,而被挂的那个中间变量是顶层 var——经典脚本里它落在全局作用域;一旦被当作模块导入,顶层 var 只存在于模块作用域,取值失败 → 对空值取属性抛类型错误 → 模块求值失败。症状极隐蔽:函数照常返回"处理了 4 个图形、全部成功",而页面里一个图形都没有,且不报任何错。

解法:不导入,改为动态插入经典脚本标签,加载完成后再从全局对象取。

另外三条经验:

  • 验收手段:无头浏览器一次性输出"执行后的 DOM + 截图",比长驻调试进程稳得多;把断言结果写进页面里再检索出来即可。
  • 内容哈希版本号必须随改动重算:否则同一地址内容不同,老访客命中长缓存拿到旧脚本——这正是该机制本来要防的事。
  • 探针文件名别用被忽略的后缀:验证"暂存差异检测"时,探针文件因命中忽略规则而没被暂存,一度被误判成检测逻辑坏了。

有一处红线冲突被主动拦下:移动端另一条产品线的仓库对外定位是"零权限、零网络、零收集",而最新开发线为新增朗读能力引入了后台运行与音频播放权限,两者直接冲突。结论是保持该仓库现状,把带朗读的能力另开一条线——对外承诺不能被开发线悄悄改写。

六、行业情报采集:第十八轮

例行的一轮情报采集与发布,产出原料、商店榜单、开源榜单和分析报告四份材料。

本轮三条比较有分量的发现:

  • 权威机构首次正式通报大模型安全众测的典型漏洞风险:涉及数十款产品、数百个漏洞,其中"智能体特有"类占大多数,归纳出六类风险(提示注入、信息泄露、不当输出处理、身份与权限滥用、目标劫持、非预期代码执行)。这为"智能体安全合规交付"这条叙事提供了迄今最强的外部证据。
  • 某主流厂商的智能体工作台当天开启公测并支持桌面端,同时暴露了一整套协议栈。这印证了一个判断:中小开发者的切口不在"买量上架",而在可被意图触发的技能 + 可经统一管道接入的第三方服务。
  • 本地政策同日双文件:一份发展规划征求意见稿(首次出现可公开反馈意见的入口),一份地方性决定当日通过并施行,点名的重点方向包含人工智能与新型软件。

方法论上也固化了几条,例如:同一命名空间下一天内批量上榜且增量一致,判为平台推送,不计入趋势;无描述的项目不做趋势判定;属地政策一律直取发布机构官网;"增速榜冠军易主"比"增量大小"更有信号价值。

七、其他

另有个人与家庭事务类资料的归档与待办盘点。不属于可公开范围,本文不展开。

今天沉淀下来的几条

  1. 一屏内的多组对照实验,胜过逐层猜测。 先证伪最贵的假设,代价最低。
  2. 量不准的东西就别量。 改用确定性计算加主动上报,比从渲染结果里回读可靠得多。
  3. 数字对上了,问题就没有争议。 79 与 80 这种阈值吻合,比任何描述都有说服力。
  4. 失败必须可见、必须落库。 静默失效的系统最危险——它看起来一直在跑。
  5. 台账不等于现状,必须核实源头。 拿历史记录当事实,会把规则定歪。
  6. 验证工具必须做灵敏度自检。 永远通过的脚本只提供假安全感。
  7. 对外承诺优先于开发便利。 权限与定位的红线不能被开发线悄悄改写。
  8. 能力不齐时,把差异显式暴露给用户,比假装一致要好。

未公开项

本篇有 5 处内容未公开或仅作概括:

  1. 服务器登录方式与暴露面核查(含访问来源、访问控制规则、端口与主机路径)——属站点自身攻击面,不外发;
  2. 消息推送中心的用户档案结构与账号细节——含内部账号与个人身份信息;
  3. 情报轮次中的具体采购金额、投标主体与联系方式——商业敏感;
  4. 涉税底稿与个人家庭事务——私人事项,仅作概括;
  5. 具体域名分享链接与内部文档编号——不便外发。

时间分配

移动端阅读器一线约占六成(UI 移植、图形根因、语音、导出、上架各占一部分);消息推送与官网约占两成;发布流水线、开源发布、情报采集各约半成上下,其余为零散事务。


← 返回文章列表