mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
6bab6fbe97
Documents the agent writes open in the workspace panel instead of another application, and local images the agent mentions show up in the conversation. Workspace preview - PDF (pdf.js with its own layout and text layer), Word (docx-preview inside a scripts-disabled sandboxed iframe) and Excel (SheetJS; .xlsx, .xlsm, .xls) open in the side panel with zoom and fit, per-file scroll/zoom/sheet memory, and a refresh when the agent rewrites the file. The engines load lazily. - Bytes come from a new GET /api/sessions/:id/workspace/raw route, with an extension allowlist, size caps, the workspace boundary and canonical-path checks. The file endpoint returns metadata and a version for documents. The client fetches with the bearer credential, so it works in Electron, LAN H5 and remote access alike. - Chat links, output cards and the change card open pdf/docx/xlsx in the workspace; documents outside the workdir still go to the system application. - Image viewer with fit, zoom and pan, and "open in system app". Chat images - Markdown images outside the workdir, at ~/, C:\ and file:// paths render, open in the viewer, and offer "open original" (pictures only). - Images returned by tools such as Read appear as thumbnails under the call. Hardening found in review - previewFsUrl escapes each path segment; a double-escaped %2e%2e used to leave /preview-fs/<session>/. - The CORS, API timing and remote-access header decorators set headers in place. Rebuilding the response buffered whole files in memory and dropped Content-Length. - The engine owns the pdf.js worker, so closing one document no longer fails the next open. - Office archives are inflated in steps to check their real sizes, not the sizes they declare. - A viewer that fails to load stays in its panel instead of taking the window down. Adds pdfjs-dist, docx-preview, xlsx (SheetJS 0.20.3 tarball) and fflate as renderer dev dependencies; Vite bundles them. Refs #1397
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
import fs from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import path from 'node:path'
|
|
import type { Plugin } from 'vite'
|
|
import { PDFJS_ASSET_DIRS, pdfjsAssetsPrefix } from '../src/components/workspace/surfaces/document/pdfAssets'
|
|
|
|
/**
|
|
* Not shipped: pdf.js' scripting sandbox (JavaScript embedded in PDF forms). A
|
|
* read-only preview never runs it, and it is a sizeable wasm the app would carry
|
|
* for nothing.
|
|
*/
|
|
const EXCLUDED = /^wasm\/quickjs-eval\./
|
|
|
|
const MIME_TYPES: Record<string, string> = {
|
|
'.wasm': 'application/wasm',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.ttf': 'font/ttf',
|
|
}
|
|
|
|
/** Every file to ship, as `dir/name` paths relative to the pdfjs-dist package. */
|
|
export function listPdfjsAssets(packageDir: string): string[] {
|
|
return PDFJS_ASSET_DIRS.flatMap((dir) => {
|
|
let names: string[]
|
|
try {
|
|
names = fs.readdirSync(path.join(packageDir, dir))
|
|
} catch (error) {
|
|
throw new Error(
|
|
`pdfjs-dist has no "${dir}" folder at ${packageDir}. This version lays its data out differently; `
|
|
+ 'update PDFJS_ASSET_FOLDERS in pdfAssets.ts to match.',
|
|
{ cause: error },
|
|
)
|
|
}
|
|
return names.map((name) => `${dir}/${name}`).filter((relative) => !EXCLUDED.test(relative))
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Ships pdf.js' run-time data with the app, and serves it from the dev server at
|
|
* the same URL, so the renderer finds it identically in `vite dev`, in the
|
|
* packaged Electron app (`file://` inside the asar) and on the H5 static host.
|
|
*
|
|
* `packageDir` is for tests; by default the installed pdfjs-dist is used.
|
|
*/
|
|
export function pdfjsAssets(options: { packageDir?: string } = {}): Plugin {
|
|
const packageDir = options.packageDir
|
|
?? path.dirname(createRequire(import.meta.url).resolve('pdfjs-dist/package.json'))
|
|
const { version } = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as { version: string }
|
|
const prefix = pdfjsAssetsPrefix(version)
|
|
|
|
return {
|
|
name: 'cc-haha:pdfjs-assets',
|
|
|
|
configureServer(server) {
|
|
server.middlewares.use(`/${prefix}`, (request, response, next) => {
|
|
let relative: string
|
|
try {
|
|
relative = decodeURIComponent((request.url ?? '/').split('?')[0]!).replace(/^\/+/, '')
|
|
} catch {
|
|
next() // a malformed escape is not one of our files
|
|
return
|
|
}
|
|
const file = path.resolve(packageDir, relative)
|
|
// Only the data folders: nothing else in the package, and no way out of it.
|
|
const inDataDir = PDFJS_ASSET_DIRS.some((dir) => file.startsWith(path.join(packageDir, dir) + path.sep))
|
|
if (!inDataDir || EXCLUDED.test(path.relative(packageDir, file).split(path.sep).join('/')) || !fs.existsSync(file)) {
|
|
next()
|
|
return
|
|
}
|
|
response.setHeader('Content-Type', MIME_TYPES[path.extname(file)] ?? 'application/octet-stream')
|
|
response.setHeader('Cache-Control', 'no-cache')
|
|
fs.createReadStream(file).pipe(response)
|
|
})
|
|
},
|
|
|
|
generateBundle() {
|
|
for (const relative of listPdfjsAssets(packageDir)) {
|
|
this.emitFile({
|
|
type: 'asset',
|
|
// A fixed name, not a content hash: the folder already carries the version.
|
|
fileName: prefix + relative,
|
|
source: fs.readFileSync(path.join(packageDir, relative)),
|
|
})
|
|
}
|
|
},
|
|
}
|
|
}
|