| 1 | import fs from 'fs' |
| 2 | import os from 'os' |
| 3 | import path from 'path' |
| 4 | import { FilesystemBackend, createDeepAgent } from 'deepagents' |
| 5 | import { resolveModelTimeoutMs } from '@shared/model-timeout' |
| 6 | import { resolveModel } from '../../agent-runtime/model' |
| 7 | import type { ModelRuntimeConfig } from '../../agent-runtime/model' |
| 8 | import { readStylePackage } from '../style-package' |
| 9 | import { buildStylePreviewPrompt } from '../../agent-runtime/prompt' |
| 10 | |
| 11 | const MAX_PREVIEW_HTML_BYTES = 1024 * 1024 |
| 12 | |
| 13 | type StylePreviewAgentArgs = { |
| 14 | provider: string |
| 15 | apiKey: string |
| 16 | model: string |
| 17 | baseUrl: string |
| 18 | maxTokens?: number |
| 19 | modelRuntime?: ModelRuntimeConfig |
| 20 | modelTimeoutMs: number |
| 21 | workspaceDir: string |
| 22 | prompt: string |
| 23 | } |
| 24 | |
| 25 | type GenerateStylePreviewArgs = Omit<StylePreviewAgentArgs, 'workspaceDir' | 'prompt'> & { |
| 26 | styleId: string |
| 27 | stylePackageDir: string |
| 28 | } |
| 29 | |
| 30 | type StylePreviewGeneratorDependencies = { |
| 31 | runAgent?: (args: StylePreviewAgentArgs) => Promise<void> |
| 32 | } |
| 33 | |
| 34 | async function runStylePreviewAgent(args: StylePreviewAgentArgs): Promise<void> { |
| 35 | const model = resolveModel( |
| 36 | args.provider, |
| 37 | args.apiKey, |
| 38 | args.model, |
| 39 | args.baseUrl, |
| 40 | 0.65, |
| 41 | args.maxTokens, |
| 42 | args.modelRuntime |
| 43 | ) |
| 44 | const agent = createDeepAgent({ |
| 45 | model, |
| 46 | backend: new FilesystemBackend({ |
| 47 | rootDir: args.workspaceDir, |
| 48 | virtualMode: true |
| 49 | }), |
| 50 | systemPrompt: |
| 51 | 'You are a presentation style preview designer. Read the provided style package and use write_file to create the requested standalone preview.html. The file, not the chat response, is the deliverable.' |
| 52 | }) |
| 53 | |
| 54 | const stream = await agent.stream( |
| 55 | { |
| 56 | messages: [{ role: 'user', content: args.prompt }] |
| 57 | }, |
| 58 | { |
| 59 | streamMode: ['updates', 'messages'], |
| 60 | subgraphs: true, |
| 61 | signal: AbortSignal.timeout(resolveModelTimeoutMs(args.modelTimeoutMs, 'document')) |
| 62 | } |
| 63 | ) |
| 64 | |
| 65 | for await (const _chunk of stream as AsyncIterable<unknown>) { |
| 66 | // Consuming the stream drives the agent until preview.html has been written. |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | export async function generateStylePreviewHtml( |
| 71 | args: GenerateStylePreviewArgs, |
| 72 | dependencies: StylePreviewGeneratorDependencies = {} |
| 73 | ): Promise<string> { |
| 74 | const stylePackageDir = path.resolve(args.stylePackageDir) |
| 75 | const existingPackage = await readStylePackage(stylePackageDir) |
| 76 | if (existingPackage.previewPath) { |
| 77 | throw new Error('该风格已有预览,无需重复生成。') |
| 78 | } |
| 79 | |
| 80 | const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ohmyppt-style-preview-')) |
| 81 | const workspaceDir = path.join(tempRoot, args.styleId) |
| 82 | |
| 83 | try { |
| 84 | await fs.promises.mkdir(workspaceDir, { recursive: true }) |
| 85 | await Promise.all([ |
| 86 | fs.promises.copyFile( |
| 87 | path.join(stylePackageDir, 'style.json'), |
| 88 | path.join(workspaceDir, 'style.json') |
| 89 | ), |
| 90 | fs.promises.copyFile( |
| 91 | path.join(stylePackageDir, 'SKILL.md'), |
| 92 | path.join(workspaceDir, 'SKILL.md') |
| 93 | ) |
| 94 | ]) |
| 95 | |
| 96 | const prompt = buildStylePreviewPrompt() |
| 97 | await (dependencies.runAgent || runStylePreviewAgent)({ |
| 98 | provider: args.provider, |
| 99 | apiKey: args.apiKey, |
| 100 | model: args.model, |
| 101 | baseUrl: args.baseUrl, |
| 102 | maxTokens: args.maxTokens, |
| 103 | modelRuntime: args.modelRuntime, |
| 104 | modelTimeoutMs: args.modelTimeoutMs, |
| 105 | workspaceDir, |
| 106 | prompt |
| 107 | }) |
| 108 | |
| 109 | const generatedPackage = await readStylePackage(workspaceDir) |
| 110 | if (!generatedPackage.previewPath) { |
| 111 | throw new Error('预览生成失败:模型未创建 preview.html。') |
| 112 | } |
| 113 | const previewHtml = await fs.promises.readFile(generatedPackage.previewPath, 'utf8') |
| 114 | if (Buffer.byteLength(previewHtml, 'utf8') > MAX_PREVIEW_HTML_BYTES) { |
| 115 | throw new Error('预览生成失败:preview.html 超过 1MB。') |
| 116 | } |
| 117 | return previewHtml |
| 118 | } finally { |
| 119 | await fs.promises.rm(tempRoot, { recursive: true, force: true }).catch(() => undefined) |
| 120 | } |
| 121 | } |
| 122 |