diff --git a/apps/mobile/app/sessions/[sessionId].tsx b/apps/mobile/app/sessions/[sessionId].tsx index cb65aaee05..64583e05c7 100644 --- a/apps/mobile/app/sessions/[sessionId].tsx +++ b/apps/mobile/app/sessions/[sessionId].tsx @@ -34,6 +34,7 @@ import { } from 'expo-audio'; import { addScreenshotListener, + renderConversationShareHtmlToPng, } from 'xdt-screenshot-monitor'; import { useFocusEffect, useLocalSearchParams, useNavigation, useRouter } from 'expo-router'; import { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode, type RefObject, type SetStateAction } from 'react'; @@ -100,6 +101,8 @@ import { type ShareableMessageViewport, } from '@/session/MessageRenderer'; import { + bundledAssetToDataUri, + cleanupConversationSharePngTemps, deleteConversationSharePngTemp, writeConversationSharePngTemp, } from '@/session/ConversationShareWebView'; @@ -108,6 +111,7 @@ import { type ConversationShareSvgHandle, } from '@/session/ConversationShareSvg'; import { + buildConversationShareHtml, type ConversationShareMessage, type ConversationShareWebViewColors, } from '@/session/conversationShareWebViewHtml'; @@ -607,6 +611,15 @@ const REOPEN_MESSAGE_WINDOW_LIMITS = [20, 10, 5, 1] as const; // 覆盖 settling 窗口上限(10s)之后仍无任何在途证据的场景。 const TAIL_RETRY_HIDE_TIMEOUT_MS = 15_000; const SCREENSHOT_SHARE_ACTIVATION_DEBOUNCE_MS = 1_200; +const nativeConversationShareAvailable = Platform.OS === 'ios'; + +// 原生 WKWebView 只能稳定读取 data URI;SVG 兜底直接使用同一组 bundle asset。 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const shareCharacterAsset = require('../../assets/share/cindy-share-character.jpg'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const shareLogoLightAsset = require('../../assets/login/login-wordmark.png'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const shareLogoDarkAsset = require('../../assets/login/login-wordmark-dark.png'); /** * 排队消息「复用 composer 编辑」的会话内状态:clientId 定位队列条目, @@ -1001,6 +1014,9 @@ export default function SessionScreen() { const shareOperationSeqRef = useRef(0); const [conversationShareBusy, setConversationShareBusy] = useState(false); const [shareSelectionTriggeredByScreenshot, setShareSelectionTriggeredByScreenshot] = useState(false); + const [shareCharacterSrc, setShareCharacterSrc] = useState(null); + const [shareLogoSrc, setShareLogoSrc] = useState(null); + const shareLogoModeRef = useRef(null); // chat-text-quote:待随下一条消息发送的选中文字引用(全局 store,消息流选区 // 按钮 / 文件预览页写入;发送时拼进正文,命中本地命令时保留)。 const quotes = useSessionQuotes(sessionId); @@ -1080,6 +1096,28 @@ export default function SessionScreen() { }; }, [sessionId]), ); + useEffect(() => { + if (!nativeConversationShareAvailable || !shareSelectionActive) return undefined; + let cancelled = false; + const logoNeedsLoad = shareLogoModeRef.current !== mode || !shareLogoSrc; + void Promise.all([ + shareCharacterSrc + ? Promise.resolve(shareCharacterSrc) + : bundledAssetToDataUri(shareCharacterAsset, 'image/jpeg'), + logoNeedsLoad + ? bundledAssetToDataUri( + mode === 'dark' ? shareLogoDarkAsset : shareLogoLightAsset, + 'image/png', + ) + : Promise.resolve(shareLogoSrc), + ]).then(([character, logo]) => { + if (cancelled) return; + shareLogoModeRef.current = mode; + setShareCharacterSrc(character); + setShareLogoSrc(logo); + }); + return () => { cancelled = true; }; + }, [mode, shareCharacterSrc, shareLogoSrc, shareSelectionActive]); const [composerFocused, setComposerFocused] = useState(false); const [composerInputContentHeight, setComposerInputContentHeight] = useState(COMPOSER_INPUT_SINGLE_LINE_CONTENT_HEIGHT); const [voiceDraftCaretFrame, setVoiceDraftCaretFrame] = useState({ left: 0, top: 0 }); @@ -6000,6 +6038,30 @@ export default function SessionScreen() { textTertiary: colors.textTertiary, dark: mode === 'dark', }), [colors, mode]); + const conversationShareHtml = useMemo(() => { + if ( + !nativeConversationShareAvailable + || !shareSelectionActive + || selectedShareMessages.length === 0 + ) return ''; + return buildConversationShareHtml({ + allShareableIds, + characterSrc: shareCharacterSrc ?? undefined, + colors: conversationShareColors, + contentWidth: windowDimensions.width, + logoSrc: shareLogoModeRef.current === mode ? shareLogoSrc ?? undefined : undefined, + selectedMessages: selectedShareMessages, + }); + }, [ + allShareableIds, + conversationShareColors, + mode, + selectedShareMessages, + shareCharacterSrc, + shareLogoSrc, + shareSelectionActive, + windowDimensions.width, + ]); const enterShareSelection = useCallback((clientId: string) => { Keyboard.dismiss(); setShareSelectionTriggeredByScreenshot(false); @@ -6012,10 +6074,30 @@ export default function SessionScreen() { shareSelectionStore.exit(); }, []); const exportConversationSharePng = useCallback(async () => { + const nativeShareAssetsReady = Boolean( + nativeConversationShareAvailable + && shareCharacterSrc + && shareLogoSrc + && shareLogoModeRef.current === mode, + ); + if (conversationShareHtml && nativeShareAssetsReady) { + try { + const nativeBase64 = await renderConversationShareHtmlToPng({ + html: conversationShareHtml, + width: windowDimensions.width, + }); + if (nativeBase64) { + console.info('[conversation-share] native webview export succeeded'); + return nativeBase64; + } + } catch (error) { + console.warn('[conversation-share] native webview export failed; falling back to svg', error); + } + } const svg = conversationShareSvgRef.current; if (!svg) throw new Error('conversation share svg renderer is unavailable'); return svg.exportPng(); - }, []); + }, [conversationShareHtml, mode, shareCharacterSrc, shareLogoSrc, windowDimensions.width]); const shareSelectedConversation = useCallback(async () => { if ( conversationShareBusy @@ -6030,8 +6112,12 @@ export default function SessionScreen() { && shareSelectionActiveRef.current && shareSelectionRevisionRef.current === operationSelectionRevision; let localUri: string | null = null; + let shareCompleted = false; setConversationShareBusy(true); try { + // 成功分享的 PNG 要保留给系统扩展读取;回收更早的产物,限制 cache + // 目录增长,并不触碰本次尚未生成的文件。 + await cleanupConversationSharePngTemps(); if (!isShareOperationActive()) return; const base64 = await exportConversationSharePng(); if (!isShareOperationActive()) return; @@ -6042,6 +6128,7 @@ export default function SessionScreen() { if (!isShareOperationActive()) return; await sharing.shareAsync(localUri, { mimeType: 'image/png' }); if (!isShareOperationActive()) return; + shareCompleted = true; setShareSelectionTriggeredByScreenshot(false); shareSelectionStore.exit(); } catch (error) { @@ -6049,9 +6136,9 @@ export default function SessionScreen() { console.warn('[conversation-share] failed to generate or open share image', error); Alert.alert(t('session.screen.shareFailedTitle'), t('session.screen.shareImageFailed')); } finally { - if (localUri && Platform.OS !== 'android') { - await deleteConversationSharePngTemp(localUri); - } + // shareAsync 返回后,iOS 分享扩展仍可能继续读取该 URL。成功写入的文件留在 + // cache 目录交给下一次有界清理;失败、取消或中途失活则立即删除当前产物。 + if (!shareCompleted && localUri) await deleteConversationSharePngTemp(localUri); if (shareOperationSeqRef.current === operationSeq) setConversationShareBusy(false); } }, [conversationShareBusy, exportConversationSharePng, selectedShareMessages.length, shareSelectionActive, shareSelectionRevision, t]); diff --git a/apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift b/apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift index a489072e2d..bca472c7a8 100644 --- a/apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift +++ b/apps/mobile/modules/xdt-screenshot-monitor/ios/XdtScreenshotMonitorModule.swift @@ -6,6 +6,7 @@ private let onScreenshot = "onScreenshot" private let conversationShareRenderTimeout: TimeInterval = 20 private let conversationShareMaxOutputPixels: CGFloat = 12_000_000 private let conversationShareMaxSourcePixels: CGFloat = 12_000_000 +private let conversationShareViewportHeight: CGFloat = 760 public class XdtScreenshotMonitorModule: Module { private var screenshotObserver: NSObjectProtocol? @@ -87,6 +88,7 @@ private final class ConversationShareHtmlRenderer: NSObject, WKNavigationDelegat private var completed = false private var timeoutWorkItem: DispatchWorkItem? private var webView: WKWebView? + private var hostingWindow: UIWindow? init( html: String, @@ -101,13 +103,48 @@ private final class ConversationShareHtmlRenderer: NSObject, WKNavigationDelegat } func start() { + guard let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }) + else { + finish(.failure(ConversationShareRenderError("Conversation share renderer has no active window scene."))) + return + } + let configuration = WKWebViewConfiguration() configuration.websiteDataStore = .nonPersistent() - let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: width, height: 1), configuration: configuration) - webView.navigationDelegate = self + let viewportHeight = min(conversationShareViewportHeight, max(1, width * 2)) + let webView = WKWebView( + frame: CGRect(x: 0, y: 0, width: width, height: viewportHeight), + configuration: configuration + ) webView.isOpaque = false - webView.scrollView.isScrollEnabled = false + webView.scrollView.isScrollEnabled = true + webView.scrollView.showsVerticalScrollIndicator = false + webView.scrollView.showsHorizontalScrollIndicator = false + webView.backgroundColor = .clear + + // WKWebView 的离屏 snapshot 仍需要挂在可见的 UIKit window 层级中。 + // 不把它挂到业务页面,避免导出期间改变用户当前页面的布局或焦点。 + let hostingWindow = UIWindow(windowScene: windowScene) + hostingWindow.frame = CGRect(x: 0, y: 0, width: width, height: viewportHeight) + // 放到主窗口上方才能让 WebKit 进入可合成状态;极低透明度避免导出时闪屏。 + // takeSnapshot 直接读取 WKWebView 内容,不会继承 hostingWindow 的透明度。 + hostingWindow.windowLevel = UIWindow.Level(rawValue: UIWindow.Level.normal.rawValue + 1) + hostingWindow.backgroundColor = .clear + hostingWindow.alpha = 0.01 + hostingWindow.isUserInteractionEnabled = false + let viewController = UIViewController() + viewController.view.backgroundColor = .clear + viewController.view.frame = hostingWindow.bounds + webView.frame = viewController.view.bounds + viewController.view.addSubview(webView) + hostingWindow.rootViewController = viewController + hostingWindow.isHidden = false + + webView.navigationDelegate = self self.webView = webView + self.hostingWindow = hostingWindow let timeout = DispatchWorkItem { [weak self] in self?.finish(.failure(ConversationShareRenderError("Conversation share rendering timed out."))) @@ -187,28 +224,171 @@ private final class ConversationShareHtmlRenderer: NSObject, WKNavigationDelegat self.finish(.failure(ConversationShareRenderError("Conversation share content is too large."))) return } - webView.frame = CGRect(x: 0, y: 0, width: captureWidth, height: captureHeight) + guard captureWidth <= webView.bounds.width + 1 else { + self.finish(.failure(ConversationShareRenderError("Conversation share width changed unexpectedly."))) + return + } + let viewportHeight = webView.bounds.height webView.setNeedsLayout() webView.layoutIfNeeded() + self.hostingWindow?.rootViewController?.view.setNeedsLayout() + self.hostingWindow?.rootViewController?.view.layoutIfNeeded() let requestedScale = max(0.25, self.scale) let maxScale = sqrt( conversationShareMaxOutputPixels / max(1, captureWidth * captureHeight) ) let effectiveScale = min(requestedScale, maxScale) let snapshot = WKSnapshotConfiguration() - snapshot.rect = CGRect(x: 0, y: 0, width: captureWidth, height: captureHeight) + snapshot.rect = webView.bounds snapshot.snapshotWidth = NSNumber(value: Double(captureWidth * effectiveScale)) snapshot.afterScreenUpdates = true - webView.takeSnapshot(with: snapshot) { image, error in - if let error { + // 长页面不能用超出 WKWebView.bounds 的 rect 一次截图;按固定视口滚动分片, + // 每片的 rect 始终位于 bounds 内,再在原生侧拼接成完整 PNG。 + CATransaction.flush() + self.captureTiles( + webView: webView, + snapshot: snapshot, + contentWidth: captureWidth, + contentHeight: captureHeight, + viewportHeight: viewportHeight, + effectiveScale: effectiveScale + ) { [weak self] result in + guard let self else { return } + switch result { + case .success(let image): + guard let data = image.pngData(), !data.isEmpty else { + self.finish(.failure(ConversationShareRenderError("Conversation share PNG is empty."))) + return + } + self.finish(.success(data.base64EncodedString())) + case .failure(let error): self.finish(.failure(error)) + } + } + } + } + + private func captureTiles( + webView: WKWebView, + snapshot: WKSnapshotConfiguration, + contentWidth: CGFloat, + contentHeight: CGFloat, + viewportHeight: CGFloat, + effectiveScale: CGFloat, + completion: @escaping (Result) -> Void + ) { + let outputSize = CGSize( + width: contentWidth * effectiveScale, + height: contentHeight * effectiveScale + ) + guard outputSize.width > 0, outputSize.height > 0 else { + completion(.failure(ConversationShareRenderError("Conversation share output is empty."))) + return + } + let tileOffsets: [CGFloat] = { + if contentHeight <= viewportHeight { return [0] } + var offsets = stride(from: CGFloat(0), through: contentHeight - viewportHeight, by: viewportHeight).map { $0 } + let lastOffset = contentHeight - viewportHeight + if offsets.last != lastOffset { offsets.append(lastOffset) } + return offsets + }() + captureTile( + index: 0, + offsets: tileOffsets, + webView: webView, + snapshot: snapshot, + effectiveScale: effectiveScale, + outputSize: outputSize, + tiles: [], + completion: completion + ) + } + + private func captureTile( + index: Int, + offsets: [CGFloat], + webView: WKWebView, + snapshot: WKSnapshotConfiguration, + effectiveScale: CGFloat, + outputSize: CGSize, + tiles: [(offset: CGFloat, image: UIImage)], + completion: @escaping (Result) -> Void + ) { + guard !completed else { return } + guard !offsets.isEmpty else { + completion(.failure(ConversationShareRenderError("Conversation share produced no tiles."))) + return + } + guard index < offsets.count else { + let format = UIGraphicsImageRendererFormat() + // outputSize 已包含 effectiveScale;renderer 再采用屏幕 scale 会把像素数额外 + // 放大 4~9 倍,长图会在合成阶段超时或触发内存压力。 + format.scale = 1 + let renderer = UIGraphicsImageRenderer(size: outputSize, format: format) + let merged = renderer.image { _ in + for tile in tiles { + tile.image.draw(in: CGRect( + x: 0, + y: tile.offset * effectiveScale, + width: outputSize.width, + height: snapshot.rect.height * effectiveScale + )) + } + } + guard merged.hasVisibleVariation else { + completion(.failure(ConversationShareRenderError("Conversation share PNG is blank."))) + return + } + completion(.success(merged)) + return + } + let offset = offsets[index] + webView.scrollView.setContentOffset(CGPoint(x: 0, y: offset), animated: false) + waitForWebContentPaint(webView) { [weak self, weak webView] result in + guard let self, let webView else { return } + guard case .success = result else { + if case .failure(let error) = result { completion(.failure(error)) } + return + } + webView.takeSnapshot(with: snapshot) { [weak self] image, error in + guard let self else { return } + if let error { + completion(.failure(error)) return } - guard let data = image?.pngData(), !data.isEmpty else { - self.finish(.failure(ConversationShareRenderError("Conversation share PNG is empty."))) + guard let image else { + completion(.failure(ConversationShareRenderError("Conversation share tile is empty."))) return } - self.finish(.success(data.base64EncodedString())) + self.captureTile( + index: index + 1, + offsets: offsets, + webView: webView, + snapshot: snapshot, + effectiveScale: effectiveScale, + outputSize: outputSize, + tiles: tiles + [(offset: offset, image: image)], + completion: completion + ) + } + } + } + + private func waitForWebContentPaint( + _ webView: WKWebView, + completion: @escaping (Result) -> Void + ) { + let script = """ + return await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + }); + """ + webView.callAsyncJavaScript(script, arguments: [:], in: nil, in: .page) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) } } } @@ -236,11 +416,56 @@ private final class ConversationShareHtmlRenderer: NSObject, WKNavigationDelegat timeoutWorkItem = nil webView?.stopLoading() webView?.navigationDelegate = nil + webView?.removeFromSuperview() + hostingWindow?.isHidden = true + hostingWindow?.rootViewController = nil + hostingWindow = nil webView = nil completion(result) } } +private extension UIImage { + var hasVisibleVariation: Bool { + guard let cgImage else { return false } + let sampleWidth = 64 + let sampleHeight = 64 + let bytesPerPixel = 4 + let bytesPerRow = sampleWidth * bytesPerPixel + var pixels = [UInt8](repeating: 0, count: sampleHeight * bytesPerRow) + guard let context = CGContext( + data: &pixels, + width: sampleWidth, + height: sampleHeight, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { + return false + } + context.interpolationQuality = .low + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: sampleWidth, height: sampleHeight)) + + var minimum = [Int](repeating: 255, count: 3) + var maximum = [Int](repeating: 0, count: 3) + var hasOpaquePixel = false + for offset in stride(from: 0, to: pixels.count, by: bytesPerPixel) { + guard pixels[offset + 3] > 8 else { continue } + hasOpaquePixel = true + for channel in 0..<3 { + let value = Int(pixels[offset + channel]) + minimum[channel] = min(minimum[channel], value) + maximum[channel] = max(maximum[channel], value) + } + } + guard hasOpaquePixel else { return false } + return zip(minimum, maximum).contains { lower, upper in + upper - lower >= 4 + } + } +} + private struct ConversationShareRenderError: LocalizedError { let message: String diff --git a/apps/mobile/src/__tests__/conversationShareWebViewHtml.test.ts b/apps/mobile/src/__tests__/conversationShareWebViewHtml.test.ts index 3e654e8ce9..4b54186f52 100644 --- a/apps/mobile/src/__tests__/conversationShareWebViewHtml.test.ts +++ b/apps/mobile/src/__tests__/conversationShareWebViewHtml.test.ts @@ -149,7 +149,7 @@ describe('buildConversationShareHtml 富内容导出', () => { expect(html).toContain(`alt="${i18n.t('message.renderer.imageFallbackTitle')}"`); }); - it('限制原生与降级 renderer 的完整源尺寸,并清理一次性 PNG', () => { + it('限制原生与降级 renderer 的完整源尺寸,并安全保留已分享 PNG', () => { const nativeSource = readFileSync( resolve( process.cwd(), @@ -164,9 +164,20 @@ describe('buildConversationShareHtml 富内容导出', () => { const sessionSource = readFileSync( resolve(process.cwd(), 'app/sessions/[sessionId].tsx'), 'utf8', - ); + ).replace(/\r\n/g, '\n'); expect(nativeSource).toContain('conversationShareMaxSourcePixels'); + expect(nativeSource).toContain('UIWindow(windowScene: windowScene)'); + expect(nativeSource).toContain('hostingWindow.rootViewController = viewController'); + expect(nativeSource).toContain('hostingWindow?.isHidden = true'); + expect(nativeSource).toContain('no active window scene'); + expect(nativeSource).toContain('UIWindow.Level.normal.rawValue + 1'); + expect(nativeSource).toContain('hostingWindow.alpha = 0.01'); + expect(nativeSource).toContain('waitForWebContentPaint(webView)'); + expect(nativeSource).toContain('requestAnimationFrame(resolve)'); + expect(nativeSource).toContain('merged.hasVisibleVariation'); + expect(nativeSource).toContain('Conversation share PNG is blank.'); + expect(nativeSource).toContain('format.scale = 1'); expect(nativeSource).toContain( 'captureWidth * captureHeight <= conversationShareMaxSourcePixels', ); @@ -174,8 +185,34 @@ describe('buildConversationShareHtml 富内容导出', () => { expect(webViewSource).toContain( 'await deleteConversationSharePngTemp(file.uri);', ); - expect(sessionSource).toContain("localUri && Platform.OS !== 'android'"); + expect(webViewSource).toContain('SHARE_PNG_RETAIN_COUNT = 3'); + expect(webViewSource).toContain('SHARE_PNG_CLEANUP_BATCH = 8'); + expect(webViewSource).toContain( + 'files.slice(SHARE_PNG_RETAIN_COUNT, SHARE_PNG_RETAIN_COUNT + SHARE_PNG_CLEANUP_BATCH)', + ); + expect(sessionSource).toContain('deleteConversationSharePngTemp'); + expect(sessionSource).toContain('cleanupConversationSharePngTemps'); + expect(sessionSource).toContain('cache 目录交给下一次有界清理'); + expect(sessionSource).toContain('if (!shareCompleted && localUri)'); expect(sessionSource).toContain(' { diff --git a/apps/mobile/src/session/ConversationShareWebView.tsx b/apps/mobile/src/session/ConversationShareWebView.tsx index 9e8af37b3e..ee1e7b449a 100644 --- a/apps/mobile/src/session/ConversationShareWebView.tsx +++ b/apps/mobile/src/session/ConversationShareWebView.tsx @@ -15,6 +15,11 @@ import { interceptHtmlNavigation } from "@/session/htmlNavigationPolicy"; const EXPORT_TIMEOUT_MS = 20_000; const EXPORT_SCALE = 2; const EXPORT_DIR_NAME = "conversation-share"; +// 分享扩展在 shareAsync 返回前后都可能读取 URL。保留最近几份成功产物, +// 下次开始分享时再回收更早的文件,避免成功预览与清理竞态,同时限制 cache +// 目录无限增长。 +const SHARE_PNG_RETAIN_COUNT = 3; +const SHARE_PNG_CLEANUP_BATCH = 8; export interface ConversationShareWebViewHandle { exportPng(options?: { scale?: number }): Promise; @@ -266,6 +271,38 @@ export async function writeConversationSharePngTemp( } } +/** + * 回收消息分享产生的旧 PNG。 + * + * 只在下一次分享开始前调用,当前这次刚写入的文件不会被删除;保留最近 + * 几份成功产物给系统分享扩展读取,旧文件按批次回收。清理是 best-effort, + * 不能影响当前分享流程。 + */ +export async function cleanupConversationSharePngTemps(): Promise { + try { + const directory = new Directory(Paths.cache, EXPORT_DIR_NAME); + if (!directory.exists) return; + const files = directory + .list() + .filter( + (entry): entry is File => + entry instanceof File + && entry.name.startsWith("conversation-") + && entry.extension.toLowerCase() === ".png" + && entry.exists, + ) + .sort( + (left, right) => + (right.modificationTime ?? right.creationTime ?? 0) + - (left.modificationTime ?? left.creationTime ?? 0), + ); + const stale = files.slice(SHARE_PNG_RETAIN_COUNT, SHARE_PNG_RETAIN_COUNT + SHARE_PNG_CLEANUP_BATCH); + await Promise.all(stale.map((file) => deleteConversationSharePngTemp(file.uri))); + } catch { + // 一次性缓存清理是 best-effort,不覆盖当前分享操作。 + } +} + export async function deleteConversationSharePngTemp(uri: string): Promise { if (!uri) return; try {