diff --git a/bun.lock b/bun.lock index 50d9e86..c66aaab 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "morgan": "^1.10.1", "pino": "^9.9.5", "pino-pretty": "^13.1.1", + "playwright": "^1.55.0", "prisma": "^6.16.1", "zod": "^4.1.8", }, @@ -173,6 +174,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.2", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -259,6 +262,10 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "playwright": ["playwright@1.55.0", "https://registry.npmmirror.com/playwright/-/playwright-1.55.0.tgz", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="], + + "playwright-core": ["playwright-core@1.55.0", "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.55.0.tgz", { "bin": { "playwright-core": "cli.js" } }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="], + "prisma": ["prisma@6.16.1", "", { "dependencies": { "@prisma/config": "6.16.1", "@prisma/engines": "6.16.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w=="], "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], diff --git a/package.json b/package.json index cb64de3..2ae6ac9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "morgan": "^1.10.1", "pino": "^9.9.5", "pino-pretty": "^13.1.1", + "playwright": "^1.55.0", "prisma": "^6.16.1", "zod": "^4.1.8" }, diff --git a/src/app.ts b/src/app.ts index 03e8e37..7a4afeb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import helmet from 'helmet'; import morgan from 'morgan'; import { router as apiRouter } from './routes'; import { errorHandler } from './middlewares/errorHandler'; +import path from 'path'; export function createApp() { const app = express(); @@ -16,6 +17,19 @@ export function createApp() { app.get('/health', (_req: express.Request, res: express.Response) => res.json({ ok: true })); app.use('/api', apiRouter); + + // 静态资源目录(相对于项目根目录) + const distDir = path.resolve(process.cwd(), 'dist'); + + // 提供 dist 下的静态文件 + app.use(express.static(distDir)); + + // 对于非 /api 前缀且未命中的路由,回退返回 dist/index.html(适用于 SPA) + app.get(/^\/(?!api)(.*)/, (_req, res, next) => { + res.sendFile(path.join(distDir, 'index.html'), (err) => { + if (err) next(err); + }); + }); app.use(errorHandler); return app; } diff --git a/src/routes/models.ts b/src/routes/models.ts index 9b541f4..0b3e8e4 100644 --- a/src/routes/models.ts +++ b/src/routes/models.ts @@ -3,6 +3,33 @@ import { prisma } from '../lib/prisma'; import { basicAuth } from '../middlewares/basicAuth'; import { CreateModelSchema, UpdateModelSchema, ListQuerySchema } from '../lib/validators'; import { decodeBase64ToBuffer } from '../lib/base64'; +import { chromium } from 'playwright'; +import { readFile } from 'fs/promises'; +import path from 'path'; + +const dirname = new URL('.', import.meta.url).pathname; + +async function generatePreviewImage(circuitModel: any) { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.setViewportSize({ width: 1024, height: 1024 }); + // Load the lab page + await page.goto(`http://localhost:${Number(process.env.PORT ?? 3000)}/lab?preview=true`, { waitUntil: 'load' }); + await page.waitForFunction(() => { + // @ts-ignore + return window.loadCircuitFromJSONText !== undefined; + }); + const jsonText = JSON.stringify(circuitModel); + // Inject the model data and render + await page.evaluate((jsonText) => { + // @ts-ignore + window.loadCircuitFromJSONText(jsonText); + }, jsonText); + const image = await page.screenshot({ type: 'png' }); + await browser.close(); + return image; +} export const router = Router(); @@ -10,14 +37,14 @@ export const router = Router(); router.post('/', basicAuth, async (req: Request, res: Response) => { const parsed = CreateModelSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); - const { title, desc, model, previewBase64, previewMime } = parsed.data; + const { title, desc, model } = parsed.data; const created = await prisma.circuitModel.create({ data: { title, desc, model, - preview: decodeBase64ToBuffer(previewBase64), - previewMime: previewBase64 ? (previewMime ?? 'image/png') : null, + preview: await generatePreviewImage(model), + previewMime: 'image/png', authorId: (req as any).user.id, }, });