import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { HtmlWrapper, resolveTemplateStyle } from '@karinjs/template-react' import React from 'react' import { renderToString } from 'react-dom/server' import { PreviewLayout } from './components/PreviewLayout' import type { PreviewState } from './types' type VideoPreviewRenderOptions = { filename: string filePath: string videoUrl: string removeCache: boolean createdAt: number expireAt?: number eventsUrl?: string } type VideoPreviewState = PreviewState & { serverNow: number } /** * 向上定位 koishi-plugin-kkk 包根目录。 * 生产环境当前模块在 lib/core_chunk 内,向上两级即包根; * 开发环境本文件在 packages/template/src/_preview,回退到 monorepo 的 packages/core。 * @returns core 包根目录绝对路径。 */ const findCoreRoot = (): string => { // Koishi 移植:产物是 CJS,没有 import.meta;直接取 __dirname。 // 同时把包名判断放宽到移植后的包名(koishi-plugin-kkk),否则永远找不到包根。 const moduleDir = __dirname let dir = moduleDir for (let i = 0; i < 10; i++) { const pkgPath = path.join(dir, 'package.json') if (fs.existsSync(pkgPath)) { try { const name = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).name if (name === 'koishi-plugin-kkk' || name === 'koishi-plugin-kkk') { return dir } } catch { // package.json 解析失败时继续向上找 } } const parent = path.dirname(dir) if (parent === dir) break dir = parent } return path.resolve(moduleDir, '..', '..', '..', 'core') } /** 读取并按需内联模板 CSS(相对 url 资源转 data URI)。 */ const loadInlineTemplateCss = (): string => { const coreRoot = findCoreRoot() const cssPath = resolveTemplateStyle({ cachePath: path.join(coreRoot, 'node_modules', '.cache', 'ktr', 'style.css'), distDir: path.join(coreRoot, 'lib') }) // HtmlWrapper 只借用它的 CSS 内联能力,不走完整外壳包装。 return new HtmlWrapper({ cssPath }).loadInlineCss(cssPath) } const escapeVideoPreviewJson = (data: unknown): string => { return JSON.stringify(data).replace(/ { const serverNow = Date.now() const remainingMs = options.removeCache && options.expireAt ? Math.max(options.expireAt - serverNow, 0) : null const state: VideoPreviewState = { ...options, eventsUrl: options.eventsUrl ?? '', serverNow, remainingMs, removed: false } const appHtml = renderToString(React.createElement(PreviewLayout, { state })) const serializedState = escapeVideoPreviewJson(state) const cssContent = loadInlineTemplateCss() return `