feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
/**
|
|
|
|
|
* persona-studio · 预览 API
|
|
|
|
|
* GET /api/ps/preview/:devId/:project 提供 iframe 实时预览
|
|
|
|
|
*/
|
|
|
|
|
const express = require('express');
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
|
|
|
|
const WORKSPACE_DIR = path.join(__dirname, '..', '..', 'workspace');
|
|
|
|
|
|
2026-03-11 19:52:13 +08:00
|
|
|
// 简易速率限制:每个 IP 每分钟最多 60 次请求
|
|
|
|
|
const rateLimitMap = new Map();
|
|
|
|
|
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
|
|
|
const RATE_LIMIT_MAX = 60;
|
|
|
|
|
|
|
|
|
|
function rateLimit(req, res, next) {
|
|
|
|
|
const ip = req.ip || req.connection.remoteAddress || 'unknown';
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const entry = rateLimitMap.get(ip);
|
|
|
|
|
|
|
|
|
|
if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
|
|
|
|
rateLimitMap.set(ip, { start: now, count: 1 });
|
|
|
|
|
return next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry.count++;
|
|
|
|
|
if (entry.count > RATE_LIMIT_MAX) {
|
|
|
|
|
return res.status(429).json({
|
|
|
|
|
error: true,
|
|
|
|
|
code: 'RATE_LIMITED',
|
|
|
|
|
message: '请求过于频繁,请稍后再试'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定期清理过期条目
|
|
|
|
|
setInterval(function () {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
for (const [ip, entry] of rateLimitMap) {
|
|
|
|
|
if (now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
|
|
|
|
rateLimitMap.delete(ip);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, RATE_LIMIT_WINDOW_MS);
|
|
|
|
|
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
// GET /api/ps/preview/:devId/:project
|
2026-03-11 19:52:13 +08:00
|
|
|
router.get('/:devId/:project', rateLimit, (req, res) => {
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
const { devId, project } = req.params;
|
|
|
|
|
|
|
|
|
|
if (!devId || !project) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
error: true,
|
|
|
|
|
code: 'MISSING_PARAMS',
|
|
|
|
|
message: '缺少必要参数'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 19:50:12 +08:00
|
|
|
// 安全校验:确保 devId 和 project 匹配预期格式(防止路径遍历)
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
const safeDevId = path.basename(devId);
|
|
|
|
|
const safeProject = path.basename(project);
|
2026-03-11 19:50:12 +08:00
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(safeDevId) || !/^[a-zA-Z0-9_.-]+$/.test(safeProject)) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
error: true,
|
|
|
|
|
code: 'INVALID_PARAMS',
|
|
|
|
|
message: '参数格式无效'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
const previewDir = path.join(WORKSPACE_DIR, safeDevId, safeProject, 'preview');
|
|
|
|
|
const projectDir = path.join(WORKSPACE_DIR, safeDevId, safeProject);
|
|
|
|
|
|
|
|
|
|
// 优先从 preview/ 子目录查找
|
|
|
|
|
let targetDir = previewDir;
|
|
|
|
|
if (!fs.existsSync(previewDir)) {
|
|
|
|
|
targetDir = projectDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const indexPath = path.join(targetDir, 'index.html');
|
|
|
|
|
if (!fs.existsSync(indexPath)) {
|
|
|
|
|
return res.status(404).send(
|
|
|
|
|
'<html><body style="background:#0a0e1a;color:#94a3b8;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">' +
|
|
|
|
|
'<div style="text-align:center"><p style="font-size:2rem">🌊</p><p>预览正在准备中…</p></div>' +
|
|
|
|
|
'</body></html>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.sendFile(indexPath);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// GET /api/ps/preview/:devId/:project/:file (sub-resources like CSS/JS)
|
2026-03-11 19:52:13 +08:00
|
|
|
router.get('/:devId/:project/:file', rateLimit, (req, res) => {
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
const { devId, project, file } = req.params;
|
|
|
|
|
|
|
|
|
|
const safeDevId = path.basename(devId);
|
|
|
|
|
const safeProject = path.basename(project);
|
|
|
|
|
const safeFile = path.basename(file);
|
|
|
|
|
|
2026-03-11 19:50:12 +08:00
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(safeDevId) || !/^[a-zA-Z0-9_.-]+$/.test(safeProject) || !/^[a-zA-Z0-9_.-]+$/.test(safeFile)) {
|
|
|
|
|
return res.status(400).send('Invalid parameters');
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement split-screen layout, enhanced email modal, preview panel, and WebSocket
- chat.html: Enhanced modal with email+contact fields, split-screen HTML structure,
resizer divider, preview panel with status bar and iframe
- chat.js: Email validation (frontend regex), email prefill on repeat visits,
split-screen enterDevMode(), draggable resizer with touch support,
preview panel status updates, WebSocket connection for build progress
- style.css: Split-screen dev-mode layout, resizer styles, preview panel styles
with status indicators (waiting/building/done/error), modal field styles
- backend: Preview API route, WebSocket server for progress push,
enhanced build route with email validation and broadcast,
enhanced email template with file list and brand signature
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
2026-03-11 19:47:50 +08:00
|
|
|
const previewDir = path.join(WORKSPACE_DIR, safeDevId, safeProject, 'preview');
|
|
|
|
|
const projectDir = path.join(WORKSPACE_DIR, safeDevId, safeProject);
|
|
|
|
|
|
|
|
|
|
let targetDir = previewDir;
|
|
|
|
|
if (!fs.existsSync(previewDir)) {
|
|
|
|
|
targetDir = projectDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filePath = path.join(targetDir, safeFile);
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
return res.status(404).send('File not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.sendFile(filePath);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|