Files
apes-sidebar/src/client/builtins/viewers.tsx
T
Menghuan1918 302c20109f feat: 内置 HTML 预览器 + 沙箱浏览器 tab(v0.5.0)
- html viewer:.html/.htm 沙箱 iframe 预览(相对资源经 /sidebar/html 路径编码路由加载,CSP sandbox + 信任围栏 + cwd 边界),预览/编辑切换
- browser tab:+ 菜单多开(browser:<n>),地址栏后退/前进/刷新/在浏览器中打开(15px 图标),URL 与标题按会话持久化(patchTab + nextBrowser)
- 安全:iframe 一律不透明源沙箱(无 allow-same-origin/top-navigation、no-referrer、权限策略全禁、禁 srcdoc);地址栏仅 http(s)、拒绝 localhost 等本机地址;界面实时沙箱状态(关闭红色警示)+ 临时解锁/恢复
- 设置:htmlViewerNoSandbox / htmlViewerDefaultUnsafe / browserNoSandbox / browserInterceptLinks 四个声明式二级设置(默认安全)
- 外链拦截:聊天/界面 http(s) 外链默认在侧边栏打开(openTab seed.url),Ctrl/Cmd 点击放行
- 嵌入拒绝面板:browser.probe 路由探测 X-Frame-Options/frame-ancestors,被拒站点显示原因 + 在浏览器中打开/仍然加载
- 设置页可滚动;内置 7 tab + 9 viewer;新增设计文档;268 个测试全绿
2026-08-11 19:28:48 +08:00

140 lines
4.8 KiB
TypeScript

/**
* The 9 built-in file viewer descriptors: every preview surface is a
* registered viewer (image / pdf / docx / xlsx / pptx / markdown / html /
* code / binary-download), exactly like external plugins register theirs.
* The `binary-download` viewer sniffs NUL bytes via `detect` for unknown
* binaries and serves doc/xls/ppt by extension; `code` is the catch-all
* (`exts: []`, lowest priority) that claims any file no other viewer did.
*
* Every viewer carries the declarative settings-surface fields — `title`
* and `icon` — so the Side card settings page can render the enable/disable
* inventory without hardcoding (eating our own dogfood).
*/
import { IconCodeOutline16, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import { DocxView, XlsxView } from '../office-view.tsx'
import { PdfView } from '../PdfView.tsx'
import { PptxView } from '../PptxView.tsx'
import { TextEditor } from '../TextEditor.tsx'
import { BinaryDownload } from '../binary-download.tsx'
import {
IconImageOutline16,
IconMarkdownOutline16,
IconPdfOutline16,
IconDocxOutline16,
IconXlsxOutline16,
IconPptxOutline16,
IconHtmlOutline16,
} from '../icons.tsx'
import type { FileViewerDescriptor } from '../service.ts'
import { t } from '../locales.ts'
import css from '../sidebar.module.css'
/** The 9 built-in file viewer descriptors. */
export function builtinViewers(): readonly FileViewerDescriptor[] {
return [
{
id: 'image',
title: () => t('viewerImage'),
icon: (size: number) => <IconImageOutline16 size={size} />,
exts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'],
fetchStrategy: 'mediaUrl',
component: ({ mediaUrl: url, title }) => (
<div className={css.editorImageWrap}>
<img className={css.editorImage} src={url} alt={title} />
</div>
),
},
{
id: 'pdf',
title: () => t('viewerPdf'),
icon: (size: number) => <IconPdfOutline16 size={size} />,
exts: ['pdf'],
fetchStrategy: 'mediaUrl',
component: ({ scope, path, title }) => (
<PdfView scope={scope} path={path} title={title} />
),
},
{
id: 'docx',
title: () => t('viewerDocx'),
icon: (size: number) => <IconDocxOutline16 size={size} />,
exts: ['docx'],
fetchStrategy: 'mediaUrl',
component: ({ scope, path, title }) => (
<DocxView scope={scope} path={path} title={title} />
),
},
{
id: 'xlsx',
title: () => t('viewerXlsx'),
icon: (size: number) => <IconXlsxOutline16 size={size} />,
exts: ['xlsx'],
fetchStrategy: 'mediaUrl',
component: ({ scope, path, title }) => (
<XlsxView scope={scope} path={path} title={title} />
),
},
{
id: 'pptx',
title: () => t('viewerPptx'),
icon: (size: number) => <IconPptxOutline16 size={size} />,
exts: ['pptx'],
fetchStrategy: 'mediaUrl',
component: ({ scope, path, title }) => (
<PptxView scope={scope} path={path} title={title} />
),
},
{
id: 'markdown',
title: () => t('viewerMarkdown'),
icon: (size: number) => <IconMarkdownOutline16 size={size} />,
exts: ['md', 'markdown'],
fetchStrategy: 'fsRead',
component: (props) => <TextEditor {...props} />,
},
{
id: 'html',
title: () => t('viewerHtml'),
icon: (size: number) => <IconHtmlOutline16 size={size} />,
exts: ['html', 'htm'],
fetchStrategy: 'fsRead',
// Declarative settings: the sandbox escape hatch and the default-unsafe
// start state render under this viewer's row in the Side card settings
// page (both warned on).
settings: {
toggles: [{
key: 'htmlViewerNoSandbox',
title: () => t('settingsHtmlSandboxTitle'),
desc: () => t('settingsHtmlSandboxDesc'),
}, {
key: 'htmlViewerDefaultUnsafe',
title: () => t('settingsHtmlDefaultUnsafeTitle'),
desc: () => t('settingsHtmlDefaultUnsafeDesc'),
}],
},
component: (props) => <TextEditor {...props} />,
},
{
id: 'code',
title: () => t('viewerCode'),
icon: (size: number) => <IconCodeOutline16 size={size} />,
exts: [],
priority: -100,
fetchStrategy: 'fsRead',
component: (props) => <TextEditor {...props} />,
},
{
id: 'binary-download',
title: () => t('viewerBinary'),
icon: (size: number) => <IconDownloadOutline16 size={size} />,
exts: ['doc', 'xls', 'ppt'],
priority: -50,
fetchStrategy: 'binary-download',
// NUL probe: a file whose head bytes contain a NUL is binary — claimed
// before the catch-all code viewer on the head re-match.
detect: (_path, head) => head.includes(0),
component: ({ scope, path }) => <BinaryDownload scope={scope} path={path} />,
},
]
}