Repository navigation
feat(mobile): 支持将对话分享为图片 - #2188
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd454cc285
ℹ️ 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".
|
| Filename | Overview |
|---|---|
| apps/mobile/app/sessions/[sessionId].tsx | 串联截图事件、可见消息预选、分享状态、HTML 生成、PNG 导出及系统分享生命周期。 |
| apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift | 新增 iOS 截图通知桥接和带源尺寸、输出像素预算的非持久化 WKWebView PNG 渲染器。 |
| apps/mobile/src/session/conversationShareWebViewHtml.ts | 构建离线分享 HTML,使用严格 CSP、内联富文本资源、外部图片清理和降级导出预算。 |
| apps/mobile/src/session/conversationShareProjection.ts | 将消息投影为分享安全数据,仅保留可见正文、紧凑引用及不含 URI 的附件名称。 |
| apps/mobile/scripts/sync-rich-content-assets.mjs | 从根依赖同步 KaTeX 和 Mermaid 资源,并将 KaTeX 字体转换为内联 data URI。 |
| apps/mobile/src/session/ConversationShareWebView.tsx | 在缺少原生渲染器时提供隐藏 WebView PNG 导出和受控导航降级路径。 |
Sequence Diagram
sequenceDiagram
participant User as 用户
participant Screen as 会话页面
participant Store as 分享选择状态
participant Builder as 分享 HTML 构建器
participant Native as iOS 原生渲染器
participant Fallback as RN WebView 降级渲染器
participant Share as 系统分享面板
User->>Screen: 点击分享或完成 iOS 截图
Screen->>Store: 进入分享模式并预选消息
User->>Store: 调整消息选择
Screen->>Builder: 投影选中消息并生成受 CSP 约束的 HTML
Screen->>Native: 请求生成 PNG
alt 原生渲染器可用
Native-->>Screen: PNG Base64
else 原生渲染器不可用
Screen->>Fallback: 导出 PNG
Fallback-->>Screen: PNG Base64
end
Screen->>Share: 写入临时文件并打开分享面板
Reviews (22): Last reviewed commit: "fix(mobile): harden share resource lifec..." | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
为 Mobile 端会话页补齐「对话分享为图片」能力:进入分享模式后可多选消息生成 PNG 并调用系统分享;iOS 额外支持监听系统截图并按可见区域自动预选消息,同时引入原生 Expo Module(并提供 WebView 降级渲染)。
Changes:
- 新增分享选择态的状态管理与 UI(消息复选框、底部分享操作条),并在消息操作栏加入分享入口
- 新增对话导出 HTML/PNG 的渲染链路(JS WebView 导出 + iOS 原生 WKWebView 渲染)
- iOS 增加截图监听,按实际可见区域计算并自动进入分享模式预选消息;补齐相关 i18n 与测试断言
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | 增加 xdt-screenshot-monitor 本地模块依赖与锁文件条目 |
| apps/mobile/package.json | 引入 xdt-screenshot-monitor 到 mobile 依赖 |
| apps/mobile/modules/xdt-screenshot-monitor/package.json | 新增截图监听/原生渲染模块的包定义 |
| apps/mobile/modules/xdt-screenshot-monitor/src/index.ts | Expo 可选原生模块封装:截图事件 + HTML→PNG 能力探测与调用 |
| apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift | iOS 侧监听截图通知并提供 WKWebView 生成 PNG 的原生实现 |
| apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitor.podspec | iOS Pod 规格定义,接入 ExpoModulesCore |
| apps/mobile/modules/xdt-screenshot-monitor/expo-module.config.json | Expo module 配置(Apple 平台注册) |
| apps/mobile/src/session/shareSelectionStore.ts | 分享选择态的外部 store + shareable 消息判定与 hooks |
| apps/mobile/src/session/ShareSelectionBar.tsx | 分享模式底部操作条(取消、全选/取消全选、分享) |
| apps/mobile/src/session/ShareMessageCheckbox.tsx | 分享模式下的消息复选框组件 |
| apps/mobile/src/session/selectableMarkdownHtml.ts | 抽出可复用的 Markdown fragment HTML 与导出 CSS(供分享渲染复用) |
| apps/mobile/src/session/conversationShareWebViewHtml.ts | 将选中消息渲染为可导出 PNG 的 HTML(含页脚与导出脚本) |
| apps/mobile/src/session/ConversationShareWebView.tsx | 隐藏 WebView 渲染并导出 PNG、写入临时文件供系统分享 |
| apps/mobile/src/session/MessageRenderer.tsx | 分享入口/选择态 UI 注入;提供“读取实际可见 shareable 消息”的测量回调 |
| apps/mobile/app/sessions/[sessionId].tsx | 会话页接线:截图触发选择、导出/分享链路、隐藏 composer 相关 UI、挂载 WebView 降级 |
| apps/mobile/src/i18n/locales/zh-CN/session.json | 增加分享图片相关文案(中文) |
| apps/mobile/src/i18n/locales/en/session.json | 增加分享图片相关文案(英文) |
| apps/mobile/src/i18n/locales/ja/session.json | 增加分享图片相关文案(日文) |
| apps/mobile/src/i18n/locales/ko/session.json | 增加分享图片相关文案(韩文) |
| apps/mobile/src/tests/sessionHeaderDesktopFirst.test.ts | 更新源码字符串断言以匹配新增 ref |
| apps/mobile/src/tests/sessionComposerDesktopFirst.test.ts | 更新 env import 断言以匹配新增导入 |
| apps/mobile/src/tests/messageActionDesktopFirst.test.ts | 更新断言以匹配分享态 gating 与结构调整 |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2361ab04a
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 23 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
apps/mobile/app/sessions/[sessionId].tsx:5488
- 当前实现里导出过程(conversationShareBusy=true)并不会阻止用户继续勾选/取消勾选消息;在 Android(以及 iOS 未启用原生 renderer 的降级路径)会导致 ConversationShareWebView 的 html 频繁变更触发 WebView reload,从而打断 in-flight 的 export(pendingRef 只能等超时),最终出现“生成超时/失败”且导出的内容可能与用户最终选择不一致。
建议在生成期间冻结选择集合(禁用消息复选框/全选按钮之外的选择入口),或在开始导出时把用于渲染/导出的 HTML 快照固定到一次操作结束,避免 WebView 重新加载。
const shareSelectedConversation = useCallback(async () => {
if (
conversationShareBusy
|| !shareSelectionActive
|| selectedShareMessages.length === 0
apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift:127
- iOS 原生 WKWebView 渲染 PNG 的路径在 didFinish 后就立即计算尺寸并 takeSnapshot,但这里没有像 JS 降级版一样等待 footer 里的 data: 图片(角色图/Logo)完成解码与布局;在某些机型/压力场景下可能截到“页脚图片为空/只剩文字”的结果。
建议在截图前显式等待 document.images 全部 load/complete(例如使用 iOS 16+ 的 callAsyncJavaScript 等待 Promise,或轮询图片 complete 状态),再进行尺寸测量与 snapshot。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let script = """
(function () {
const stage = document.getElementById('xdt-content');
if (!stage) throw new Error('stage-not-found');
apps/mobile/package.json:65
- 这里新增了本地 Expo 原生模块依赖(xdt-screenshot-monitor),会改变 Mobile runtime fingerprint(属于冷更边界输入)。按仓库规则,这类 PR 需要仓库指定把关人对冷更影响做显式确认后才能合并/随原生包发版;请确认合并路径与发版节奏已对齐(不能按普通 OTA 发布)。
"xdt-ios-app-distribution": "file:./modules/xdt-ios-app-distribution",
"xdt-mobile-realtime-audio": "file:./modules/xdt-mobile-realtime-audio",
"xdt-screenshot-monitor": "file:./modules/xdt-screenshot-monitor",
"xdt-tapdb": "file:./modules/xdt-tapdb",
e2361ab to
a96af4b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a96af4bc9e
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 24 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
apps/mobile/app/sessions/[sessionId].tsx:5425
- selectedShareMessages 的 useMemo 目前依赖 shareSelectionCount,但 shareSelectionCount 只反映数量;当选中集合变化但数量不变时,这里不会重新计算,导出内容可能与当前勾选不一致。建议改为依赖一个随选中集合变化的 key(例如 useSyncExternalStore 读 getSelectedIds().join('|'))。
return shareSelectionStore
.getSelectedIdsInOrder(allShareableIds)
.map((clientId) => shareMessageById.get(clientId))
.filter((message): message is ConversationShareMessage => message !== undefined);
}, [allShareableIds, shareMessageById, shareSelectionActive, shareSelectionCount]);
apps/mobile/app/sessions/[sessionId].tsx:839
- 当前页面只通过 useShareSelectionCount() 订阅了选中数量;当 shareSelectionStore 发生“选中集合变化但数量不变”(例如一次事件里连续 toggle 两条消息、或未来用 setSelection 替换为等长集合)时,useSyncExternalStore 会因 snapshot 相等而跳过重渲,导致导出 HTML/PNG 仍使用旧的选中集合。建议额外订阅一个会随选中集合变化而变化的 snapshot key,并在后续 memo 依赖中使用。
This issue also appears on line 5421 of the same file.
const sessionId = readRouteParam(params.sessionId) ?? '';
const shareSelectionActive = useShareSelectionActive(sessionId);
const shareSelectionCount = useShareSelectionCount();
const deviceId = readRouteParam(params.deviceId) ?? remoteSessionStore.getSessionDeviceId(sessionId) ?? '';
|
@dashhuang 👋 这个 PR 还有 5 条 review conversation 没 resolve(apps/mobile/src/session/conversationShareWebViewHtml.ts / apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift / apps/mobile/app/sessions/[sessionId].tsx / apps/mobile/src/session/ConversationShareWebView.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
a96af4b to
52b5e3e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52b5e3e85e
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 36 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
apps/mobile/src/session/ConversationShareWebView.tsx:107
- 上面把
readyWaitersRef的 waiter 改为(error) => void后,这里需要显式以null表示 ready 成功;否则会把undefined当成错误分支处理/类型不匹配。
if (record.type === "conversation-share-ready") {
readyRef.current = true;
const waiters = readyWaitersRef.current.splice(0);
waiters.forEach((waiter) => waiter());
return;
apps/mobile/src/session/ConversationShareWebView.tsx:140
- 组件卸载时当前实现会把
readyWaitersRef里的 waiter 直接执行(等价于“ready 成功”),这会让exportPng()的 await 继续往下走并尝试对已卸载的 WebView 注入脚本。更合理的是在卸载时用 Error reject 所有 waiter,并用同一个 Error reject 所有 pending export。
() => () => {
readyWaitersRef.current.splice(0).forEach((waiter) => waiter());
for (const pending of pendingRef.current.values()) {
clearTimeout(pending.timer);
pending.reject(new Error("conversation share webview unmounted"));
apps/mobile/src/session/ConversationShareWebView.tsx:60
readyWaitersRef里存的是包装后的回调,但超时分支用(waiter) => waiter !== resolve去移除,永远匹配不上,导致 waiter 泄漏并可能在后续 ready 时被错误触发。建议把 waiter 本体保存在局部变量里,并让 waiter 同时承载 resolve/reject(ready 时 resolve,超时/卸载时 reject)。
This issue also appears in the following locations of the same file:
- line 103
- line 136
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
readyWaitersRef.current = readyWaitersRef.current.filter(
(waiter) => waiter !== resolve,
);
apps/mobile/app/sessions/[sessionId].tsx:74
AUTH_REGION在本文件内没有被引用(仅出现在 import 行),会增加噪音并可能在开启 noUnusedLocals/noUnusedParameters 或 lint 时变成告警。若确实不需要,建议移除该 import(同时更新对应的 desktop-first 断言测试)。
import { AUTH_REGION, DEVICE_LINK_API_BASE_URL, MOBILE_VISUAL_MOCK_ENABLED } from '@/config/env';
|
@dashhuang 👋 这个 PR 还有 3 条 review conversation 没 resolve(apps/mobile/app/sessions/[sessionId].tsx / apps/mobile/src/session/ConversationShareWebView.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: Dash <dashhuang@gmail.com>
83f0ba6 to
87a073d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 47 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
apps/mobile/app/sessions/[sessionId].tsx:5583
exportConversationSharePng里原生 rendererrenderConversationShareHtmlToPng一旦 reject/throw,会直接中断并进入上层 catch,从而完全跳过 WebView 降级路径;同时 JSX 里也只在!conversationShareNativeRendererAvailable时才挂载ConversationShareWebView,即便想在原生渲染失败时降级也拿不到 webview ref。建议明确选择:要么把原生渲染异常也纳入降级(try/catch 后回落 WebView,并确保 WebView 在 share 模式下可用/可延迟挂载),要么在注释/策略上说明“原生失败不降级”的原因,避免后续误解为已具备可靠兜底。
outbox: outboxDisplayItems,
hiddenClientIds: queueHiddenClientIds,
sendingClientIds: sendingQueueBadgeClientIds,
editingClientId: queueEditing?.clientId ?? null,
steeringClientIds: new Set(inputProjection.steeringQueueClientIds),
presentationByClientId,
previewByOssRef: sentPreviewByOssRefRef.current,
});
},
[
inputProjection,
outboxDisplayItems,
apps/mobile/src/session/MessageRenderer.tsx:1840
- 分享选择模式的判据目前是
const shareSelectionActive = actions.shareSelectionActive === true && shareableMessage;,导致非 shareable 的消息在分享模式下仍可能渲染完成态操作条(复制/时间/More 等)。这与同段注释“分享态只保留与导出图片一致的消息内容,不显示操作图标、时间或费用”不一致,也容易在分享模式里出现可交互控件干扰选择/截图预选。建议把“是否处于分享模式”与“该消息是否可分享”拆开,并用全局 share 模式来 gate 操作条。
const shareableMessage = isShareableMessage(item.message);
const handleShareableMessageViewChange = useCallback((view: View | null) => {
actions.onShareableMessageViewChange?.(clientId, view);
}, [actions.onShareableMessageViewChange, clientId]);
const shareSelectionActive = actions.shareSelectionActive === true
&& shareableMessage;
const isFirstUserMessage = item.message.kind === 'user' && clientId === actions.firstUserMessageClientId;
const copyText = buildMobileMessageCopyText(item.message);
const canUseCompletedActions = !isStreamingAssistant;
// 操作行只挂在每轮收尾正文、且该行确实是一条发言(判据见
// mobileMessageShowsActionBar):中间句不再逐条带复制/分叉/时间,系统边界卡整行
// 不挂。分享态只保留与导出图片一致的消息内容,不显示操作图标、时间或费用。
// user 消息、流式「生成中」状态与正文的文本选择(canSelectVisibleText)
// 不受影响。
const showCompletedActionBar = !shareSelectionActive && mobileMessageShowsActionBar({
hasSystemCard: !!item.message.systemCardType,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 47 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
apps/mobile/src/session/userMessageCollapse.ts:103
- truncateTextToVisualLines 目前只把 '\n' 视为换行,但同文件里 LINE_BREAK_RE/estimateTextVisualLineCount 使用的是 /\r\n|\r|\n/。如果消息正文包含 CR 或 CRLF(例如跨平台复制/同步进来的文本),这里的“可见行”截取会与实际折行不一致,可能把本应在收起态隐藏的后续内容带进分享导出。建议统一按 CRLF/CR/LF 处理,并在输出中规范化为 '\n'。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87a073dccf
ℹ️ 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".
Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6306cce6d5
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 47 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
apps/mobile/src/session/mathWebViewHtml.ts:45
- buildKatexLoaderJs 每次调用都会对大体积常量 MOBILE_KATEX_CSS / MOBILE_KATEX_JS 执行 JSON.stringify,并把结果拼进模板字符串。会话里出现多个公式块时会重复做多 MB 字符串的序列化与分配,容易造成 JS 线程卡顿。建议把 stringify 结果提升到模块级常量(只计算一次),loader 内直接复用。
apps/mobile/src/session/mermaidWebViewHtml.ts:36 - buildMermaidLoaderJs 里每次生成 HTML 都会对大体积的 MOBILE_MERMAID_JS 做一次 JSON.stringify。mermaid 图表在消息里可能多次出现,重复序列化多 MB 字符串会造成不必要的分配与卡顿。建议把 stringify 结果提升到模块级常量(只计算一次),loader 内直接复用。
|
@dashhuang 👋 这个 PR 还有 3 条 review conversation 没 resolve(apps/mobile/package.json / apps/mobile/src/session/conversationShareWebViewHtml.ts / apps/mobile/app/sessions/[sessionId].tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19aa2fb93f
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 47 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
apps/mobile/src/session/shareSelectionStore.ts:19
- isShareableMessage 当前只按 kind/systemCardType/orcaCard/hookSource/synthetic trigger 过滤,但不会排除 assistant 流式生成中的消息;在 iOS 截图自动预选路径里,这会把「生成中」的可见气泡也纳入候选集并可能被导出为图片,导致分享内容不完整/与实际 UI 期望不一致。建议把 isStreaming===true 的消息视为不可分享。
Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 47 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
apps/mobile/package.json:65
- 新增
xdt-screenshot-monitor属于原生模块接入,会改变 Mobile runtime fingerprint,按仓库规则需要由指定把关人对“冷更/出包影响”做显式确认后才能合并(不能按普通 OTA 功能发布)。
"xdt-ios-app-distribution": "file:./modules/xdt-ios-app-distribution",
"xdt-mobile-realtime-audio": "file:./modules/xdt-mobile-realtime-audio",
"xdt-screenshot-monitor": "file:./modules/xdt-screenshot-monitor",
"xdt-tapdb": "file:./modules/xdt-tapdb",
apps/mobile/src/session/MessageRenderer.tsx:1839
- 这里用
shareSelectionActive(它被定义为“全局分享态 && 当前消息可分享”)来 gateshowCompletedActionBar,会导致在分享模式下,不可分享的消息仍然会显示操作栏/时间等“非导出内容”,与“分享态只保留导出一致的消息内容”的注释语义不一致。建议改为用全局的actions.shareSelectionActive来统一隐藏操作栏。
const showCompletedActionBar = !shareSelectionActive && mobileMessageShowsActionBar({
MagicLizi
left a comment
There was a problem hiding this comment.
Auto-Review: PR #2188 (heavy tier)
P1 (blocking)
-
冷更指纹变更 (
apps/mobile/modules/xdt-screenshot-monitor/**): 新增原生 Expo Module 会改变 iOS/Android runtime fingerprint,触发冷更。按docs/dev-rules/mobile-development.md冷更边界规则,需仓库指定把关人明确确认后才能合并。PR 已在 Description 标注并说明原因。 -
Android 路径未实机验证 (
apps/mobile/app/sessions/[sessionId].tsx): Android 走 React Native WebView 降级渲染路径(原生 renderConversationShareHtmlToPng 为 iOS-only),包含 SVG→Canvas→PNG 导出管线、WebView postMessage 桥接和 temp 文件写入,仅靠 iOS Simulator 验证存在跨平台风险。需补 Android 实机验证。 -
双模式目检未分别执行 (
ShareSelectionBar.tsx/ConversationShareWebView.tsx/conversationShareWebViewHtml.ts): 代码层已正确使用语义 token(colors.cta/colors.surfaceChip/colors.textPrimary 等),HTML 生成侧传入 dark flag。此条提示性,不阻塞合并。
P2
- Android 临时 PNG 清理跳过逻辑建议加注释说明原因
richContentAssets.generated.ts(~4.36 MB)建议标记为 linguist-generated 或 CI 构建期生成- WebView 导出脚本使用已废弃的
unescape()(WebView 环境限制,无替代方案) - ConversationShareWebView 组件无单元测试(核心数据转换层已有充分覆盖)
核查通过
- CSP 严格:
default-src 'none'; img-src data:; script-src 'unsafe-inline' - WKWebView nonPersistent 存储、allowFileAccess=false
- 用户内容经 redactSensitiveText + escapeHtml 多层转义
- 附件投影显式剥离文件路径和 data: URI
- 无凭证泄露、无数据外发风险
|
冷更影响明确确认如下:
@MagicLizi 冷更影响已明确确认。麻烦你帮忙完成 Android 实机验证,重点覆盖:手动进入分享态、多选消息、生成 PNG、打开系统分享面板,以及接收 App 能正常读取图片。验证后请把结果补到 PR,并重新评审以更新当前 |
MagicLizi
left a comment
There was a problem hiding this comment.
Auto Review 通过
审查完成:0 个 P0/P1 阻断项(冷更新指纹变更是移动端分享截图功能的必要代价,已在 PR 描述中正确记录并由维护者确认门放行)。
2 个 P2 建议:
- Android 端 PNG 临时文件未清理,建议补充清理逻辑或加注释说明
- WebView originWhitelist 可收窄为 做纵深防御
合并后迟到 bot 意见(chatgpt-codex-connector 未表态)以 post-merge sweep 兜底提醒。
MagicLizi
left a comment
There was a problem hiding this comment.
Auto Review 通过
审查完成:0 个 P0/P1 阻断项(冷更新指纹变更是移动端分享截图功能的必要代价,已在 PR 描述中正确记录并由维护者确认门放行)。
2 个 P2 建议:
- Android 端 PNG 临时文件未清理,建议补充清理逻辑或加注释说明
- WebView originWhitelist 可收窄为 about:blank 做纵深防御
合并后迟到 bot 意见(chatgpt-codex-connector 未表态)以 post-merge sweep 兜底提醒。
|
合并啦 🎯 移动端对话分享为图片的 安全实现很扎实——strict CSP + 非持久 WKWebView + 外部图片剥离 + overlay 锁屏激活,每一层都把用户数据守住了。 |
这次改了什么
摘要
为手机版任务对话补齐“分享为图片”能力:用户可以从消息操作栏进入分享模式,勾选多条消息后生成 PNG 并打开系统分享面板;iOS 在任务对话内完成系统截图时,还会自动进入同一套分享模式,并按截图瞬间实际可见面积预选消息。
变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
⋯,代码块和宽表格在导出图中展开,页脚使用 Cindy 角色图与 Logo。WKWebView生成 PNG;缺少原生 renderer 时使用隐藏 React Native WebView 降级。UI 变化
docs/design-rules/DESIGN.md§4 Buttons:全选和分享主操作使用 pill 形态与语义层级。docs/design-rules/DESIGN.md§5 Layout Principles:选择框使用统一间距阶梯,分享栏与消息内容保持清晰层级。docs/design-rules/DESIGN.md§10 Light / Dark Dual-Mode Delivery Gate:颜色全部走ThemeColors语义 token,覆盖默认、选中、按下、禁用和加载态。apps/mobile/docs/mobile-design-guide.md§5 间距 / 圆角 / 触控 / 安全区:可见图标保持紧凑,hitSlop/ 外层命中区补足移动端触控范围。apps/mobile/docs/mobile-design-guide.md§6 图标:复用lucide-react-native的Share图标,与 Desktop 分享动作保持一致。怎么验证的
自动验证
手工验证
com.xd.cindy。/Users/dash/Code/Cindy/cindy-mobile-share-conversation-image。8081,mobile:sim:whoami已确认归属当前 worktree。__DEV__build label:dash/mobile-share-conversation-image@66fc9ba6e+b68a186b61。未执行的验证
origin/main后未重新启动 iOS Simulator;rebase 后已重新通过 Mobile typecheck 与完整单测门禁。风险
风险分类
影响与回滚
UIApplication.userDidTakeScreenshotNotification,并通过非持久化WKWebView将分享 HTML 渲染为 PNG。提交前检查
git commit -s,见 DCO)