Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
219aed6
feat(mobile): HTML 渲染态透传同目录资源,多文件产物不再缺图缺样式
zqchris Aug 3, 2026
9db6885
fix: address review — 资源改内联 data URI + CSP 断网络出口,取件补 SSH 上下文
zqchris Aug 3, 2026
f8171fd
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
3aa8bad
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
b236dd7
fix: address review — CSP 前置到任何作者内容之前,SVG fragment 回填保留
zqchris Aug 3, 2026
4267245
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
02a9b0e
fix: address review — 整页内联加总量预算,防不可信产物撑爆内存
zqchris Aug 3, 2026
2e0a3df
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
1d1e994
fix: address review — 资源取件后回收 OSS 对象,失败路径同样回收
zqchris Aug 3, 2026
fa4ed6b
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
11babf8
fix: address review — 预览零出网信道、惰性文本不占配额、OSS 失败窗口回收、提示分开两类
zqchris Aug 3, 2026
eff3e49
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
4d598a3
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
9cc0804
fix: address review — 模板体不占配额、下载前判上限、预算按回填倍数计费
zqchris Aug 3, 2026
d44452d
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
db46ead
fix: address review — 未闭合标记不再掩到文末;iframe/embed 不取回
zqchris Aug 3, 2026
7cdf3b0
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
f077b88
fix: address review — ossKey 在回包校验之前就交给调用方(零字节文件也会遗留对象)
zqchris Aug 3, 2026
64127cb
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
c7293b3
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
982363a
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
21a11d7
fix: address review — 删掉掩码层、收 style 属性 url()、解码字符引用、SSH 白名单同步、提示改口径
zqchris Aug 3, 2026
f44d871
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
ae008af
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
1f28d14
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
d9d92f2
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
7b86300
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
7a388a1
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
132e34e
Merge branch 'mobile-html-preview' into mobile-html-resource-passthrough
zqchris Aug 3, 2026
584e784
fix(mobile): 资源扫描跳过 RAWTEXT 内容,并修四处有界的识别缺陷
zqchris Aug 3, 2026
2111c1c
fix(mobile): image 白名单补 src —— HTML 里 <image> 是 <img> 的废弃别名
zqchris Aug 3, 2026
44837f4
perf(mobile): 跳过区间判定改二分,消掉自己引入的 O(n·m) DoS 面
zqchris Aug 3, 2026
1067881
fix(mobile): style 体也跳过标签扫描;Windows 路径判定两处统一到一份实现
zqchris Aug 3, 2026
330d2e5
test(mobile): 钉住两次 RAWTEXT span 扫描不可合并成一次+filter
zqchris Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 77 additions & 4 deletions apps/mobile/app/files/preview/[sessionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* OSS 导出原图就绪后无缝换源(不出 loading 态,规则 7);其它 = 占位 + 下载。
* markdown 与 HTML 额外有「渲染 / 源码」双态,默认渲染:两者都只用已读到的那份文本
* (不为渲染多走一遍 OSS 导出),载体分别是 MarkdownFileReader 与 HtmlFileReader。
* HTML 再多一步同目录资源透传:页面引用的相对资源经 media:fetch 逐个取回后回填
* (htmlLocalResources + useHtmlLocalResources),自包含页面零请求直接过。
*
* absPath 单文件模式(route 参 absPath,与 relPath 互斥):聊天 chip 指向
* workdir 外文件时进入。file-browser 的 relPath 通道(listDir / readFile /
Expand Down Expand Up @@ -52,6 +54,8 @@ import { appendQuote, truncateQuoteText } from '@/session/chatQuoteStore';
import { getCachedPreviewText, storeCachedPreviewText } from '@/session/fileBrowserCache';
import { exportRemoteFileToUrl } from '@/session/fileBrowserExport';
import { HtmlFileReader } from '@/session/HtmlFileReader';
import { HTML_RESOURCE_LIMIT, htmlBaseDirOf } from '@/session/htmlLocalResources';
import { useHtmlLocalResources } from '@/session/useHtmlLocalResources';
import { MarkdownFileReader } from '@/session/MarkdownFileReader';
import { RemoteMediaPlayerWebView } from '@/session/mediaPlayerWebView';
import {
Expand Down Expand Up @@ -323,6 +327,20 @@ export default function RemoteFilePreviewScreen() {
[deviceId, maker, openLink, presignGet, singleAbsPath, workdir],
);

/**
Comment thread
greptile-apps[bot] marked this conversation as resolved.
* 任意被控端绝对路径 → presign 地址(HTML 渲染态取同目录资源用)。
*
* 与 exportToUrl 的区别:后者只服务「当前这个文件」(workdir 内走两段式导出、
* absPath 模式走单一路径取件);资源透传要取的是**页面引用的其它路径**,所以
* 统一走 media:fetch 的绝对路径通道 —— 它对 workdir 内外一视同仁,一条路径一条码。
* 路径合法性(必须是 HTML 所在目录子树内的相对引用)已在 htmlLocalResources
* 里 fail-closed 判定,这里不再重复。
*/
const fetchResourceUrl = useCallback(
(absPath: string) => fetchRemoteAbsFileToUrl({ maker, deviceId, openLink, presignGet }, absPath),
[deviceId, maker, openLink, presignGet],
Comment thread
zqchris marked this conversation as resolved.
Outdated
);

// 文本预览读文件也走瞬断重试 + openLink(与列表/搜索/导出同一路径),
// relay 短暂重连不再把预览页打成「读取失败」。
// absPath 单文件模式走 text-file:read-preview(被控端绝对路径文本通道),
Expand Down Expand Up @@ -436,8 +454,10 @@ export default function RemoteFilePreviewScreen() {
renderItem={({ item, index }) => (
<View style={{ width: pageWidth }}>
<FilePreviewPage
absolutePathOf={absolutePathOf}
active={Math.abs(index - pageIndex) <= 1}
exportToUrl={exportToUrl}
fetchResourceUrl={fetchResourceUrl}
item={item}
maker={maker}
onDownload={() => void downloadAndShare(item)}
Expand Down Expand Up @@ -541,8 +561,10 @@ function PreviewNav({
}

function FilePreviewPage({
absolutePathOf,
active,
exportToUrl,
fetchResourceUrl,
item,
maker,
onDownload,
Expand All @@ -553,8 +575,10 @@ function FilePreviewPage({
targetLine,
workdir,
}: {
absolutePathOf(relPath: string): string;
active: boolean;
exportToUrl(relPath: string, mtimeMs: number): Promise<string>;
fetchResourceUrl(absPath: string): Promise<string>;
item: FileBrowserGridItem;
maker: Pick<MobileMakerTransport, 'fileBrowser'>;
onDownload(): void;
Expand Down Expand Up @@ -587,7 +611,19 @@ function FilePreviewPage({
return <AvPreviewPage active={active} exportToUrl={exportToUrl} item={item} kind={avKind} onDownload={onDownload} workdir={workdir} />;
}
if (item.thumb === 'doc') {
return <TextPreviewPage active={active} item={item} onDownload={onDownload} onQuoteSelection={onQuoteSelection} readTextFile={readTextFile} targetLine={targetLine} workdir={workdir} />;
return (
<TextPreviewPage
absolutePathOf={absolutePathOf}
active={active}
fetchResourceUrl={fetchResourceUrl}
item={item}
onDownload={onDownload}
onQuoteSelection={onQuoteSelection}
readTextFile={readTextFile}
targetLine={targetLine}
workdir={workdir}
/>
);
}
return <UnsupportedPage item={item} onDownload={onDownload} reason={t('files.preview.unsupportedType')} />;
}
Expand Down Expand Up @@ -737,15 +773,21 @@ function PdfPreviewPage({
* markdown / HTML(richTextKindOf)多一层「渲染 / 源码」切换,渲染态复用同一份已读文本。
*/
function TextPreviewPage({
absolutePathOf,
active,
fetchResourceUrl,
item,
onDownload,
onQuoteSelection,
readTextFile,
targetLine,
workdir,
}: {
/** item.relPath → 被控端绝对路径(HTML 资源透传要据此定位同目录)。 */
absolutePathOf(relPath: string): string;
active: boolean;
/** 任意绝对路径 → presign 地址(HTML 渲染态取同目录资源)。 */
fetchResourceUrl(absPath: string): Promise<string>;
item: FileBrowserGridItem;
onDownload(): void;
/** chat-text-quote:markdown 渲染态的选中引用回调(源码态暂不支持,见 PR 说明)。 */
Expand Down Expand Up @@ -830,6 +872,23 @@ function TextPreviewPage({
};
}, [active, cacheable, item.relPath, richKind, readTextFile, t, workdir]);

// HTML 资源透传:页面引用的同目录资源取回后回填,自包含页面零请求直接过。
// hook 必须在下面的早返回之前无条件调用 —— 未就绪时传空串,内部即刻短路。
const htmlSource = richKind === 'html' && state.status === 'ready' ? (state.content ?? '') : '';
const htmlBaseDir = useMemo(
() => (htmlSource ? htmlBaseDirOf(absolutePathOf(item.relPath)) : ''),
Comment thread
zqchris marked this conversation as resolved.
[absolutePathOf, htmlSource, item.relPath],
);
const htmlResources = useHtmlLocalResources(htmlSource, htmlBaseDir, fetchResourceUrl);
const resourceNotices = [
htmlResources.failed > 0
? t('files.preview.htmlResourcesMissing', { count: htmlResources.failed })
: null,
htmlResources.skipped > 0
? t('files.preview.htmlResourcesTruncated', { limit: HTML_RESOURCE_LIMIT })
: null,
Comment thread
zqchris marked this conversation as resolved.
Outdated
Comment thread
zqchris marked this conversation as resolved.
].filter((line): line is string => line !== null);

if (state.status === 'loading') {
return (
<View style={styles.centerFill}>
Expand Down Expand Up @@ -875,11 +934,25 @@ function TextPreviewPage({
))}
</View>
) : null}
{showRendered && richKind === 'html' && resourceNotices.length > 0 ? (
<View style={styles.truncBar} testID="filePreview.htmlResourceNotice">
<Info color={colors.textSecondary} size={iconSize.sm} strokeWidth={iconStroke.regular} />
<Text style={styles.truncText}>{resourceNotices.join(' · ')}</Text>
</View>
) : null}
{showRendered ? (
richKind === 'html' ? (
// HTML 生成物:已读到的文本直接进 WebView(不走 OSS 导出),同目录相对资源
// 取不到是已知边界,见 HtmlFileReader 头注。
<HtmlFileReader html={state.content ?? ''} testID="filePreview.htmlRendered" />
htmlResources.loading ? (
// 取件期间不先渲染破图再热替换 —— 那会让 WebView 重载、页面闪一下。
<View style={styles.centerFill} testID="filePreview.htmlResourceLoading">
<ActivityIndicator color={colors.textTertiary} />
<Text style={styles.hintText}>{t('files.preview.fetchingHtmlResources')}</Text>
</View>
) : (
// HTML 生成物:文本 + 回填后的同目录资源地址直接进 WebView(不走 OSS
// 两段式导出)。仍取不到的引用保留原样,渲染成破图,见 HtmlFileReader 头注。
<HtmlFileReader html={htmlResources.html} testID="filePreview.htmlRendered" />
)
) : (
<MarkdownFileReader markdown={state.content ?? ''} onQuoteSelection={onQuoteSelection} targetLine={targetLine} testID="filePreview.markdownRendered" />
)
Expand Down
18 changes: 15 additions & 3 deletions apps/mobile/src/__tests__/filePreviewPagerWiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,26 @@ describe('HTML 渲染态的 WebView 约束', () => {
});

describe('HTML 生成物的渲染态接线', () => {
it('渲染态复用已读文本,不为 HTML 另走一遍 OSS 导出', () => {
// 取件通道保持一条:richKind 非空时才留原文,渲染态直接把它喂 HtmlFileReader
it('文档正文复用已读文本,不为 HTML 另走一遍 OSS 两段式导出', () => {
// 取件通道保持一条:richKind 非空时才留原文,渲染态用的就是它(经资源回填)
expect(source).toContain('content: richKind ? content : undefined');
expect(source).toContain("<HtmlFileReader html={state.content ?? ''}");
expect(source).toContain('<HtmlFileReader html={htmlResources.html}');
// HTML 不得混进 exportToUrl 那条(图片 / PDF / 音视频 / 下载共用的)导出链路。
expect(source).not.toMatch(/HtmlFileReader[^>]*exportToUrl/);
});

it('同目录资源走 media:fetch 绝对路径通道,不复用 exportToUrl', () => {
// exportToUrl 只服务「当前这个文件」;资源要取的是页面引用的其它路径。
expect(source).toContain('const htmlResources = useHtmlLocalResources(htmlSource, htmlBaseDir, fetchResourceUrl)');
expect(source).toContain('fetchRemoteAbsFileToUrl({ maker, deviceId, openLink, presignGet }, absPath)');
});

it('资源被跳过 / 取不到时如实提示,不静默截断', () => {
expect(source).toContain("t('files.preview.htmlResourcesMissing'");
expect(source).toContain("t('files.preview.htmlResourcesTruncated'");
expect(source).toContain('testID="filePreview.htmlResourceNotice"');
});

it('markdown 与 HTML 共用同一套双态机(不再是 markdown 专用)', () => {
expect(source).toContain("const richKind = richTextKindOf(item.name)");
expect(source).toContain("useState<'rendered' | 'source'>(richKind ? 'rendered' : 'source')");
Expand Down
Loading
Loading