feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式 - #1455
Conversation
上一步(#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>
|
| 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
Reviews (29): Last reviewed commit: "test(mobile): 钉住两次 RAWTEXT span 扫描不可合并成一..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 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".
|
@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 的该用例;本地全绿。
There was a problem hiding this comment.
💡 Codex Review
cindy/apps/mobile/src/session/remoteMediaDiskCacheExpo.ts
Lines 123 to 125 in 9db6885
当 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".
|
@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。 |
There was a problem hiding this comment.
💡 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".
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>
There was a problem hiding this comment.
💡 Codex Review
当不可信 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".
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>
|
@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。 |
There was a problem hiding this comment.
💡 Codex Review
cindy/apps/mobile/src/session/HtmlFileReader.tsx
Lines 90 to 92 in 02a9b0e
当不可信文档先读取已内联资源的 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".
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>
There was a problem hiding this comment.
💡 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".
|
@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。 |
There was a problem hiding this comment.
💡 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".
|
@zqchris 👋 这个 PR 还有 6 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
There was a problem hiding this comment.
💡 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".
|
@zqchris 👋 这个 PR 还有 8 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
There was a problem hiding this comment.
💡 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".
|
@zqchris 👋 这个 PR 还有 9 条 review conversation 没 resolve(apps/mobile/src/session/htmlLocalResources.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
There was a problem hiding this comment.
💡 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".
|
@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 属性里用 `"` 写 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>
There was a problem hiding this comment.
💡 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".
自审补漏(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>
There was a problem hiding this comment.
💡 Codex Review
cindy/apps/mobile/app/files/preview/[sessionId].tsx
Lines 329 to 331 in 2111c1c
当 POSIX workdir 名本身含合法反斜杠且 HTML 位于其子目录时(如 workdir=/tmp/a\b、relPath=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>
There was a problem hiding this comment.
💡 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".
两条 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>
自审产出。上一个 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>
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
|
|
||
| /** HTML 文件绝对路径 → 它所在目录的绝对路径(保住根形态)。 */ | ||
| export function htmlBaseDirOf(htmlAbsPath: string): string { | ||
| const lastSep = Math.max(htmlAbsPath.lastIndexOf('/'), htmlAbsPath.lastIndexOf('\\')); |
There was a problem hiding this comment.
当被控端是 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 👍 / 👎.
…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>
* 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) ...
这次改了什么
摘要
#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文档、测试或工程维护范围
channel + HTTP 代理转发,安全面显著扩大,须独立立项并过确认门。
url():要先取回 CSS 再递归解析它引用的字体/图片。本 PR 只处理同一份 HTML 文档内的引用(含
<style>块)。srcset(多候选 + 密度描述符,收益低于复杂度)。assets/的多文件 HTML 时,样式与图片会正常显示;取件期间显示占位;有资源取不到或超上限时顶部给一条如实提示。
UI 变化
资源提示条)全部复用同页既有 themed 组件与语义 token:占位用
centerFill+ActivityIndicator(colors.textTertiary)+hintText(与既有 PDF / 音视频取件占位同款),提示条直接复用截断横幅的
truncBar/truncText(surfaceChip/textSecondary)+Info图标。零新增颜色值、零硬编码。「未执行的验证」。
pnpm check:i18n-glossary通过。怎么验证的
自动验证
新增 32 个用例,集中在纯函数层(
apps/mobile/src/__tests__/htmlLocalResources.test.ts):.// 子目录 / 尾分隔符 baseDir / 查询串与片段剥离 /%20解码(含非法百分号不 throw)/ Windows 被控端反斜杠 join / 中文目录名。..段、根相对/assets/x.png、/etc/passwd、本机绝对
D:\…、https://、http://localhost:5173、data:、file://、协议相对//host、纯锚点、空 baseDir —— 一律不改写。<style>块的url()、无引号属性值;<a href>/data-src/img-wrapper不误收;区间精确指向属性值本身(不含引号)。属性混排、去重与上限(超出计入
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>相对引用生效)。渲染目录里没有任何资源文件,所以它们能显示只可能是回填的结果 —— 这正是要验的那一步。
未执行的验证
会话)。上面验证的是「识别 → 回填 → 浏览器能加载」这条链;真机上还要经
media:fetch取件与 presign 下载,那两段是 feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 之前就在跑的既有通道(图片 / PDF / 音视频共用)。
用户可执行的验证步骤:让 agent 在会话 workdir 里产出
index.html+assets/(含一张图和一份 CSS)→ 手机点开该 HTML → 应看到完整样式与图片;把图删掉再看,应显示破图 +
顶部「N 项页面资源没取到」提示。
的要求这不等于「双模式已验证」,如实记为未目检。
media:fetch上传 OSS。真机上一页引用几十张大图时的总耗时未实测。
风险
风险分类
按仓库规则「产品取舍归放行人」「同一根因反复被触碰时暂停局部补丁」交出,不在实现层单方面决定。
1. 惰性文本掩码层:删掉,还是换真 tokenizer?
maskInertHtmlText存在的唯一目的是「注释 / 脚本体 / 模板体里的伪引用不要占掉 32 项取件配额」。它已经被 review 连挖四轮,每轮一种新的误判形态:注释里的伪引用 →
<template>体 →属性值里的字面标签(
<div data-tpl="<template>">)与脚本字符串里的孤立<!--→ 跨两个脚本配对的
<!--/-->。根因是正则扫标签无法判别「<处在哪个 HTML 数据态里」,要根治得有真正的 tokenizer。补特例可以无限补下去。
两条路:
风险曾经是「正常页面静默全缺」(不可见)。四轮下来,后者已经大于前者。删掉后回到照原文扫,
行为可预测、代码更少。
apps/mobile的依赖是 runtime fingerprint输入,引入解析库会触发冷更门。
当前 head 的行为是安全的(未闭合标记一律不掩码,不会再让正常页面全缺),所以留到裁决为止没有
用户风险,只是仍存在上述误判。
2. 资源预算是单页的,不是屏级的
取件挂在
active上(父级把当前页 ±1 都置 active,文本预取需要它),而visible只管 WebView挂载。所以目录里有多个相邻 HTML 文件时,3 个
TextPreviewPage会各自跑一份 8 MiB / 4 并发,实际上限变成约 24 MiB / 12 并发。
visible门后:上限自然回到单页,但滑到 HTML 文件时才开始取资源 ——必然先看到一段 spinner,把现在预取消掉等待的体验退掉。
与取消语义(翻页后在途取件退还多少额度),属独立的状态设计。
风险已被压小的部分:单资源 2 MiB 上限在下载之前就按
media.size生效,所以倍增的是「总量与并发」,不是「单个对象可以多大」。
补充说明(勾「无已知风险」但请 reviewer 主动看这四点):
浏览器打开,现在只放行
about:(页内锚点)。原因是 review 挖出的一条真实外传路径 ——页面内联了从被控电脑取回的资源字节(
data:URI),而 CSP 必须允许内联脚本,于是作者脚本能把这些字节拼进一个真实
<a href="https://attacker/?d=…">,用户随手一点数据就发出去了;CSP 管不到顶层导航(
navigate-to已从 CSP3 移除),点击门只挡程序化导航。两点让代价比看起来小:① 这条能力本来只在 iOS 上存在(Android 不上报
navigationType,一直是拒绝),删掉是把两端对齐;② 与本 PR 已接受的取舍一致 ——CSP 已让公网图片/字体在预览里不加载,预览本就是离线的。
若产品上希望保留外链可点,正确做法是补一套「只放原文里字面存在的 URL」的静态
白名单 + URL 归一化,属独立设计;本 PR 按「宁可不放行」收窄。外链退路:工具栏
「分享」送到电脑打开,或切「源码」态自己看 URL。
不触发冷更。未碰
apps/mobile/package.json/app.json/app.config.js/eas.json/plugins//modules/任何一处,零新增依赖,改动全在 JS/TS 源码。不新增 device-link channel,取件通道与安全边界不变。资源取件复用
fetchRemoteAbsFileToUrl→media:fetch(xdt-file://open?path=<abs>),与单文件预览、聊天 chip 打开 workdir 外文件走的是同一条;
media:fetch本就不限 workdir,靠 device-link 三道 gate(被控开关 / 撤销黑名单 / channel 白名单)保证「等同本地
访问」的信任级。本 PR 没有放宽这层。
路径面 fail-closed,一个 HTML 拉不到它目录之外的东西。只改写相对引用,且含
..段一律拒绝 —— 引用只能落在 HTML 自己所在目录的子树内。根相对
/x(语义是 web root)与本机绝对
file:///…/D:\…(最该警惕的形态)都不改写,保持原样渲染成破图。放行
..需要先定义「逃到哪层还算安全」,那是独立的边界决定,本 PR 不做。影响与回滚
apps/mobile(文件预览页 + 两个新模块 + 3 个 i18n key)。桌面端零改动,共享层零改动。
也容易:把
<HtmlFileReader html={htmlResources.html}>换回state.content,立刻退回feat(mobile): HTML 生成物在手机端进渲染态(WebView 离线沙箱) #1441 的行为(自包含页面照常,多文件缺资源)。
提交前检查
git commit -s,见 DCO)useHtmlLocalResources 的状态语义与「为什么抽纯函数」、预览页的通道选择理由)
Review 修复(本轮)
安全(P1,Codex + Greptile 各报一次):签名地址不得进可执行页面。 原实现把同目录资源的
预签名地址回填进 DOM,而渲染态保留 JavaScript —— 内联脚本可读
img.src再以 no-cors 外发,第三方即可在有效期内下载该被控端文件;
<img src=".env">这类引用会把同目录任意文件变成可外传的 bearer URL。我在原描述里写的「权限面比桌面更小」是自我说服,这条确实是本 PR 引入的
漏洞。两层修:
data:URI 再回填(downloadRemoteMediaAsDataUri),单资源 2 MiB 上限,MIME 按扩展名白名单给准(表外类型含音视频一律不改写 —— 整份内联会
撑爆内存;
.env这类也因此被挡)。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全绿。