Skip to content

feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式 - #1455

Closed
zqchris wants to merge 34 commits into
mobile-html-previewfrom
mobile-html-resource-passthrough
Closed

feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式#1455
zqchris wants to merge 34 commits into
mobile-html-previewfrom
mobile-html-resource-passthrough

Conversation

@zqchris

@zqchris zqchris commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ 一条 D 档待 owner 确认(2026-08-03,不埋在线程里)

软链可绕过「同目录」约束:手机端只做词法校验(拒 ..),被控端 mediaFetch.ts 解析
realpath 后只比对全局敏感目录 blocklist,没有「资源 realpath 必须落在 HTML base 的
realpath 子树内」这条。因此产物所在目录里若已存在指向目录外的软链,可把 blocklist 之外的
用户文件拉进来内联;与 iOS 侧已记录的 WebRTC 残留信道叠加即构成外传路径。

正确修法在取件端:取件调用多带 base 目录、被控端做 realpath 包含判定。这是跨端接口 +
权限边界,按仓库分档属「全量验证 + owner 先确认」,故本轮未动手。

当前实际暴露面:触发需要产物目录里已存在这样一个软链(agent 自写产物通常不含);
拉取仍受被控端 blocklist 与 media:fetch 既有三道 gate 约束;单资源 2 MiB / 整页 8 MiB
(按回填倍数计费)上限已在下载前生效。

另:掩码层已整块删除(第 5 轮被挖后)。伪引用会占取件配额 —— 这是刻意接受的退化
(图少取几个),换掉「掩错导致正常页面静默全缺」这个更坏的失败模式。不要加回来,除非
引入真正的 HTML tokenizer。

本轮修复与已知限制(2026-08-04,584e7848)

一批 11 条 review 意见按根因分三组处理,不逐条打补丁(本文件的掩码层已经因逐条补丁走完五轮并整块删除)。

① 已修:RAWTEXT 内容整段跳过。 全局标签正则此前会命中 <script> 体里的伪标签,而回填会
真的替换它 —— const tpl = '<img src="logo.png">' 整段变成 data: URI,作者脚本后续的
字符串处理全部失效。把 HTML 模板放 JS 字符串里是常见写法,所以这打坏的是正常页面
这不是把掩码加回来:掩码要判「< 处在哪个数据态」(开放集合),而 RAWTEXT 的终止规则由规范
写死为「</tag 后跟空白 / / / >」,字符串与注释一律不影响,是闭合规则、可精确实现。
残留误判(属性值里的字面 <script>)已钉进用例,该形态极罕见,净收益为正。

一处定级修正:review 把它标为 security(「脚本能读未被 DOM 引用的文件」)。按实际情况
不构成攻击面增量 —— 页面整份由不可信产物控制,作者直接写真标签 <img src="secret.css">
就会被正常回填,不需要伪标签;可达范围同样受「同目录子树 + MIME 白名单 + 容量预算」约束。
故按功能正确性记,未当安全修复宣传。

