feat: add Office, PDF, and PPTX previews

This commit is contained in:
HuanLinOTO
2026-08-10 16:46:01 +08:00
parent 4060bd53e1
commit a6befc8b6b
23 changed files with 5158 additions and 11 deletions
+24
View File
@@ -42,6 +42,20 @@ const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as P
/** The registry's strict id validation (manifest.ts / client-modules registerExternal): exactly two lowercase slash-separated segments. */
const ID_PATTERN = /^(?!node_modules\/)(?:@[a-z0-9][a-z0-9-.]*\/[a-z0-9][a-z0-9-.]*|[a-z0-9][a-z0-9-.]*\/[a-z0-9][a-z0-9-.]*)$/
/** Literal require() specifiers the frozen browser module table can answer. */
const CLIENT_REQUIRE_ALLOWED = new Set([
'react',
'react/jsx-runtime',
'react-dom',
'react-dom/client',
'cordis',
'@deepseek-ai/dsh-client-ui-slots',
'@deepseek-ai/dsh-client-web-react',
'@deepseek-ai/dsh-client-ui-primitives',
'@deepseek-ai/dsh-client-schema-form',
'@deepseek-ai/dsh-client-runtime/client',
])
/** The registered `__ModuleLoader__.load({ id })` value of a built client bundle. */
function bundleId(file: string): string {
const source = readFileSync(resolve(ROOT, file), 'utf8')
@@ -72,6 +86,16 @@ describe('registry manifest consistency (dsh.plugin.json)', () => {
expect(registryId).not.toBe(pkg.name)
})
it('client bundles require only frozen module-table entries', () => {
for (const file of ['lib/client.js', manifest.client!.main!]) {
const source = readFileSync(resolve(ROOT, file), 'utf8')
// Exclude method calls such as `freeModule.require("util")`; only the
// loader factory's lexical require() is constrained by the module table.
const required = [...source.matchAll(/(^|[^.$\w])require\("([^"]+)"\)/gm)].map(match => match[2]!)
expect([...new Set(required)].filter(id => !CLIENT_REQUIRE_ALLOWED.has(id))).toEqual([])
}
})
it('contributes declares no tools or skills (the host half registers none)', () => {
expect(manifest.contributes?.tools).toEqual([])
expect(manifest.contributes?.skills).toEqual([])
+6 -1
View File
@@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest'
import { tmpdir } from 'node:os'
import { resolve as resolvePath } from 'node:path'
import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { apply } from '../src/index.ts'
import { apply, mediaTypeForPath } from '../src/index.ts'
import * as git from '../src/git.ts'
import { listDirectory } from '../src/fs-tree.ts'
import { defaultShell, PtyManager } from '../src/pty-manager.ts'
@@ -27,6 +27,11 @@ interface FakeContext {
}
describe('host plugin smoke', () => {
it('serves PDF with the browser-native content type', () => {
expect(mediaTypeForPath('/work/report.PDF')).toBe('application/pdf')
expect(mediaTypeForPath('/work/archive.bin')).toBe('application/octet-stream')
})
it('mounts the fenced routes', () => {
const routes: SidebarWebRoute[] = []
const upgrades: SidebarWebUpgradeRoute[] = []
+33
View File
@@ -9,6 +9,8 @@ import {
import { loadPrefs, type SidebarSettingsClient } from '../src/client/prefs.ts'
import { SIDEBAR_PREFS_DEFAULTS } from '../src/prefs-shared.ts'
import { extOf, languageKeyForExt } from '../src/client/lang.ts'
import { officeKindForExt } from '../src/client/office-types.ts'
import { isPdfExt } from '../src/client/pdf-types.ts'
import { relativeTo } from '../src/client/paths.ts'
import { producedForClosing, resolveSidebarPath, selectProducedFiles } from '../src/client/produced-files.ts'
import { defaultShell, ensureSpawnHelper } from '../src/pty-manager.ts'
@@ -453,6 +455,37 @@ describe('editor language mapping', () => {
})
})
describe('office preview kind', () => {
it('routes docx/xlsx to their renderers', () => {
expect(officeKindForExt('.docx')).toBe('docx')
expect(officeKindForExt('.xlsx')).toBe('xlsx')
})
it('routes pptx to its renderer and legacy OLE formats to download-only', () => {
expect(officeKindForExt('.pptx')).toBe('pptx')
expect(officeKindForExt('.doc')).toBe('download-only')
expect(officeKindForExt('.xls')).toBe('download-only')
expect(officeKindForExt('.ppt')).toBe('download-only')
})
it('returns null for non-Office extensions and empty input', () => {
expect(officeKindForExt('.txt')).toBeNull()
expect(officeKindForExt('.md')).toBeNull()
expect(officeKindForExt('.png')).toBeNull()
expect(officeKindForExt('.pdf')).toBeNull()
expect(officeKindForExt('')).toBeNull()
})
})
describe('pdf preview kind', () => {
it('routes only .pdf to the browser-native preview', () => {
expect(isPdfExt('.pdf')).toBe(true)
expect(isPdfExt('.PDF')).toBe(false)
expect(isPdfExt('.docx')).toBe(false)
expect(isPdfExt('')).toBe(false)
})
})
describe('side card preferences', () => {
/** A fake settings wire face whose settingsGet resolves to one raw value. */
const wire = (value: unknown): SidebarSettingsClient => ({