- 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 个测试全绿
140 lines
4.8 KiB
TypeScript
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} />,
|
|
},
|
|
]
|
|
}
|