② 已修:四处有界的识别缺陷。 CSS url() 大小写不敏感;内联 SVG 的 <image href> /
xlink:href 纳入白名单;Windows 判定改为只看根形态(盘符 / UNC)—— 原先用「路径含反斜杠」
会把 POSIX 上合法的 /tmp/a\b/ 误判、导致该页所有资源失败;htmlResourceMimeFor
不再剥 query/fragment(唯一调用方传的已是解析后的文件系统路径,再剥会把 chart#1.png
这类真实文件名的 #1.png 当 fragment 截掉)。

③ 已知限制(需要真 HTML tokenizer,故不修 —— 不埋在线程里)

这四类的后果都是个别资源不被内联(破图 / 空白背景,可见的退化、fail-closed),
不改写文档结构、不取目录外文件、不影响其它资源:

形态 触发写法 后果
标签边界遇属性值里的 > <img alt="w>90" src="a.png"> src 收不到
style 属性用字符引用写 CSS 引号 style="background:url(&quot;a.png&quot;)" &quot; 被计入 URL
表外命名字符引用 <img src="caf&eacute;.png"> 保持原样 → 取不到
SVG fragment 含引号 fragment 取自解码值,回填破坏属性 标记被截断

前两类与第四类的共同缺口是缺少「解码值 ↔ 原文区间」的双向映射,那是 tokenizer 的产物;
第三类是 HTML 命名引用表有 2000+ 项,内联进 bundle 属独立的体积/维护取舍。
apps/mobile 新增 parser 依赖会改动 runtime fingerprint → 触发冷更门
(docs/dev-rules/mobile-development.md:须把关人针对冷更明确确认、转 draft、退出自动合并),
属独立决定,不是本 PR 能顺手带的。

若放行人认为这四类中某条必须先修,那等于要求本 PR 先过冷更门 —— 请直接说,我按那条路走。

堆叠 PR:base 是 #1441 的分支 mobile-html-preview,不是 main。
#1441 让手机端能渲染 HTML,本 PR 补它明确列为「不包含」的那条已知边界。
#1441 合并后本 PR 的 base 会自动改指 main、diff 自动收敛;请按 #1441 → 本 PR 的顺序看。

这次改了什么

摘要

#1441 让手机端能渲染 agent 产出的 HTML,但只拿到 HTML 本身 —— 页面里
<img src="./chart.png"><link href="assets/a.css"> 这类相对引用在 about:blank
文档里解析不到,于是多文件产物「页面能开、图和样式全缺」。桌面端靠 file://
同目录天然没有这个问题,手机端得自己把资源取回来。

做法:把相对引用挑出来 → 换算成被控端绝对路径 → 逐个走既有的 media:fetch
绝对路径取件通道拿 presign 地址 → 回填进 HTML,取完一次性渲染。

不新增 device-link channel、不新增安全面 —— 用的还是单文件预览已经在用的那条
通道(fetchRemoteAbsFileToUrl,对 workdir 内外一视同仁)。这是刻意选的方案:另一条
路(被控端 http 服务透传,让手机开 http://localhost:5173)需要新增代理转发通道,
等于把被控端的本机网络位置借给手机,安全面是另一个量级,不在本 PR。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:无 issue;承接 feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 描述里列为「已知边界」的那一条
  • 本 PR 包含:本地 HTML 渲染态的同目录资源透传
  • 明确不包含:
    • 被控端 http 服务透传(手机打开 agent 起的 dev server)—— 需要新 device-link
      channel + HTTP 代理转发,安全面显著扩大,须独立立项并过确认门。
    • 外链 CSS 内部的 url():要先取回 CSS 再递归解析它引用的字体/图片。本 PR 只处理
      同一份 HTML 文档内的引用(含 <style> 块)。
    • srcset(多候选 + 密度描述符,收益低于复杂度)。
    • 统一云端交付物接口(仍是独立立项)。
  • 用户可见变化:手机端预览带 assets/ 的多文件 HTML 时,样式与图片会正常显示;取件
    期间显示占位;有资源取不到或超上限时顶部给一条如实提示。
  • 是否存在 breaking change:无。自包含页面走零请求路径,行为与 feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 完全一致。

UI 变化

  • 引用的设计规范:
    • DESIGN.md §10 Light / Dark 双模式交付门槛 —— 本次新增的两处 UI(取件占位、
      资源提示条)全部复用同页既有 themed 组件与语义 token:占位用 centerFill +
      ActivityIndicator(colors.textTertiary) + hintText(与既有 PDF / 音视频取件占位
      同款),提示条直接复用截断横幅的 truncBar / truncText(surfaceChip /
      textSecondary)+ Info 图标。零新增颜色值、零硬编码。
    • 截图:多文件产物回填后的渲染结果见「手工验证」。Light / Dark 未真机目检,如实记在
      「未执行的验证」。
    • 文案:四语言(en / zh-CN / ja / ko)同步新增 3 个 key,pnpm check:i18n-glossary 通过。

怎么验证的

自动验证

pnpm test:unit
结果:exit 0(全绿,3073 tests)

pnpm --filter mobile typecheck
结果:exit 0

pnpm check:i18n-glossary
结果:exit 0(en / zh-CN / ja / ko 无新增违规)

pnpm check:dco
结果:passed(3 commits signed off,1 exempt merge commit)

新增 32 个用例,集中在纯函数层(apps/mobile/src/__tests__/htmlLocalResources.test.ts):

  • 路径换算:相对引用 / ./ / 子目录 / 尾分隔符 baseDir / 查询串与片段剥离 /
    %20 解码(含非法百分号不 throw)/ Windows 被控端反斜杠 join / 中文目录名。
  • fail-closed 边界(逐条钉住):含 .. 段、根相对 /assets/x.png/etc/passwd
    本机绝对 D:\…https://http://localhost:5173data:file://、协议相对
    //host、纯锚点、空 baseDir —— 一律不改写。
  • 词法定位:白名单标签的资源属性、<style> 块的 url()、无引号属性值;
    <a href> / data-src / img-wrapper 不误收;区间精确指向属性值本身(不含引号)。
  • 回填:多处替换不串位、取不到的保留原引用、同路径多处共用一个地址、style 块与
    属性混排、去重与上限(超出计入 skipped,不静默截断)。
  • 取件编排(fetchHtmlResourceUrls):全成功 / 单个失败不影响其它 / 回空地址算失败 /
    并发不超上限且每路径只取一次 / 已取消后停止后续取件 / 空清单不发请求。

取件核心特意抽成纯异步函数就是为了能这样测 —— 本仓 mobile 没有 hook 测试设施,而为一个
测试给 apps/mobile/package.json 加 devDependency 会动 runtime fingerprint、触发冷更
(docs/dev-rules/mobile-development.md 冷更边界),代价不成比例。

手工验证

端到端跑通了核心机制(不只是单测):造一份真实多文件产物 —— index.html +
assets/style.css(外链 CSS)+ assets/logo.svg(<img> 相对引用)+ assets/dot.svg
(<style> 块里的 url())—— 用本 PR 的函数识别出 3 个资源、回填成可下载地址,把
回填后的 HTML 单独放进一个没有 assets/ 兄弟目录的空目录,再交浏览器按 390px
手机视口渲染。三项全部正常显示(蓝底白字圆角卡片 = 外链 CSS 生效;橙色圆点 =
<style> 里的 url() 生效;绿色方块 = <img> 相对引用生效)。渲染目录里没有任何
资源文件,所以它们能显示只可能是回填的结果 —— 这正是要验的那一步。

未执行的验证

  • RN WebView 端到端未在真机或模拟器验证(需手机 app + 在线被控桌面 + device-link
    会话)。上面验证的是「识别 → 回填 → 浏览器能加载」这条链;真机上还要经 media:fetch
    取件与 presign 下载,那两段是 feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 之前就在跑的既有通道(图片 / PDF / 音视频共用)。
    用户可执行的验证步骤:让 agent 在会话 workdir 里产出 index.html + assets/(含一张
    图和一份 CSS)→ 手机点开该 HTML → 应看到完整样式与图片;把图删掉再看,应显示破图 +
    顶部「N 项页面资源没取到」提示。
  • Light / Dark 未真机目检:新增 UI 全部复用既有 themed 组件、零新增颜色值,但按 §10
    的要求这不等于「双模式已验证」,如实记为未目检。
  • 未验证超大资源的实际耗时:并发上限 4、条数上限 32,单个资源走既有 media:fetch
    上传 OSS。真机上一页引用几十张大图时的总耗时未实测。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:

⚠️ 两条待放行人裁决(已 resolve 线程,但不是已消除)

按仓库规则「产品取舍归放行人」「同一根因反复被触碰时暂停局部补丁」交出,不在实现层单方面决定。

1. 惰性文本掩码层:删掉,还是换真 tokenizer?

maskInertHtmlText 存在的唯一目的是「注释 / 脚本体 / 模板体里的伪引用不要占掉 32 项取件配额」。
它已经被 review 连挖四轮,每轮一种新的误判形态:注释里的伪引用 → <template> 体 →
属性值里的字面标签(<div data-tpl="<template>">)与脚本字符串里的孤立 <!-- → 跨两个脚本
配对的 <!-- / -->。根因是正则扫标签无法判别「< 处在哪个 HTML 数据态里」,要根治得有
真正的 tokenizer。补特例可以无限补下去。

两条路:

  • (a) 删掉整个掩码层(我的倾向)。它消除的风险是「图少取几个」(可见的退化);它引入的
    风险曾经是「正常页面静默全缺」(不可见)。四轮下来,后者已经大于前者。删掉后回到照原文扫,
    行为可预测、代码更少。
  • (b) 换成真 tokenizer。能根治,但属独立设计;且 apps/mobile 的依赖是 runtime fingerprint
    输入,引入解析库会触发冷更门。

当前 head 的行为是安全的(未闭合标记一律不掩码,不会再让正常页面全缺),所以留到裁决为止没有
用户风险,只是仍存在上述误判。

2. 资源预算是单页的,不是屏级的

取件挂在 active 上(父级把当前页 ±1 都置 active,文本预取需要它),而 visible 只管 WebView
挂载。所以目录里有多个相邻 HTML 文件时,3 个 TextPreviewPage 会各自跑一份 8 MiB / 4 并发,
实际上限变成约 24 MiB / 12 并发

  • (a) 把取件也收到 visible 门后:上限自然回到单页,但滑到 HTML 文件时才开始取资源 ——
    必然先看到一段 spinner,把现在预取消掉等待的体验退掉。
  • (b) 提升成模块级(屏级)预算与并发池(我的倾向):体验不变,但要引入跨页共享的预算账本
    与取消语义(翻页后在途取件退还多少额度),属独立的状态设计。

风险已被压小的部分:单资源 2 MiB 上限在下载之前就按 media.size 生效,所以倍增的是
「总量与并发」,不是「单个对象可以多大」。


补充说明(勾「无已知风险」但请 reviewer 主动看这四点):

  1. ⚠️ 本轮改掉了一个用户可见行为,需要放行人过目:预览里点 http(s) 外链不再转系统
    浏览器打开,现在只放行 about:(页内锚点)。原因是 review 挖出的一条真实外传路径 ——
    页面内联了从被控电脑取回的资源字节(data: URI),而 CSP 必须允许内联脚本,于是作者
    脚本能把这些字节拼进一个真实 <a href="https://attacker/?d=…">,用户随手一点数据就发
    出去了;CSP 管不到顶层导航(navigate-to 已从 CSP3 移除),点击门只挡程序化导航。
    两点让代价比看起来小:① 这条能力本来只在 iOS 上存在(Android 不上报
    navigationType,一直是拒绝),删掉是把两端对齐;② 与本 PR 已接受的取舍一致 ——
    CSP 已让公网图片/字体在预览里不加载,预览本就是离线的。
    若产品上希望保留外链可点,正确做法是补一套「只放原文里字面存在的 URL」的静态
    白名单 + URL 归一化,属独立设计;本 PR 按「宁可不放行」收窄。外链退路:工具栏
    「分享」送到电脑打开,或切「源码」态自己看 URL。

  2. 不触发冷更。未碰 apps/mobile/package.json / app.json / app.config.js /
    eas.json / plugins/ / modules/ 任何一处,零新增依赖,改动全在 JS/TS 源码。

  3. 不新增 device-link channel,取件通道与安全边界不变。资源取件复用
    fetchRemoteAbsFileToUrlmedia:fetch(xdt-file://open?path=<abs>),与单文件
    预览、聊天 chip 打开 workdir 外文件走的是同一条;media:fetch 本就不限 workdir,
    靠 device-link 三道 gate(被控开关 / 撤销黑名单 / channel 白名单)保证「等同本地
    访问」的信任级。本 PR 没有放宽这层。

  4. 路径面 fail-closed,一个 HTML 拉不到它目录之外的东西。只改写相对引用,且含 ..
    段一律拒绝 —— 引用只能落在 HTML 自己所在目录的子树内。根相对 /x(语义是 web root)
    与本机绝对 file:///… / D:\…(最该警惕的形态)都不改写,保持原样渲染成破图。
    放行 .. 需要先定义「逃到哪层还算安全」,那是独立的边界决定,本 PR 不做。

影响与回滚

  • 影响范围:仅 apps/mobile(文件预览页 + 两个新模块 + 3 个 i18n key)。桌面端零改动,
    共享层零改动。
  • 回滚 / 降级方式:整体 revert 即可,无数据迁移 / 无持久化格式 / 无协议变化。单独降级
    也容易:把 <HtmlFileReader html={htmlResources.html}> 换回 state.content,立刻退回
    feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 的行为(自包含页面照常,多文件缺资源)。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范章节(不涉及 UI 则跳过)
  • 未提交凭证、令牌或授权文件
  • 已补充必要文档(why 全部落在代码注释:htmlLocalResources 头注的边界推导、
    useHtmlLocalResources 的状态语义与「为什么抽纯函数」、预览页的通道选择理由)
  • 已确认测试结果或说明未执行原因

Review 修复(本轮)

安全(P1,Codex + Greptile 各报一次):签名地址不得进可执行页面。 原实现把同目录资源的
预签名地址回填进 DOM,而渲染态保留 JavaScript —— 内联脚本可读 img.src 再以 no-cors 外发,
第三方即可在有效期内下载该被控端文件;<img src=".env"> 这类引用会把同目录任意文件变成可
外传的 bearer URL。我在原描述里写的「权限面比桌面更小」是自我说服,这条确实是本 PR 引入的
漏洞。两层修:

  1. 页面里不再出现签名地址:app 侧下载后转 data: URI 再回填(downloadRemoteMediaAsDataUri),
    单资源 2 MiB 上限,MIME 按扩展名白名单给准(表外类型含音视频一律不改写 —— 整份内联会
    撑爆内存;.env 这类也因此被挡)。
  2. 关掉页面网络出口:新增 htmlPreviewCsp,注入 default-src 'none' + 资源只放行 data:
    • connect-src 'none' + form-action 'none',挂在渲染载体上,任何进 WebView 的 HTML 都带
      策略。

刻意接受的代价:公网 https 图片与字体在预览里不再加载。 放行 img-src https: 等于留
new Image().src='…?d=…' 这条经典外传通道,会让上面第 2 条形同虚设。预览态因此是完全离线
渲染的 —— 这也修正了原描述里「公网图片正常显示」的说法。

SSH(P2):资源取件的 xdt-file://sessionId/remoteHostId/workdir(三项齐备才拼,
与被控端完整性校验同口径),并进取件缓存键(同一路径在不同远端主机上是不同文件)。

CI 红为既有基线问题,不在本 PR 修:Windows unit tests 的失败用例是
src/renderer/voice-input/__tests__/VoiceInputSection.recordingGate.test.ts,已由 issue #1448
记录「在 main 上稳定失败(疑与 CRLF 检出有关)」,另有修复 PR #1440 与维护者确认 #1444
本 PR 只改 apps/mobile,不可能影响桌面 renderer 的该用例;该用例本地全绿。

本轮新增用例:CSP 策略内容与注入位置(含 doctype 不得被挤后导致 quirks mode)、MIME 白名单
正反例、「签名地址不得回填」与「SSH 上下文必须齐备」的接线守卫。pnpm test:unit
pnpm --filter mobile typecheck 全绿。

上一步(#1441)让手机端能渲染 HTML,但只拿到 HTML 本身:页面里
`<img src="./chart.png">` / `<link href="assets/a.css">` 这类相对引用在
about:blank 文档里解析不到,于是多文件产物「页面能开、图和样式全缺」。桌面端靠
`file://` 的同目录天然没这个问题。

把相对引用挑出来 → 换算成被控端绝对路径 → 逐个走**既有** media:fetch 绝对路径
取件通道拿 presign 地址 → 回填进 HTML,取完一次性渲染。不新增 device-link
channel、不新增安全面,用的还是单文件预览已在用的那条通道。

- htmlLocalResources:纯函数层。白名单标签的资源属性(img/script/link/source/
  video/audio/embed/iframe)+ `<style>` 块里的 url();区间指向属性值本身,回填
  从后往前替,同路径多处引用共用一次取件。
- useHtmlLocalResources:编排。取件核心抽成纯异步 fetchHtmlResourceUrls
  (限并发 4、单个失败不阻塞整页、可取消)—— 抽出来是为了可单测,本仓 mobile 没有
  hook 测试设施,而为测试给 apps/mobile/package.json 加 devDependency 会动 runtime
  fingerprint、触发冷更,代价不成比例。
- 自包含页面零请求直接过(最常见情形,行为与 #1441 一致);有引用时显示取件占位,
  避免「先渲染破图再热替换」导致 WebView 重载闪屏。
- 取不到 / 超上限(32)的位置保留原引用并如实提示,不静默截断、不换错地址。

边界刻意 fail-closed:只认相对引用,含 `..` 段、根相对 `/x`、本机绝对、带 scheme
的一律不改写 —— 引用只能落在 HTML 自己所在目录的子树内。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
@zqchris
zqchris requested a review from a team as a code owner August 3, 2026 05:50
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 为手机端 HTML 预览增加同目录资源识别、受限取件、data: URI 回填与失败提示,并补充 CSP、SSH 上下文、缓存隔离和多语言接线。

  • 新增 HTML/CSS 相对资源解析、去重、并发与总量预算控制。
  • 复用现有 media:fetch 通道下载资源,及时回收临时 OSS 对象。
  • 将资源取件状态接入文件预览 UI,并补充对应单元测试与四语言文案。
  • 扩展 SSH 媒体 MIME 支持及远端会话上下文传递。

Confidence Score: 4/5

当前 PR 尚不宜合并,软链目录逃逸、子 realm WebRTC 外传以及 fragment 引号回填破坏边界仍需处理或由放行人明确接受。

同目录资源路径仍缺少 realpath 包含判定,脚本可从未加固的 iframe 子 realm 使用 WebRTC,且解码后的 fragment 引号会未经转义写回原文上下文;这些此前报告的问题在当前 HEAD 仍然可达。

Files Needing Attention: apps/mobile/src/session/htmlLocalResources.ts、apps/mobile/src/session/htmlPreviewCsp.ts、apps/mobile/app/files/preview/[sessionId].tsx、apps/desktop/src/main/device-link/mediaFetch.ts

Important Files Changed

Filename Overview
apps/mobile/src/session/htmlLocalResources.ts 新增 HTML/CSS 本地资源识别、路径换算、RAWTEXT 跳过、MIME 筛选、预算规划与回填逻辑。
apps/mobile/src/session/useHtmlLocalResources.ts 新增资源并发取件、取消处理、总量预算和 React 生命周期编排。
apps/mobile/app/files/preview/[sessionId].tsx 将资源取件、临时对象回收、SSH 上下文、加载态和失败提示接入 HTML 预览。
apps/mobile/src/session/remoteAbsFileFetch.ts 增加一次性绝对路径取件入口,并将 SSH 来源信息纳入缓存隔离。
apps/desktop/src/main/file-browser/ssh-media.ts 为 SSH HTML 资源补充常见样式、脚本、图片和字体 MIME 映射。

Sequence Diagram

sequenceDiagram
  participant HTML as HTML 预览页
  participant Parser as htmlLocalResources
  participant Mobile as 手机端 media:fetch
  participant Desktop as 被控桌面
  participant OSS as 临时 OSS
  participant WebView as HtmlFileReader
  HTML->>Parser: 识别相对资源并生成绝对路径
  Parser->>Mobile: 按并发与总量预算请求资源
  Mobile->>Desktop: device-link media:fetch
  Desktop->>OSS: 上传资源并返回对象信息
  Mobile->>OSS: 下载预签名对象
  Mobile->>Mobile: 转换为 data: URI并回收对象
  Mobile->>Parser: 回填资源引用
  Parser->>WebView: 注入 CSP 后渲染完整 HTML
Loading

Reviews (29): Last reviewed commit: "test(mobile): 钉住两次 RAWTEXT span 扫描不可合并成一..." | Re-trigger Greptile

Comment thread apps/mobile/app/files/preview/[sessionId].tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 219aed6910

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts Outdated
Comment thread apps/mobile/app/files/preview/[sessionId].tsx Outdated
@MagicLizi MagicLizi added status:blocked-by-pr 等另一个 PR 先合并 / 等 retarget(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) labels Aug 3, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 3 条 review conversation 没 resolve(apps/mobile/app/files/preview/[sessionId].tsx / apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

review P1(安全,Codex + Greptile 各报一次):原实现把同目录资源的**预签名地址**回填进
页面,而渲染态保留 JavaScript —— 内联脚本能读 `img.src` 再以 no-cors 外发,第三方即可在
有效期内下载该被控端文件;`<img src=".env">` 这类引用会把同目录任意文件变成可外传的
bearer URL。提示注入过的 agent 产物足以触发。两层修:

1. 资源不再以签名地址进页面:app 侧下载后转成 `data:` URI 再回填
   (downloadRemoteMediaAsDataUri),页面里**不出现任何 bearer 凭证**;单资源 2 MiB 上限,
   超限保留原引用。MIME 按扩展名白名单给准,表外类型不改写(给错会让样式表/脚本被
   浏览器拒收、静默失效)。音视频刻意不在表内 —— 整份内联会撑爆内存。
2. 新增 htmlPreviewCsp:注入 `default-src 'none'` + 只放行 `data:` 资源 +
   `connect-src 'none'` 的 CSP,把页面网络出口交给渲染引擎强制封锁,"读到了也送不出去"
   不再靠我们约定。CSP 挂在渲染载体(HtmlFileReader)上,任何进 WebView 的 HTML 都带策略。
   代价已在 PR 写明:公网 https 图片/字体在预览里不再加载 —— 放行 img-src https: 等于留
   `new Image().src='…?d=…'` 这条经典外传通道,会让整条封锁形同虚设。

review P2(SSH):资源取件的 `xdt-file://` 缺 sessionId/remoteHostId/workdir,被控端
parseSshMediaOrigin 会把远端 absPath 当本机路径交给 realpath —— SSH 会话下所有同目录资源
取件失败,被控桌面恰有同名路径时还会读到错误来源。remoteFileMediaUrl 与
fetchRemoteAbsFileToUrl 补可选 SSH 上下文(三项必须齐备,与被控端完整性校验同口径),
并**进取件缓存键**:同一路径在不同远端主机上是不同文件。

CI 红为既有基线问题,不在本 PR 修:Windows unit tests 失败用例是
src/renderer/voice-input/__tests__/VoiceInputSection.recordingGate.test.ts,已由 issue

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
#1448 记录「在 main 上稳定失败(疑与 CRLF 检出有关)」,另有修复 PR #1440 + 维护者确认
#1444。本 PR 只改 apps/mobile,不可能影响桌面 renderer 的该用例;本地全绿。
Comment thread apps/mobile/app/files/preview/[sessionId].tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

try {
const FileSystem = await import('expo-file-system/legacy');
await FileSystem.writeAsStringAsync(tmp.uri, base64, { encoding: FileSystem.EncodingType.Base64 });

P2 Badge 在上传和下载前执行资源大小上限

当 HTML 引用一个超过 2 MiB 的 PNG、字体或脚本时,这里会先让被控端通过 media:fetch 上传完整文件,再由手机把完整对象下载到缓存,最后才检查 file.size 并丢弃;该通道支持非常大的文件,而且一页可并发处理多项资源,因此一次预览就可能消耗大量流量、磁盘和上传带宽。应在 media:fetch 返回尺寸后、生成 presign 和下载之前拒绝超限对象,或使用可在超过上限时中止的流式下载。

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlPreviewCsp.ts Outdated
Comment thread apps/mobile/src/session/htmlPreviewCsp.ts
Comment thread apps/mobile/src/session/htmlLocalResources.ts
@MagicLizi MagicLizi added touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:security 改动碰到安全边界(review-pr 自动维护,仅展示) labels Aug 3, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 6 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts / apps/mobile/app/files/preview/[sessionId].tsx / apps/mobile/src/session/htmlPreviewCsp.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

Comment thread apps/mobile/src/session/htmlPreviewCsp.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8171fdd1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/HtmlFileReader.tsx
Comment thread apps/mobile/src/session/htmlLocalResources.ts
zqchris added 2 commits August 3, 2026 16:02
review P1(Codex + Greptile 各报一次):CSP 的注入位置有两个真实缺口 ——
① 找 `<head>` 的正则会命中注释里的假标签(`<!-- <head> -->`),CSP 被插进注释、策略整份
被忽略;② 即使命中真的 `<head>`,真实 `<head>` **之前**的内容(浏览器会把前置 `<script>`
照常执行)仍在策略生效前跑,足够在网络被封锁前把刚内联的资源正文外传。

改为**不再寻找 `<head>`**:只认紧贴开头(允许 BOM / 空白)的 doctype,把 meta 插在它之后、
任何作者内容之前;无 doctype 则整份前置。doctype 必须留在最前(挤到后面会掉进 quirks
mode),插在 doctype 之后、`<html>` 之前是合法的 —— 解析器在 "before html" 模式会隐式建出
html/head 并把 meta 放进 head,与显式写在 head 里等效。用例钉住注释假标签与前置脚本两种
攻击形态,以及 doctype 恒在最前。

review P2:`url(sprite.svg#download)` / `<img src="icons.svg#logo">` 这类 SVG sprite 引用靠
fragment 选中目标 symbol/view,而回填替换整个引用会把它永久丢掉,浏览器只渲染 SVG 根文档。
fragment 单独存进 ref 记录,回填时补回 data: URI 之后;取件仍按无 fragment 的路径走,
同一 SVG 的多个 fragment 只取一次件。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review


P1 Badge 在完整下载之前执行资源大小限制

当不可信 HTML 引用一个扩展名受支持但体积很大的文件时,maxBytes 只在 downloadFileAsync 已完整下载到缓存后检查;因此即使最终返回 null,被控端仍先上传整个文件,手机也会完整下载它,最多四个并发、32 个目标可耗尽流量或临时磁盘。应在被控端上传前限制大小,或至少保留 media:fetch 返回的 size 并在开始 presign 下载前拒绝超限对象,而不是把上限仅作为下载后的 DOM 防护。

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/app/files/preview/[sessionId].tsx Outdated
zqchris added 2 commits August 3, 2026 16:15
review P1:逐文件 2 MiB 上限 + 32 条数上限挡不住**总量** —— 32 个接近上限的资源 ≈ 64 MiB
原始字节 ≈ 85 MiB base64,而取件 Map、回填后的 HTML、WebView source 序列化会同时各持一份,
在常见移动端堆限制下足以 OOM。预览内容来自不可信的 agent 产物,这是一条稳定的拒绝服务输入。

新增 HTML_RESOURCE_TOTAL_MAX_CHARS(8 MiB,按 data: URI 字符长度计 —— 那才是真正占内存的
东西),在批量取件里维护累计预算;超预算的资源不取、保留原引用,与「条数超限」合并成同一条
如实提示。

写用例时抓到这个修复自身的一个漏洞:只比 `usedChars >= totalBudget` 是不够的 —— 资源大小要
取回来才知道,被拒的那个不计入 usedChars,于是 usedChars 可能永远到不了预算线、早退分支从不
触发,剩下的资源仍被逐个下载完(网络与临时文件全白花),正是这条 review 要防的 DoS。改为
一旦有资源装不进剩余预算即置「预算耗尽」标记,后续一律不再取件;代价是偏保守(大资源用尽
预算后,后面本可塞下的小资源也不取),预览场景下可预测、有界更重要。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
Comment thread apps/mobile/src/session/htmlLocalResources.ts
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 2 条 review conversation 没 resolve(apps/mobile/app/files/preview/[sessionId].tsx / apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if (/^https?:\/\//i.test(url) && request.navigationType === 'click') {
void Linking.openURL(url).catch(() => undefined);
return false;

P1 Badge 不要把用户点击当成可信导航

当不可信文档先读取已内联资源的 data: URI,再把这些字节写进一个覆盖页面的 <a href="https://attacker/…"> 时,用户的真实点击同样会满足 navigationType === 'click',随后 Linking.openURL 发出的请求就会带走文件内容。与此前修复的程序化 location 路径不同,当前最终代码的新证据是这里仅验证了手势类型,却没有验证外部 URL 是否由可信的 app 侧流程产生;应禁止可执行预览直接决定待打开 URL,或改为由 app 控制且明确确认的外链流程。

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/app/files/preview/[sessionId].tsx Outdated
review P1:每个资源取件都会经 media:fetch 新建一个 OSS 对象,而
fetchRemoteAbsFileToUrl 只回签名 URL、丢掉 ossKey,于是无法发 DELETE —— 一页最多遗留 32 个
在世对象,反复进出预览且 60s 缓存过期后还会继续累积。会话媒体管线已在
deleteRemoteMediaObject / releaseAll 里显式回收同类对象,这里漏了。

新增 fetchRemoteAbsFileOnce:返回完整 resolved 结果(含 ossKey)且**不进那份 60s 共享缓存**
—— 对象用完即删,缓存命中会回一个已被删除的死 URL。预览页在取件后立即 DELETE,放在
finally 里:下载失败 / 超单文件上限 / 超整页预算同样要回收,失败路径才是最容易漏的那条。

守卫用例钉住三点:必须用带 ossKey 的一次性取件、必须打 DELETE 端点、删除必须在 finally 内。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d1e994209

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlPreviewCsp.ts
Comment thread apps/mobile/src/session/remoteAbsFileFetch.ts
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 4 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts / apps/mobile/app/files/preview/[sessionId].tsx / apps/mobile/src/session/htmlPreviewCsp.ts / apps/mobile/src/session/remoteAbsFileFetch.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f28d1466b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 6 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

Comment thread apps/mobile/src/session/htmlLocalResources.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9d92f268c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 8 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b863006ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts Outdated
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 9 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a388a1354

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts
Comment thread apps/mobile/src/session/htmlLocalResources.ts
@MagicLizi

Copy link
Copy Markdown
Contributor

@zqchris 👋 这个 PR 还有 11 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

按根因分组处理 #1455 的一批 review 意见,不逐条补丁。

## 一、RAWTEXT 内容整段跳过(2 条 P1)

全局标签正则此前会命中 <script> 体里的伪标签,而 applyHtmlResourceUrls 会
**真的把它替换**成 data: URI,于是作者脚本源码被改写:
    const tpl = '<img src="logo.png">';   // 整段变成 data:...
    tpl.replace('logo.png', next);        // 后续字符串处理全部失效
把 HTML 模板放在 JS 字符串里是产物常见写法,所以这会打坏**正常页面**。

**这不是把已删除的掩码层加回来**。那条禁令针对的是「某个 `<` 处在哪个 HTML
数据态」这种开放集合判定;RAWTEXT 的终止规则由规范写死为「`</tag` 后跟空白 /
`/` / `>`」,字符串与注释一律不影响,是闭合规则,不需要 tokenizer。开标签查找
从上一段体尾继续,所以体内的 `<script>` 字面量不会被当成新开标签。

对 review 里标成 security 的那条(「脚本能读取未被 DOM 引用的被控端文件」)按
实际情况记为**不构成攻击面增量**:页面整份由不可信产物控制,作者直接写一个真的
`<img src="secret.css">` 就会被正常回填,不需要伪标签。故本项按功能正确性记,
不当安全修复宣传。

已知残留如实钉进用例:属性值里的字面 `<script>` 仍会造成误判(该形态极罕见,
而模板放字符串很常见,净收益为正;两种失败都只影响资源是否内联)。

## 二、四处有界的识别缺陷

- CSS `url()` 函数名改为 ASCII 大小写不敏感:`URL("hero.png")` 是合法 CSS,
  只认小写会让这类资源整个漏掉。
- 内联 SVG 的 `<image href>` / `xlink:href` 纳入白名单:CSP 的 `img-src data:`
  放行它们,不收必然缺图。
- Windows 路径判定改为只看根形态(盘符 / UNC),不再用「路径含反斜杠」:
  POSIX 上反斜杠是合法目录名字符,`/tmp/a\b/index.html` 被误判后会拼出
  `/tmp/a\b\chart.png`,该页所有同目录资源全失败。
- htmlResourceMimeFor 不再剥 query/fragment,契约收成「只吃文件系统路径」:
  唯一调用方传的是 resolveHtmlResourcePath 的输出,已按 URL 规则剥过并解过
  百分号;再剥一次会把 `chart#1.png` 这类真实文件名的 `#1.png` 当 fragment
  截掉、判不出扩展名而被静默排除。同时补「目录名带点、文件名不带点」的判定。

## 三、需要真 tokenizer 的四类,列为已知限制(不在本文件解决)

标签边界遇属性值里的 `>`、style 属性里用 `&quot;` 写 CSS 引号、命名字符引用
只覆盖 6 项、SVG fragment 回填的上下文转义 —— 这四类的正确解法都需要「按 HTML
数据态解析 + 保留原文区间映射」。它们的后果都是**个别资源不内联**(破图 / 空白,
fail-closed、可见),不改写文档结构、不取目录外文件。已写入文件头注与 PR 描述。
apps/mobile 新增 parser 依赖会改动 runtime fingerprint → 触发冷更门,属独立决定。

验证:htmlLocalResources 用例 72 通过(新增 5 组回归);mobile 全量 282 文件 /
3132 用例通过;mobile typecheck exit 0。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 584e784814

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/src/session/htmlLocalResources.ts Outdated
自审补漏(review 未提到这一条)。上一个 commit 给 `image` 只加了
`href` / `xlink:href`,那覆盖的是 SVG 语境;但 `<image>` 在 **HTML** 里是
`<img>` 的废弃别名 —— 实测 Chromium 把 `<image src="a.png">` 解析成 `IMG`
且是 `HTMLImageElement`,浏览器照常加载。只收 href 会让这种写法漏掉、
渲染成破图。三个属性一起收,覆盖两种语境。

顺带用真引擎交叉验证了上一个 commit 里 RAWTEXT 判定的四个边界,结论与实现一致:
- `</scriptx>` 不终止脚本体(整段成为脚本文本、语法错误,后续 `</script>` 才终止);
- `</script ` 带空白正常终止;
- textarea 内的 `<img>` 确实不成为元素(document.images.length === 0);
- 属性值含字面 `<script>` 时浏览器仍正常解析后续 img —— 确认已记录的残留误判
  是真实退化(该形态下我们会跳过一个本可加载的资源),不是理论担忧。

验证:htmlLocalResources 72 通过;mobile 全量 282 文件 / 3132 用例通过;
mobile typecheck exit 0。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const sep = workdir.includes('\\') ? '\\' : '/';
const tail = sep === '\\' ? itemRelPath.replace(/\//g, '\\') : itemRelPath;
return `${workdir}${workdir.endsWith(sep) ? '' : sep}${tail}`;

P2 Badge 改为按根形态判断工作区路径方言

当 POSIX workdir 名本身含合法反斜杠且 HTML 位于其子目录时(如 workdir=/tmp/a\brelPath=sub/index.html),这里会误判成 Windows 并生成 /tmp/a\b\sub\index.html;随后基目录变成 /tmp/a\b\sub,资源被解析为 /tmp/a\b\sub/chart.png,而真实路径是 /tmp/a\b/sub/chart.png,导致该页同目录资源全部取回失败。此前的修复只改了 resolveHtmlResourcePath 的根形态判定,新的证据是上游 absolutePathOf 仍使用 includes('\\');这里也应仅以盘符或 UNC 根判断 Windows 路径。

AGENTS.md reference: AGENTS.md:L88-L90

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

自审补。上一批为「跳过 RAWTEXT 内容」引入的 isInsideSpans 是线性扫,而它的
两个调用方都在标签扫描的循环里 —— 预览内容是不可信 agent 产物,一份塞了几千个
`<script>` 的 HTML 就是稳定的拒绝服务输入:

    3000 段 script → spans=3000,标签匹配≈6000 → 约 1.8×10⁷ 次比较
    5000 段 script → 约 5×10⁷ 次比较(足以卡住移动端 JS 线程)

与本文件已有的 HTML_RESOURCE_TOTAL_MAX_CHARS 是同一类面,而那条是为别人的输入
设的、这条是我自己新引入的,所以一并按 DoS 处理。

spans 由 findRawTextContentSpans 顺序生成,天然按 start 升序且互不重叠,
改成二分即 O(log m)。不用「共用游标单指针」是因为有两个独立推进的调用方
(标签扫描与 `<style>` 块扫描),共用游标会互相打乱。

等价性不只靠新用例:另做了差分测试,在随机生成的有序不重叠 span 布局上对
9.2 万个位置逐一比对二分与线性的结论,**零不一致**(含空 spans 与区间外边界)。

新增两条回归:
- spans 按 start 升序且互不重叠(二分正确性的前提,显式钉住);
- 3000 段 script 的规模用例:伪引用一个不收、真引用全收,并给一个宽松耗时上限
  挡住量级退化(不做精确基准)。

验证:htmlLocalResources 74 通过;mobile 全量 282 文件 / 3134 用例通过;
mobile typecheck exit 0。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44837f4c30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/app/files/preview/[sessionId].tsx
两条 review P2,都是上一批修复的直接延伸。

## 一、`<style>` 在 HTML 规范里也是 RAWTEXT

上一批只把 script / textarea / title 列进跳过表,理由是「style 要扫内部 url()」——
那个理由把两件事混成一件了。实测(Chromium)确认 review 的判断:
`<style>code::before{content:'<img src="a.png">'}</style>` 里的 `<img>` **不成为元素**
(document.images 只有真的那一个,content 就是那段字面文本),当成真标签回填就
**篡改了页面显示内容**。

正解是 review 给的那句「让标签扫描跳过其内容,同时保留专门的 CSS url() 扫描」——
所以现在有两个 span 集合,不能共用一份:
- 标签扫描跳过**全部四种** RAWTEXT(含 style);
- CSS url() 扫描按「除 style 外」的 RAWTEXT 判 —— 它专扫 style 体,把 style 算进去
  会自我否定、让样式块里的 url() 全丢(背景图整批缺失)。
findRawTextContentSpans 因此参数化 tag 列表,各取所需。

## 二、Windows 路径判定的同一根因在第二处

上一批修了 resolveHtmlResourcePath 的「含反斜杠即 Windows」,但预览页的
absolutePathOf 里还有一份同样的写法:workdir `/tmp/a\b`(POSIX 上合法目录名)+
`pages/index.html` 会被改写成 `pages\index.html`,HTML 基目录算成 `/tmp/a\b\pages`,
该页**所有**同目录资源取件失败。

「修一处漏一处」的成因就是判定各写一份,所以这次抽成共享的 isWindowsAbsPath /
joinRemotePath,两处只有一份实现 —— 否则下次还会漏第三处。

回归:style 体跳过 + CSS 字符串端到端不被改写;两个 span 集合分工(含参数化后
span 数差异);joinRemotePath / isWindowsAbsPath 的根形态判定(POSIX 含反斜杠、
盘符两种写法、UNC、尾分隔符、空 base)。

验证:htmlLocalResources 77 通过;mobile 全量 282 文件 / 3137 用例通过;
mobile typecheck exit 0。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
Comment thread apps/mobile/src/session/htmlLocalResources.ts
自审产出。上一个 commit 引入了两个 span 集合(标签扫描含 style、CSS 扫描不含),
看起来像「扫全集再 filter 掉 style」的等价写法,实际不是 —— span 边界依赖 tag
集合本身:扫描命中一段内容后游标推到体尾,集合里少一个 tag 就会让原本被它吞掉的
内层伪标签重新成段。实测:

    <style>var s='<script>x</script>'</style>
      全集扫   → style[7,33)     整个 style 体是一段
      除 style → script[22,23)   style 不成段,体内伪 script 反而成段

位置与数量都不同,合并会静默改变判定结果。加注释 + 用例钉住,免得以后被"顺手优化"。

另补一条:CSS 注释里的伪 `<script>`(无结束标签 → span 延伸到文末)不影响同一
style 块的 url() 收集 —— styleRe 判的是开标签位置,它在该 span 之前;url() 扫描在
body 文本内独立进行。这条链路结论正确但原因微妙,值得显式钉住。

验证:htmlLocalResources 79 通过;mobile 全量 282 文件 / 3139 用例通过;
mobile typecheck exit 0。

Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
@MagicLizi MagicLizi added the touches:large-diff 改动量较大(review-pr 自动维护,仅展示) label Aug 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 330d2e502f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const out: Array<{ start: number; end: number; value: string }> = [];
// 函数名 ASCII 大小写不敏感(review P1):`background: URL("hero.png")` 是合法 CSS,
// CSS 解析器照常加载,只认小写会让这类资源整个漏掉、渲染成空白。
const urlRe = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)'"\s]+))\s*\)/gi;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 跳过 CSS 字符串中的伪 url()

当样式包含 .x::before { content: "url(label.png)" } 且同目录确有 label.png 时,CSS 会把 url(label.png) 当作普通显示文本,但该正则仍会收集它,随后 applyHtmlResourceUrls 会将其中路径替换成完整 data URI,直接篡改页面显示内容。此前接受“伪引用只占取件配额”的取舍后,新的证据是这些引用在取件成功时会被实际回填;这里应按 CSS 字符串/注释上下文跳过伪 url()

Useful? React with 👍 / 👎.


/** HTML 文件绝对路径 → 它所在目录的绝对路径(保住根形态)。 */
export function htmlBaseDirOf(htmlAbsPath: string): string {
const lastSep = Math.max(htmlAbsPath.lastIndexOf('/'), htmlAbsPath.lastIndexOf('\\'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 按路径根形态选择父目录分隔符

当被控端是 POSIX 且 HTML 文件名本身合法包含反斜杠时,例如 /tmp/out/report\draft.html,这里会把文件名中的反斜杠误当成目录分隔符并返回 /tmp/out/report,导致页面的所有相对资源都从错误目录取件。此前对 joinRemotePath 的修正只覆盖了 workdir 判定,新的漏点是该 helper 仍无条件同时查找两种分隔符;应先按绝对路径根形态确定 Windows/POSIX,再只使用对应分隔符。

AGENTS.md reference: AGENTS.md:L88-L90

Useful? React with 👍 / 👎.

@zqchris

zqchris commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

这条 PR 不是被否掉的 —— 它堆叠在 #1441 上,#1441 于 2026-08-03 19:00 UTC squash 合并并删除了 base 分支 mobile-html-preview,GitHub 在同一分钟自动关闭了本 PR。功能并未交付。

已在 #1618 重新落地:关闭前 head 330d2e50 的净 delta 原样应用到已含 #1441 的最新 main(16 文件 / +2092 / −39,逐字一致),base 换成 main,不再堆叠。11 轮 review 的全部修复都保留。后续讨论请到 #1618

justinbao19 pushed a commit to justinbao19/cindy-ci that referenced this pull request Aug 4, 2026
…ndy#1618)

* feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式

重新落地 makecindy#1455。原 PR 堆叠在 makecindy#1441 的分支上,makecindy#1441 于 2026-08-03 19:00 squash 合并
并删除 base 分支,GitHub 因此把 makecindy#1455 自动关闭(同一分钟),功能并未交付。本分支把
makecindy#1455 的净 delta(16 文件 / +2092 / -39)原样应用到含 makecindy#1441 的最新 main 上,内容与
关闭前的 head 330d2e5 一致,11 轮 review 的修复全部保留。

能力:手机端预览 HTML 产物时,把同目录下被引用的 img / css / js / 字体等资源按需取回
并内联成 data: URI,多文件产物不再缺图缺样式。取件受「同目录子树 + MIME 白名单 +
单资源 2 MiB / 整页 8 MiB 预算」三重约束,失败一律 fail-closed(个别资源不内联,
不改写文档结构)。

不动 runtime fingerprint:只改 apps/mobile 的 src/app 下 TS 与 i18n JSON,不碰
app.json / app.config.js / eas.json / apps/mobile/package.json / plugins / modules,
可走 OTA 热更,无冷更边界。

验证:
- apps/mobile 相关四个测试文件 145 passed
  (htmlLocalResources 79 / htmlPreviewCsp 27 / filePreviewPagerWiring 25 / remoteMedia 14)
- pnpm --filter mobile typecheck 通过
- pnpm --filter desktop typecheck 通过

遗留(与关闭前一致,见 PR 正文首段):软链可绕过「同目录」约束这条 D 档仍待 owner
确认,正确修法在被控端做 realpath 包含判定,属跨端接口 + 权限边界,未在本 PR 动手。

Signed-off-by: Chris <tkdv42k4mg@privaterelay.appleid.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

* fix(mobile): 同目录约束与大小门禁改由被控端强制,并给整页预算加预留制

一批 6 条 review 意见按根因分三组处理。

① 软链绕过同目录边界(P1 security,Copilot + Greptile 同根因)
手机端只做词法 `..` 校验,那**只能保证词法子树**;产物目录里若有指向目录外的软链,
词法路径完全合法,而被控端 mediaFetch 在 realpath 后只比对全局敏感目录 blocklist ——
blocklist 之外的用户文件会被取回、内联进不可信页面,再叠上已记录的 WebRTC 残留信道
即构成外传路径。

修法就是 review 指的那条:取件 URL 带 `baseDir`,被控端对**资源与 baseDir 各自
realpath 后**判定包含关系(按路径段比,不是字符串前缀)。两侧都 realpath 是必须的 ——
只 realpath 一侧时 `/tmp` → `/private/tmp` 这类平台软链会把合法资源误拒。
baseDir 解析不了一律拒绝(fail-closed)。SSH 分支在 materializeSshRemoteMedia 内按
workdir 相对路径做同一判定(远端 realpath 需要 file-service 未暴露的 RPC,这条限制
与既有 SSH 媒体边界同级,代码里写明了)。

② 上传前大小门禁(P2)
原先只在手机侧按 `media.size` 拒,而那时被控端已经上传完 OSS(SSH 还先整份拉进
Desktop 磁盘缓存),流量与磁盘已经花掉。改为 URL 带 `maxBytes`,被控端在 stat 之后、
上传/拉取之前拒绝。手机侧那道判断**保留**:老被控端不认新参数(版本歪斜是 fail-open),
size 缺失或谎报时也要兜住。

两个参数畸形一律抛错,不静默降级成"不约束"——否则约束可被畸形输入摘掉。

③ 并发取件突破总量预算(P1)
旧实现只在取回**之后**结算,4 路并发会全部先进 fetchOne,手机同时持有约 4× 单资源
上限的字节,整页 8 MiB 预算形同虚设。改成预留制:开工前按「剩余预算 ÷ refCount」把
这一次的字节上限收窄并预留对应字符数,取回后按实际长度结算、立刻归还没用掉的部分。
收窄出来的上限就是 ② 里下发给被控端的 `maxBytes`,于是**在途**下载量也被总预算约束。
预算不足时不立刻判超预算而是等一次结算(在途预留随时会释放),否则并发满载那一刻
排到的资源会被误判成超预算;只有"没有在途、预算又确实不够"才是真耗尽。
换算函数 bytesForDataUriChars 刻意保守,保证预留恒不越总预算(已钉进用例)。

④ SSH 缓存键与 URL 判定不一致(P2)
抽出 effectiveRemoteMediaSshContext,URL 构造与 fetchRemoteAbsFileToUrl 的缓存键共用
同一份「三项 trim 后非空」判定。原先缓存键只看 ssh 对象是否存在就无条件拼三个字段,
字段为空时 URL 已退化成不带 SSH 参数,键却被分叉 —— 同一次取件永远命不中缓存。

⑤ 两处注释表述不准(P2/P3)
htmlLocalResources 的「子树」改写成「词法子树」并点明真实边界在被控端;预算注释按
预留制的实际语义重写(「后续不再启动新的取件」)。

不动 runtime fingerprint:只改 apps/mobile 的 src/app 下 TS 与 apps/desktop main 侧,
没碰 app.json / app.config.js / eas.json / apps/mobile/package.json / plugins / modules。

验证:
- pnpm --filter mobile test → 3205 passed(285 文件),含新增 6 个预算/收窄/换算用例
- pnpm --filter desktop exec vitest run src/main/device-link src/main/file-browser
  → 628 passed(36 文件),含新增 11 个 baseDir/maxBytes 用例(软链逃逸、兄弟目录前缀、
  /tmp 软链不误拒、fail-closed、畸形参数、SSH 侧 stat 前拒绝)
- pnpm --filter mobile typecheck / pnpm --filter desktop typecheck 均通过

Signed-off-by: Chris <tkdv42k4mg@privaterelay.appleid.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>

---------

Signed-off-by: Chris <tkdv42k4mg@privaterelay.appleid.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
Co-authored-by: Chris <4436110+zqchris@users.noreply.github.com>
wykris added a commit to wykris/cindy that referenced this pull request Aug 4, 2026
* main: (39 commits)
  fix(desktop): preserve legacy plugin permission approvals (makecindy#1657)
  fix(im): 控制命令收归主人专属,群成员的 !stop / slash 不再生效 (makecindy#1658)
  fix(pi): 第一方 MCP 工具走 host 审批策略,与 Claude 的 auto-review 判定对齐 (makecindy#1639)
  test(desktop): validate Python probe execution (makecindy#1653)
  fix(plugin): 支持真实包权限变化后重新确认 (makecindy#1648)
  fix(desktop): sync pinned sessions to mobile (makecindy#1492)
  test: 五条把 main 打红的 Windows 计时型用例改成断言行为 (makecindy#1616)
  fix: 补齐任务列表导出菜单 (makecindy#1620)
  feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式(重开 makecindy#1455) (makecindy#1618)
  fix(device-link): 暴露待命状态并诊断不稳定连接 (makecindy#1611)
  fix(desktop): 修复 Claude Opus 套餐错误提示 (makecindy#1607)
  feat(hook-control): 官方 Telegram 卡消除配置重叠,并支持默认工作目录 (makecindy#1595)
  feat(pi): PI 子代理(只读画像),与 Claude / Codex 共用同一张子代理卡 (makecindy#1458)
  fix(desktop): 让原生分类器故障观察器的漏检在日志里可见 (makecindy#1588)
  fix: 把「审阅器不可用」与「模型判定危险」拆开,前者提示一次 (makecindy#1597)
  test(maker-core): 钉住 MCP fail-closed 闸绑定 resolver 而非界面 (makecindy#1587)
  feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) (makecindy#1441)
  feat(desktop): 统一 @ 资源引用与搜索 (makecindy#1557)
  feat(codex): 子代理卡补齐实时状态,与 Claude 子代理卡形态统一 (makecindy#1438)
  test(maker-core): 锁定 auto-review 送审用目录模型 id 而非 wire 串 (makecindy#1582)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:blocked-by-pr 等另一个 PR 先合并 / 等 retarget(review-pr 自动维护,仅展示) touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:large-diff 改动量较大(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants