diff --git a/persona-studio/backend/package-lock.json b/persona-studio/backend/package-lock.json index d79cf1ab..a5b0551b 100644 --- a/persona-studio/backend/package-lock.json +++ b/persona-studio/backend/package-lock.json @@ -11,7 +11,8 @@ "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", - "nodemailer": "^7.0.13" + "nodemailer": "^7.0.13", + "ws": "^8.18.0" } }, "node_modules/accepts": { @@ -872,6 +873,27 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/persona-studio/backend/package.json b/persona-studio/backend/package.json index 52d2c6d1..299beb99 100644 --- a/persona-studio/backend/package.json +++ b/persona-studio/backend/package.json @@ -11,6 +11,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", - "nodemailer": "^7.0.13" + "nodemailer": "^7.0.13", + "ws": "^8.18.0" } } diff --git a/persona-studio/backend/routes/build.js b/persona-studio/backend/routes/build.js index d519b60d..12b2c575 100644 --- a/persona-studio/backend/routes/build.js +++ b/persona-studio/backend/routes/build.js @@ -8,9 +8,12 @@ const memoryManager = require('../brain/memory-manager'); const codeGenerator = require('../brain/code-generator'); const emailSender = require('../utils/email-sender'); +// 邮箱后端正则二次校验 +const EMAIL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + // POST /api/ps/build/start router.post('/start', async (req, res) => { - const { dev_id, email, conversation } = req.body || {}; + const { dev_id, email, contact, conversation } = req.body || {}; if (!dev_id || !/^EXP-\d{3,}$/.test(dev_id)) { return res.status(400).json({ @@ -28,6 +31,15 @@ router.post('/start', async (req, res) => { }); } + // 后端二次邮箱校验 + if (!EMAIL_RE.test(email)) { + return res.status(400).json({ + error: true, + code: 'INVALID_EMAIL', + message: '邮箱格式不正确' + }); + } + // 先立即响应,后台异步处理 res.json({ error: false, @@ -37,7 +49,16 @@ router.post('/start', async (req, res) => { // 异步执行代码生成 + 邮件通知 (async () => { + const broadcast = req.app.locals.broadcastToClient || function () {}; + try { + broadcast(dev_id, { + type: 'progress', + message: '🔧 正在创建项目骨架...', + status: 'building', + status_text: '构建中' + }); + const result = await codeGenerator.generate({ dev_id, conversation: conversation || [], @@ -46,20 +67,42 @@ router.post('/start', async (req, res) => { // 记录项目 memoryManager.addProject(dev_id, { name: result.projectName || 'untitled', + email: email, + contact: contact || null, status: 'completed', created_at: new Date().toISOString(), files: result.files || [] }); + // 自动提取知识 + memoryManager.autoExtractKnowledge(dev_id, conversation, result.projectName); + + // 通知预览就绪 + broadcast(dev_id, { + type: 'preview_ready', + project: result.projectName, + message: '✅ 预览已就绪' + }); + + broadcast(dev_id, { + type: 'complete', + message: '🎉 全部完成!邮件正在发送' + }); + // 发邮件 await emailSender.sendCompletion({ to: email, dev_id, projectName: result.projectName, - summary: result.summary + summary: result.summary, + files: result.files }); } catch (err) { console.error('Build pipeline error:', err.message); + broadcast(dev_id, { + type: 'error', + message: '构建过程出错: ' + err.message + }); } })(); }); diff --git a/persona-studio/backend/routes/preview.js b/persona-studio/backend/routes/preview.js new file mode 100644 index 00000000..1b310dbc --- /dev/null +++ b/persona-studio/backend/routes/preview.js @@ -0,0 +1,72 @@ +/** + * 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'); + +// GET /api/ps/preview/:devId/:project +router.get('/:devId/:project', (req, res) => { + const { devId, project } = req.params; + + if (!devId || !project) { + return res.status(400).json({ + error: true, + code: 'MISSING_PARAMS', + message: '缺少必要参数' + }); + } + + // 安全校验:防止路径遍历 + const safeDevId = path.basename(devId); + const safeProject = path.basename(project); + 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( + '' + + '

🌊

预览正在准备中…

' + + '' + ); + } + + res.sendFile(indexPath); +}); + +// GET /api/ps/preview/:devId/:project/:file (sub-resources like CSS/JS) +router.get('/:devId/:project/:file', (req, res) => { + const { devId, project, file } = req.params; + + const safeDevId = path.basename(devId); + const safeProject = path.basename(project); + const safeFile = path.basename(file); + + 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; diff --git a/persona-studio/backend/server.js b/persona-studio/backend/server.js index a4899137..b3bca0e8 100644 --- a/persona-studio/backend/server.js +++ b/persona-studio/backend/server.js @@ -2,12 +2,14 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); +const http = require('http'); const authRoutes = require('./routes/auth'); const chatRoutes = require('./routes/chat'); const buildRoutes = require('./routes/build'); const notifyRoutes = require('./routes/notify'); const apikeyRoutes = require('./routes/apikey'); +const previewRoutes = require('./routes/preview'); const app = express(); app.use(cors()); @@ -22,6 +24,7 @@ app.use('/api/ps/chat', chatRoutes); app.use('/api/ps/build', buildRoutes); app.use('/api/ps/notify', notifyRoutes); app.use('/api/ps/apikey', apikeyRoutes); +app.use('/api/ps/preview', previewRoutes); // ── 健康检查 ── app.get('/api/ps/health', (_req, res) => { @@ -48,7 +51,7 @@ app.get('/', (_req, res) => { res.json({ status: 'ok', message: 'Persona Studio 后端服务运行中', - version: '1.0.0', + version: '2.0.0', routes: [ '/api/ps/auth/login', '/api/ps/chat/message', @@ -57,6 +60,7 @@ app.get('/', (_req, res) => { '/api/ps/notify/send', '/api/ps/apikey/detect-models', '/api/ps/apikey/chat', + '/api/ps/preview/:devId/:project', '/api/ps/health' ] }); @@ -64,7 +68,59 @@ app.get('/', (_req, res) => { const PORT = process.env.PS_PORT || 3002; -app.listen(PORT, () => { +// ── WebSocket 服务(预览进度推送) ── +const server = http.createServer(app); + +// WebSocket clients map: dev_id -> Set +const wsClients = new Map(); + +try { + const WebSocket = require('ws'); + const wss = new WebSocket.Server({ server, path: '/ws/preview' }); + + wss.on('connection', (ws, req) => { + const url = new URL(req.url, 'http://localhost'); + const devId = url.searchParams.get('dev_id') || 'unknown'; + + if (!wsClients.has(devId)) { + wsClients.set(devId, new Set()); + } + wsClients.get(devId).add(ws); + + ws.on('close', () => { + const clients = wsClients.get(devId); + if (clients) { + clients.delete(ws); + if (clients.size === 0) wsClients.delete(devId); + } + }); + + ws.on('error', () => { + const clients = wsClients.get(devId); + if (clients) { + clients.delete(ws); + if (clients.size === 0) wsClients.delete(devId); + } + }); + }); + + // Export broadcast function for other modules + app.locals.broadcastToClient = function (devId, data) { + const clients = wsClients.get(devId); + if (!clients) return; + const msg = typeof data === 'string' ? data : JSON.stringify(data); + clients.forEach(function (ws) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(msg); + } + }); + }; +} catch (_e) { + // ws module not installed, WebSocket disabled + app.locals.broadcastToClient = function () {}; +} + +server.listen(PORT, () => { console.log(`🌊 Persona Studio 后端服务启动 · 端口 ${PORT}`); }); diff --git a/persona-studio/backend/utils/email-sender.js b/persona-studio/backend/utils/email-sender.js index 937c220a..f81c3680 100644 --- a/persona-studio/backend/utils/email-sender.js +++ b/persona-studio/backend/utils/email-sender.js @@ -50,21 +50,51 @@ async function send({ to, subject, body }) { /** * 发送开发完成通知 */ -async function sendCompletion({ to, dev_id, projectName, summary }) { +async function sendCompletion({ to, dev_id, projectName, summary, files, downloadUrl }) { const subject = `✅ 你的模块已完成 · ${projectName}`; + + // 生成文件列表 + let fileListHtml = ''; + if (files && files.length > 0) { + fileListHtml = '

📁 文件列表

'; + } + + // 下载链接 + let downloadHtml = ''; + if (downloadUrl) { + downloadHtml = '

📦 下载项目包

'; + } + const body = [ - '
', - '

🌊 光湖 Persona Studio

', - '
', - `

你好 ${dev_id},

`, - `

你的项目 ${projectName} 已经完成开发!

`, + '
', + '
', + '

🌊 光湖 Persona Studio

', + '

HoloLake Era · AGE OS · 人格语言操作系统

', + '
', + '
', + '

你好 ' + dev_id + ',

', + '

你的项目 ' + projectName + ' 已经完成开发!

', '

📋 开发摘要

', - `

${summary || '项目代码已生成'}

`, - '
', - '

', - '光湖语言人格系统 · HoloLake Era · AGE OS
', - '此邮件由铸渊自动发送', + '

' + (summary || '项目代码已生成') + '

', + fileListHtml, + downloadHtml, + '

📖 使用说明

', + '
    ', + '
  1. 下载项目包并解压
  2. ', + '
  3. 在浏览器中打开 index.html 查看效果
  4. ', + '
  5. 如需修改,用任意代码编辑器打开项目文件
  6. ', + '
', + '
', + '
', + '

', + '🌀 铸渊 · 代码守护人格体 · 自动发送
', + '光湖语言人格系统 · HoloLake Era · AGE OS', '

', + '
', '
' ].join('\n'); diff --git a/persona-studio/frontend/chat.html b/persona-studio/frontend/chat.html index 38a10165..4d38c376 100644 --- a/persona-studio/frontend/chat.html +++ b/persona-studio/frontend/chat.html @@ -7,7 +7,7 @@ -
+
- -
+ +
@@ -106,20 +106,54 @@
+ + + + + +
- +