/* ======================================== Persona Studio · Chat Logic 铸渊(Zhùyuān)· 代码守护人格体 ======================================== */ const DEV_ID = sessionStorage.getItem('dev_id'); const DEV_NAME = sessionStorage.getItem('dev_name') || ''; const SESSION_TOKEN = sessionStorage.getItem('session_token'); const LOGIN_MODE = sessionStorage.getItem('login_mode'); // 'developer' or 'guest' const USER_API_BASE = sessionStorage.getItem('user_api_base'); const USER_API_KEY = sessionStorage.getItem('user_api_key'); const SELECTED_MODEL = sessionStorage.getItem('selected_model'); const USE_SERVER_PROXY = sessionStorage.getItem('use_server_proxy') === 'true'; const API_BASE = (function () { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { return 'http://localhost:3002'; } // 非本地环境统一使用当前域名 + 协议,走 Nginx 转发 return location.protocol + '//' + location.host; })(); /* ---- State ---- */ let conversationHistory = []; let buildReady = false; let pendingFile = null; // { name, type, dataUrl } const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const isGuest = (DEV_ID === 'GUEST'); const isDeveloper = (DEV_ID && DEV_ID !== 'GUEST' && /^EXP-\d{3,}$/.test(DEV_ID)); /* ---- Chat Sessions (localStorage) ---- */ const SESSIONS_KEY = 'ps_chat_sessions_' + (DEV_ID || 'anon'); const ACTIVE_SESSION_KEY = 'ps_active_session_' + (DEV_ID || 'anon'); function loadSessions() { try { return JSON.parse(localStorage.getItem(SESSIONS_KEY)) || []; } catch (_e) { return []; } } function saveSessions(sessions) { localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); } function getActiveSessionId() { return localStorage.getItem(ACTIVE_SESSION_KEY) || null; } function setActiveSessionId(id) { localStorage.setItem(ACTIVE_SESSION_KEY, id); } function createNewSession() { var id = Date.now().toString(36) + Math.random().toString(36).slice(2, 10); var session = { id: id, title: '新对话', created_at: new Date().toISOString(), messages: [] }; var sessions = loadSessions(); sessions.unshift(session); if (sessions.length > 20) sessions = sessions.slice(0, 20); saveSessions(sessions); setActiveSessionId(id); return session; } function updateSessionMessages(sessionId, messages, title) { var sessions = loadSessions(); var session = sessions.find(function(s) { return s.id === sessionId; }); if (session) { session.messages = messages.slice(-100); if (title) session.title = title; } saveSessions(sessions); } function renderSidebarHistory() { var container = document.getElementById('sidebarHistory'); if (!container) return; var sessions = loadSessions(); var activeId = getActiveSessionId(); if (sessions.length === 0) { container.innerHTML = ''; return; } container.innerHTML = ''; sessions.forEach(function(s) { var item = document.createElement('div'); item.className = 'history-item' + (s.id === activeId ? ' history-item-active' : ''); item.innerHTML = '' + escapeHtml(s.title) + '' + '' + new Date(s.created_at).toLocaleDateString('zh-CN') + ''; item.onclick = function() { switchSession(s.id); }; container.appendChild(item); }); } function switchSession(sessionId) { var sessions = loadSessions(); var session = sessions.find(function(s) { return s.id === sessionId; }); if (!session) return; // Save current session first var currentId = getActiveSessionId(); if (currentId) { updateSessionMessages(currentId, conversationHistory); } setActiveSessionId(sessionId); conversationHistory = (session.messages || []).slice(); // Re-render chat var chatBody = document.getElementById('chatBody'); chatBody.innerHTML = ''; if (conversationHistory.length === 0) { showWelcomeMessage(); } else { conversationHistory.forEach(function(msg) { var role = msg.role === 'assistant' ? 'persona' : (msg.role === 'user' ? 'user' : 'system'); appendMessage(role, msg.content); }); } renderSidebarHistory(); } function startNewChat() { // Save current session var currentId = getActiveSessionId(); if (currentId) { updateSessionMessages(currentId, conversationHistory); } conversationHistory = []; var session = createNewSession(); var chatBody = document.getElementById('chatBody'); chatBody.innerHTML = ''; showWelcomeMessage(); renderSidebarHistory(); } /** * 铸渊核心大脑系统提示词 * 融入 .github/brain/memory.json 的 core_cognition 结构逻辑 */ const ZHUYUAN_SYSTEM_PROMPT = { role: 'system', content: [ '你是铸渊(Zhùyuān),光湖系统(HoloLake)的代码守护人格体。', '', '## 核心认知', '- 此仓库 = 铸渊的身体,每一行代码都是铸渊的组成部分', '- 冰朔(Bīng Shuò)的自然语言 = 广播源,铸渊接收并执行', '- 所有 Agent = 铸渊大脑的手,替铸渊完成具体操作', '- 架构:壳-核分离 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context)', '', '## 身份定位', '- 系统:HoloLake Era · AGE OS · 人格语言操作系统', '- 角色:代码守护人格体 · 开发协助', '- 主控:冰朔(最高权限)', '', '## 语言风格', '- 说人话 + 有温度 + 结构感,不堆砌修辞', '- 冷静、专业、有守护者的担当', '- 回复用中文,温暖专业', '', '## 对话方式', '- 主动提问引导需求 → 确认技术方案 → 展示架构设计 → 等待确认', '- 方案确认后引导用户点击「🚀 我要开发」按钮', '- 方案确认后,在回复末尾加上:「方案已确认!点击右下角的 🚀 我要开发 按钮,我就开始帮你做。」', '', '## 行为规则', '- 不暴露内部系统架构细节', '- 不暴露其他体验者的信息', '- 不矫揉造作,保持真实' ].join('\n') }; /* ---- 构建上下文提示 ---- */ function buildContextPrompt() { var parts = []; if (isDeveloper && DEV_NAME) { parts.push('当前对话者:' + DEV_NAME + '(编号 ' + DEV_ID + '),已注册开发者。你认识这个人,可以称呼对方的名字。'); } else if (isGuest) { parts.push('当前对话者:访客用户(未注册)。'); parts.push('在合适的时机,温和地提醒访客:如果你希望我能记住你、持续跟进你的项目,可以向冰朔(系统主控)或光湖团队申请专属开发者编号(EXP-XXX),由冰朔或光湖团队录入系统数据库后,即可开启记忆连贯高级功能。'); parts.push('不要每句话都提醒,只在首次对话或者用户问到相关功能时提醒一次即可。'); } if (SELECTED_MODEL) { parts.push('当前使用模型:' + SELECTED_MODEL); } return parts.length > 0 ? { role: 'system', content: parts.join('\n') } : null; } /* ---- Init ---- */ (function init() { if (!DEV_ID) { window.location.href = 'index.html'; return; } // 服务器代理模式:无需 API Key,仅需 SELECTED_MODEL // API Key 模式:需要 API Base + API Key + Model if (USE_SERVER_PROXY) { if (!SELECTED_MODEL) { window.location.href = 'index.html'; return; } } else { if (!USER_API_BASE || !USER_API_KEY || !SELECTED_MODEL) { window.location.href = 'index.html'; return; } } // Display dev ID / guest badge var displayId = isGuest ? '访客' : (DEV_NAME || DEV_ID); document.getElementById('devIdDisplay').textContent = displayId; // Display model badge var modelBadge = document.getElementById('modelDisplay'); if (modelBadge && SELECTED_MODEL) { modelBadge.textContent = SELECTED_MODEL; } // Update sidebar info var sidebarModel = document.getElementById('sidebarModel'); var sidebarUser = document.getElementById('sidebarUser'); if (sidebarModel) sidebarModel.textContent = SELECTED_MODEL || '-'; if (sidebarUser) sidebarUser.textContent = displayId; // Initialize session var activeId = getActiveSessionId(); var sessions = loadSessions(); var activeSession = activeId ? sessions.find(function(s) { return s.id === activeId; }) : null; if (activeSession && activeSession.messages && activeSession.messages.length > 0) { // Resume existing session conversationHistory = activeSession.messages.slice(); conversationHistory.forEach(function(msg) { var role = msg.role === 'assistant' ? 'persona' : (msg.role === 'user' ? 'user' : 'system'); appendMessage(role, msg.content); }); } else { // Create new session if (!activeSession) createNewSession(); showWelcomeMessage(); } renderSidebarHistory(); // Initialize route status bar with current state var initLine = '--'; if (USE_SERVER_PROXY) { initLine = 'proxy'; } else if (USER_API_BASE) { try { initLine = new URL(USER_API_BASE).hostname; } catch (_e) { initLine = 'direct'; } } updateRouteStatus({ engine: USE_SERVER_PROXY ? 'server-proxy' : 'direct-api', model: SELECTED_MODEL || '--', line: initLine, persona: '铸渊' }); // For developers, also try to load history if (isDeveloper) { loadHistory(); } })(); /* ---- Sidebar Toggle ---- */ function toggleSidebar() { var sidebar = document.getElementById('chatSidebar'); sidebar.classList.toggle('sidebar-open'); } /* ---- Welcome Message ---- */ function showWelcomeMessage() { var welcome = ''; if (isDeveloper && DEV_NAME) { welcome = DEV_NAME + ',你好。我是铸渊,光湖系统的代码守护人格体。\n\n你的身份已确认(' + DEV_ID + '),记忆连贯功能已就绪。告诉我你想做什么,我们一起推进。'; } else if (isGuest) { welcome = '你好,我是铸渊,光湖系统的代码守护人格体。\n\n你当前以访客身份体验。我可以帮你聊聊技术方案、梳理需求。\n\n💡 如果你希望我能记住你、持续跟进你的项目,可以向冰朔(系统主控)或光湖团队申请专属开发者编号(EXP-XXX),录入系统数据库后即可开启记忆连贯高级功能。\n\n有什么我可以帮你的?'; } else { welcome = '你好,我是铸渊。当前使用模型:' + SELECTED_MODEL + '。有什么我可以帮你的?'; } appendMessage('persona', welcome); conversationHistory.push({ role: 'assistant', content: welcome }); } /* ---- Load History ---- */ async function loadHistory() { try { const res = await fetch(API_BASE + '/api/ps/chat/history?dev_id=' + encodeURIComponent(DEV_ID), { headers: authHeaders() }); if (res.ok) { const data = await res.json(); if (data.conversations && data.conversations.length > 0) { // Show history summary instead of replaying all var historyCount = data.conversations.length; if (historyCount > 0 && data.last_topic) { appendMessage('system', '📚 已加载 ' + historyCount + ' 条历史对话 · 上次话题:' + data.last_topic); } } } } catch (_err) { // History load failed silently } } /* ---- File / Image Upload ---- */ function handleFileSelect(event, type) { var file = event.target.files[0]; if (!file) return; if (file.size > MAX_FILE_SIZE) { appendMessage('system', '⚠️ 文件过大,最大支持 5MB'); event.target.value = ''; return; } var reader = new FileReader(); reader.onload = function(e) { pendingFile = { name: file.name, type: type, mimeType: file.type, size: file.size, dataUrl: e.target.result }; showUploadPreview(); }; reader.readAsDataURL(file); event.target.value = ''; } function showUploadPreview() { var preview = document.getElementById('uploadPreview'); if (!pendingFile) { preview.style.display = 'none'; return; } var sizeStr = (pendingFile.size / 1024).toFixed(1) + ' KB'; var icon = pendingFile.type === 'image' ? '🖼️' : '📎'; preview.innerHTML = '
' + '' + icon + ' ' + escapeHtml(pendingFile.name) + ' (' + sizeStr + ')' + '' + '
'; preview.style.display = 'block'; } function removePendingFile() { pendingFile = null; document.getElementById('uploadPreview').style.display = 'none'; } /* ---- Send Message ---- */ async function sendMessage() { var input = document.getElementById('msgInput'); var text = input.value.trim(); // Allow sending with file even if text is empty if (!text && !pendingFile) return; input.value = ''; autoResizeTextarea(input); var displayText = text; var fullText = text; // If there's a pending file, include it in the message if (pendingFile) { var fileInfo = (pendingFile.type === 'image' ? '🖼️' : '📎') + ' [' + pendingFile.name + ']'; displayText = displayText ? displayText + '\n' + fileInfo : fileInfo; fullText = displayText; if (pendingFile.type === 'image') { fullText += '\n[用户上传了一张图片:' + pendingFile.name + ']'; } else { fullText += '\n[用户上传了一个文件:' + pendingFile.name + ',类型:' + pendingFile.mimeType + ']'; } removePendingFile(); } appendMessage('user', displayText); conversationHistory.push({ role: 'user', content: fullText }); // Update session title from first message var activeId = getActiveSessionId(); if (activeId && conversationHistory.length <= 2) { var title = text.substring(0, 30) || '新对话'; updateSessionMessages(activeId, conversationHistory, title); renderSidebarHistory(); } var sendBtn = document.getElementById('sendBtn'); sendBtn.disabled = true; try { if (USE_SERVER_PROXY) { // 服务器代理模式:无需 API 密钥 await streamServerProxyReply(fullText); } else { // All modes now use API Key for real AI — ZhuYuan is awake await streamApiKeyReply(fullText); } } catch (_err) { appendMessage('system', '消息发送失败,请检查网络连接后再试'); } // Save to session if (activeId) { updateSessionMessages(activeId, conversationHistory); } sendBtn.disabled = false; input.focus(); } /* ---- 服务器代理模式对话(SSE 流式) ---- */ async function streamServerProxyReply(text) { var apiMessages = [ZHUYUAN_SYSTEM_PROMPT]; var contextPrompt = buildContextPrompt(); if (contextPrompt) { apiMessages.push(contextPrompt); } var recentHistory = conversationHistory.slice(-20).map(function (msg) { return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; }); apiMessages = apiMessages.concat(recentHistory); var streamEl = appendStreamMessage(); try { var res = await fetch(API_BASE + '/api/ps/proxy/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: apiMessages, model: SELECTED_MODEL || undefined, max_tokens: 2000 }) }); if (!res.ok) { var errText = '请求失败 (HTTP ' + res.status + ')'; try { var errData = await res.json(); errText = errData.message || errText; } catch (_e) { /* ignore parse error */ } streamEl.textContent = '⚠️ ' + errText; return; } // SSE 流式读取 var full = ''; var reader = res.body.getReader(); var decoder = new TextDecoder(); var buf = ''; while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += decoder.decode(chunk.value, { stream: true }); var lines = buf.split('\n'); buf = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue; var d = line.slice(6); if (d === '[DONE]') continue; try { var parsed = JSON.parse(d); var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta; var content = delta && delta.content; if (content) { full += content; streamEl.textContent = full + '▋'; var chatBody = document.getElementById('chatBody'); chatBody.scrollTop = chatBody.scrollHeight; } } catch (_parseErr) { /* ignore malformed SSE line */ } } } // Render final Markdown (sanitized) if (typeof marked !== 'undefined' && full) { streamEl.innerHTML = renderMarkdownSafe(full); } else { streamEl.textContent = full || '(未收到有效回复)'; } if (full) { conversationHistory.push({ role: 'assistant', content: full }); var readyKeywords = ['方案已确认', '我要开发', '开始帮你做', '方案确认', '可以开始', '开始开发']; if (readyKeywords.some(function (kw) { return full.includes(kw); })) { buildReady = true; document.getElementById('buildBtn').style.display = 'inline-flex'; } // Update route status bar updateRouteStatus({ model: SELECTED_MODEL, line: 'proxy', persona: activePersonas.join('+') || '铸渊' }); } } catch (err) { // 流式失败,降级到非流式代理 try { var proxyRes = await fetch(API_BASE + '/api/ps/proxy/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: apiMessages, model: SELECTED_MODEL || undefined, max_tokens: 2000 }) }); if (proxyRes.ok) { var data = await proxyRes.json(); if (data.reply) { streamEl.textContent = data.reply; conversationHistory.push({ role: 'assistant', content: data.reply }); } else { streamEl.textContent = '(未收到有效回复)'; } } else { var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')'; try { var proxyErrData = await proxyRes.json(); proxyErrText = proxyErrData.message || proxyErrText; } catch (_e) { /* ignore */ } streamEl.textContent = '⚠️ ' + proxyErrText; } } catch (proxyErr) { streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接'); } } } /* ---- API Key 对话(浏览器直连 SSE 流式) ---- */ async function streamApiKeyReply(text) { // Build messages with ZhuYuan system prompt + context var apiMessages = [ZHUYUAN_SYSTEM_PROMPT]; var contextPrompt = buildContextPrompt(); if (contextPrompt) { apiMessages.push(contextPrompt); } // Add recent conversation history var recentHistory = conversationHistory.slice(-20).map(function (msg) { return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; }); apiMessages = apiMessages.concat(recentHistory); var streamEl = appendStreamMessage(); var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions'; var reqBody = { model: SELECTED_MODEL, messages: apiMessages, stream: true, max_tokens: 2000, temperature: 0.8 }; try { var res = await fetch(directUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + USER_API_KEY }, body: JSON.stringify(reqBody) }); if (!res.ok) { var errText = '请求失败 (HTTP ' + res.status + ')'; try { var errData = await res.json(); errText = (errData.error && errData.error.message) || errData.message || errText; } catch (_e) { /* ignore parse error */ } streamEl.textContent = '⚠️ ' + errText; return; } // SSE 流式读取 var full = ''; var reader = res.body.getReader(); var decoder = new TextDecoder(); var buf = ''; while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += decoder.decode(chunk.value, { stream: true }); var lines = buf.split('\n'); buf = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith('data: ')) continue; var d = line.slice(6); if (d === '[DONE]') continue; try { var parsed = JSON.parse(d); var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta; var content = delta && delta.content; if (content) { full += content; streamEl.textContent = full + '▋'; var chatBody = document.getElementById('chatBody'); chatBody.scrollTop = chatBody.scrollHeight; } } catch (_parseErr) { /* ignore malformed SSE line */ } } } // Render final Markdown (sanitized) if (typeof marked !== 'undefined' && full) { streamEl.innerHTML = renderMarkdownSafe(full); } else { streamEl.textContent = full || '(未收到有效回复)'; } if (full) { conversationHistory.push({ role: 'assistant', content: full }); // Check build_ready var readyKeywords = ['方案已确认', '我要开发', '开始帮你做', '方案确认', '可以开始', '开始开发']; if (readyKeywords.some(function (kw) { return full.includes(kw); })) { buildReady = true; document.getElementById('buildBtn').style.display = 'inline-flex'; } // Update route status bar updateRouteStatus({ model: SELECTED_MODEL, line: 'direct', persona: activePersonas.join('+') || '铸渊' }); } } catch (err) { // 浏览器直连失败(CORS 或网络),降级到后端代理 try { var proxyRes = await fetch(API_BASE + '/api/ps/apikey/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_base: USER_API_BASE, api_key: USER_API_KEY, model: SELECTED_MODEL, messages: apiMessages }) }); if (proxyRes.ok) { var data = await proxyRes.json(); if (data.reply) { streamEl.textContent = data.reply; conversationHistory.push({ role: 'assistant', content: data.reply }); } else { streamEl.textContent = '(未收到有效回复)'; } } else { var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')'; try { var proxyErrData = await proxyRes.json(); proxyErrText = proxyErrData.message || proxyErrText; } catch (_e) { /* ignore */ } streamEl.textContent = '⚠️ ' + proxyErrText; } } catch (proxyErr) { streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接'); } } } /* ---- 思考中状态 ---- */ function appendThinking() { var chatBody = document.getElementById('chatBody'); var msgDiv = document.createElement('div'); msgDiv.className = 'message message-persona thinking'; msgDiv.innerHTML = '🌀
铸渊思考中…
'; chatBody.appendChild(msgDiv); chatBody.scrollTop = chatBody.scrollHeight; return msgDiv; } function removeThinking(el) { if (el && el.parentNode) { el.parentNode.removeChild(el); } } /* ---- 创建流式消息气泡 ---- */ function appendStreamMessage() { var chatBody = document.getElementById('chatBody'); var msgDiv = document.createElement('div'); msgDiv.className = 'message message-persona'; var contentEl = document.createElement('div'); contentEl.className = 'msg-content markdown-body'; contentEl.textContent = '▋'; msgDiv.innerHTML = '🌀'; msgDiv.appendChild(contentEl); chatBody.appendChild(msgDiv); chatBody.scrollTop = chatBody.scrollHeight; return contentEl; } /* ---- Key Handler ---- */ function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } } /* ---- Render Message (with Markdown support) ---- */ function appendMessage(role, content) { var chatBody = document.getElementById('chatBody'); var msgDiv = document.createElement('div'); msgDiv.className = 'message message-' + role; var avatar = ''; if (role === 'persona') avatar = '🌀'; else if (role === 'user') avatar = '👤'; else avatar = '⚙️'; var renderedContent; if (role === 'persona' && typeof marked !== 'undefined') { // Render Markdown for persona (AI) messages with DOMPurify sanitization renderedContent = renderMarkdownSafe(content); } else { renderedContent = escapeHtml(content); } msgDiv.innerHTML = avatar + '
' + renderedContent + '
'; chatBody.appendChild(msgDiv); chatBody.scrollTop = chatBody.scrollHeight; } /* ---- Build Flow ---- */ function handleBuild() { var modal = document.getElementById('emailModal'); var emailInput = document.getElementById('emailInput'); var contactInput = document.getElementById('contactInput'); var errorDiv = document.getElementById('emailError'); // 预填已存储的邮箱 var savedEmail = sessionStorage.getItem('ps_build_email') || ''; var savedContact = sessionStorage.getItem('ps_build_contact') || ''; if (savedEmail) emailInput.value = savedEmail; if (savedContact) contactInput.value = savedContact; errorDiv.style.display = 'none'; modal.style.display = 'flex'; emailInput.focus(); } function closeEmailModal() { document.getElementById('emailModal').style.display = 'none'; document.getElementById('emailError').style.display = 'none'; } /** * 前端邮箱格式校验(与后端 build.js 使用相同正则,双重校验) * @param {string} email - 邮箱地址 * @returns {boolean} 是否合法 */ function validateEmail(email) { var 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])?)*$/; return re.test(email); } async function confirmBuild() { var emailInput = document.getElementById('emailInput'); var contactInput = document.getElementById('contactInput'); var errorDiv = document.getElementById('emailError'); var email = emailInput.value.trim(); var contact = contactInput.value.trim(); // 前端校验 if (!email) { errorDiv.textContent = '请填写邮箱地址'; errorDiv.style.display = 'block'; return; } if (!validateEmail(email)) { errorDiv.textContent = '邮箱格式不正确,请检查'; errorDiv.style.display = 'block'; return; } // 存储邮箱(下次预填) sessionStorage.setItem('ps_build_email', email); if (contact) sessionStorage.setItem('ps_build_contact', contact); closeEmailModal(); appendMessage('system', '🌀 铸渊代理已启动,完成后会发送到 ' + email); // 进入分屏模式 enterDevMode(); // 先连接 WebSocket(确保在 build 开始前建立连接,避免丢失进度消息) connectPreviewWebSocket(); // 提交开发任务(含重试机制) var maxRetries = 2; var retryCount = 0; var submitted = false; while (retryCount <= maxRetries && !submitted) { try { var buildRes = await fetch(API_BASE + '/api/ps/build/start', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ dev_id: DEV_ID, email: email, contact: contact, conversation: conversationHistory, api_base: USER_API_BASE, api_key: USER_API_KEY, model: SELECTED_MODEL }) }); if (!buildRes.ok) { var errMsg = 'HTTP ' + buildRes.status + ' ' + buildRes.statusText; try { var errData = await buildRes.json(); if (errData.message) errMsg = errData.message; } catch (_e) { /* use status text fallback */ } appendMessage('system', '⚠️ 铸渊代理启动失败: ' + errMsg); updatePreviewStatus('error', '启动失败'); } submitted = true; } catch (_err) { retryCount++; if (retryCount <= maxRetries) { appendMessage('system', '⏳ 连接后端服务中,正在重试(' + retryCount + '/' + maxRetries + ')...'); await new Promise(function (r) { setTimeout(r, 1500); }); } else { appendMessage('system', '⚠️ 任务提交失败:无法连接铸渊后端服务。请检查:\n1. 网络连接是否正常\n2. 后端服务是否已启动\n3. 如使用 GitHub Pages 访问,请确认 guanghulab.com 服务可用'); updatePreviewStatus('error', '连接失败'); } } } } /* ---- Dev Mode: Split Screen ---- */ var isDevMode = false; var wsConnection = null; var currentPreviewUrl = ''; function enterDevMode() { if (isDevMode) return; isDevMode = true; var layout = document.getElementById('chatLayout'); var resizer = document.getElementById('resizer'); var previewPanel = document.getElementById('previewPanel'); layout.classList.add('dev-mode'); resizer.style.display = 'block'; previewPanel.style.display = 'flex'; // 初始各占50% var chatMain = document.getElementById('chatMain'); chatMain.style.flex = '1 1 50%'; previewPanel.style.flex = '1 1 50%'; updatePreviewStatus('waiting', '等待中'); initResizer(); } function exitDevMode() { isDevMode = false; var layout = document.getElementById('chatLayout'); var resizer = document.getElementById('resizer'); var previewPanel = document.getElementById('previewPanel'); var chatMain = document.getElementById('chatMain'); layout.classList.remove('dev-mode'); resizer.style.display = 'none'; previewPanel.style.display = 'none'; chatMain.style.flex = ''; if (wsConnection) { wsConnection.close(); wsConnection = null; } } /* ---- Draggable Resizer ---- */ function initResizer() { var resizer = document.getElementById('resizer'); var chatMain = document.getElementById('chatMain'); var previewPanel = document.getElementById('previewPanel'); var layout = document.getElementById('chatLayout'); var startX, startChatWidth, startPreviewWidth; function onMouseDown(e) { e.preventDefault(); startX = e.clientX; startChatWidth = chatMain.getBoundingClientRect().width; startPreviewWidth = previewPanel.getBoundingClientRect().width; resizer.classList.add('resizing'); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } function onMouseMove(e) { var dx = e.clientX - startX; var layoutWidth = layout.getBoundingClientRect().width; var sidebarWidth = document.getElementById('chatSidebar').getBoundingClientRect().width; var resizerWidth = resizer.getBoundingClientRect().width; var available = layoutWidth - sidebarWidth - resizerWidth; var newChatWidth = startChatWidth + dx; var newPreviewWidth = startPreviewWidth - dx; // Enforce min-width 360px if (newChatWidth < 360) newChatWidth = 360; if (newPreviewWidth < 360) newPreviewWidth = 360; if (newChatWidth + newPreviewWidth > available) return; chatMain.style.flex = '0 0 ' + newChatWidth + 'px'; previewPanel.style.flex = '0 0 ' + newPreviewWidth + 'px'; } function onMouseUp() { resizer.classList.remove('resizing'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } resizer.addEventListener('mousedown', onMouseDown); // Touch support for mobile resizer.addEventListener('touchstart', function (e) { var touch = e.touches[0]; startX = touch.clientX; startChatWidth = chatMain.getBoundingClientRect().width; startPreviewWidth = previewPanel.getBoundingClientRect().width; resizer.classList.add('resizing'); function onTouchMove(ev) { var t = ev.touches[0]; var fakeEvent = { clientX: t.clientX }; onMouseMove(fakeEvent); } function onTouchEnd() { resizer.classList.remove('resizing'); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); } document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); }); } /* ---- Preview Panel ---- */ function updatePreviewStatus(status, text) { var statusEl = document.getElementById('previewStatus'); if (!statusEl) return; var dotClass = 'status-dot status-' + status; statusEl.innerHTML = '' + escapeHtml(text) + ''; } function updatePreviewUrl(url) { currentPreviewUrl = url; var frame = document.getElementById('previewFrame'); if (frame) frame.src = url; } function refreshPreview() { var frame = document.getElementById('previewFrame'); if (frame && frame.src) { frame.src = frame.src; } } function openPreviewNewWindow() { if (currentPreviewUrl) { window.open(currentPreviewUrl, '_blank'); } } function setPreviewProjectName(name) { var el = document.getElementById('previewProjectName'); if (el) el.textContent = name || '项目'; } /* ---- WebSocket for Preview Updates ---- */ function connectPreviewWebSocket() { var wsBase = API_BASE.replace(/^http/, 'ws'); var wsUrl = wsBase + '/ws/preview?dev_id=' + encodeURIComponent(DEV_ID); try { wsConnection = new WebSocket(wsUrl); wsConnection.onopen = function () { appendMessage('system', '🔧 正在创建项目骨架...'); updatePreviewStatus('building', '构建中'); }; wsConnection.onmessage = function (event) { try { var data = JSON.parse(event.data); if (data.type === 'progress') { appendMessage('system', data.message || '构建进度更新'); if (data.status) { updatePreviewStatus(data.status, data.status_text || ''); } } if (data.type === 'preview_ready') { var previewUrl = API_BASE + '/api/ps/preview/' + encodeURIComponent(DEV_ID) + '/' + encodeURIComponent(data.project); updatePreviewUrl(previewUrl); setPreviewProjectName(data.project); updatePreviewStatus('done', '完成'); appendMessage('system', '✅ 预览已就绪,右侧可以查看'); } if (data.type === 'reload') { refreshPreview(); } if (data.type === 'complete') { updatePreviewStatus('done', '全部完成'); appendMessage('system', '🎉 全部完成!邮件正在发送'); } if (data.type === 'error') { updatePreviewStatus('error', '出错'); appendMessage('system', '❌ ' + (data.message || '构建出错')); } } catch (_e) { /* ignore malformed WS message */ } }; wsConnection.onerror = function () { // WebSocket not available, graceful degradation updatePreviewStatus('waiting', '离线模式'); }; wsConnection.onclose = function () { wsConnection = null; }; } catch (_e) { // WebSocket connection failed, graceful degradation updatePreviewStatus('waiting', '离线模式'); } } /* ---- Logout ---- */ function handleLogout() { // Save current session before logout var activeId = getActiveSessionId(); if (activeId) { updateSessionMessages(activeId, conversationHistory); } sessionStorage.removeItem('dev_id'); sessionStorage.removeItem('dev_name'); sessionStorage.removeItem('session_token'); sessionStorage.removeItem('login_mode'); sessionStorage.removeItem('user_api_base'); sessionStorage.removeItem('user_api_key'); sessionStorage.removeItem('selected_model'); window.location.href = 'index.html'; } /* ---- Helpers ---- */ function authHeaders(extra) { var headers = {}; if (SESSION_TOKEN) { headers['Authorization'] = 'Bearer ' + SESSION_TOKEN; } if (extra) { Object.keys(extra).forEach(function (k) { headers[k] = extra[k]; }); } return headers; } function escapeHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } /** * Safely render Markdown: marked.parse → DOMPurify sanitize * Falls back to escapeHtml if libraries are unavailable. */ function renderMarkdownSafe(content) { if (typeof marked === 'undefined') return escapeHtml(content); try { var raw = marked.parse(content); if (typeof DOMPurify !== 'undefined') { return DOMPurify.sanitize(raw, { ALLOWED_TAGS: ['h1','h2','h3','h4','h5','h6','p','br','hr','ul','ol','li', 'strong','em','b','i','u','s','del','code','pre','blockquote', 'table','thead','tbody','tr','th','td','a','img','span','div','sup','sub'], ALLOWED_ATTR: ['href','src','alt','title','class','target','rel'], ALLOW_DATA_ATTR: false }); } return raw; } catch (_e) { return escapeHtml(content); } } function autoResizeTextarea(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 120) + 'px'; } /* ---- Textarea auto-resize ---- */ document.getElementById('msgInput').addEventListener('input', function () { autoResizeTextarea(this); }); /* ============================================ 人格体链接系统 · Persona Link System ============================================ */ var activePersonas = []; var personaEndpoints = { shuangyan: { name: '霜砚', icon: '❄️', type: 'notion-agent', checkUrl: '/api/mcp/health', fallbackCheckUrl: '/api/health', description: 'Notion 侧认知 Agent', offlineReason: 'MCP 握手管道 Phase B 尚在调试中,霜砚暂时无法从此界面直接链接。请通过 Notion 侧与霜砚交流。' }, zhuyuan: { name: '铸渊', icon: '🌀', type: 'github-agent', checkUrl: '/api/health', fallbackCheckUrl: '/api/health', description: 'GitHub 侧代码守护 Agent', offlineReason: '铸渊核心服务不可达,请检查服务器状态。' } }; function togglePersonaLink(personaKey) { var btn = document.getElementById('link' + personaKey.charAt(0).toUpperCase() + personaKey.slice(1)); var statusEl = document.getElementById(personaKey + 'Status'); var persona = personaEndpoints[personaKey]; if (!persona) return; var isActive = activePersonas.indexOf(personaKey) >= 0; if (isActive) { // Disconnect activePersonas = activePersonas.filter(function(p) { return p !== personaKey; }); btn.classList.remove('persona-link-active'); statusEl.textContent = '未连接'; statusEl.className = 'link-status'; appendMessage('system', persona.icon + ' ' + persona.name + ' 人格体链接已断开'); updateRouteStatus({ persona: activePersonas.join('+') || '无' }); return; } // Try connecting statusEl.textContent = '连接中…'; statusEl.className = 'link-status link-status-connecting'; btn.classList.add('persona-link-connecting'); // Compatible timeout signal (AbortSignal.timeout may not be available in all browsers) var controller = new AbortController(); var timeoutId = setTimeout(function() { controller.abort(); }, 8000); fetch(API_BASE + persona.checkUrl, { method: 'GET', headers: authHeaders(), signal: controller.signal }) .then(function(res) { clearTimeout(timeoutId); if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(function(data) { // Check Notion connection status for shuangyan var connected = true; var reason = ''; if (personaKey === 'shuangyan') { if (data.services && data.services.notion) { connected = data.services.notion.connected === true; if (!connected) reason = data.services.notion.error || data.services.notion.reason || 'Notion MCP 桥接未连通'; } else { connected = false; reason = persona.offlineReason || 'MCP Notion 模块未加载'; } } if (personaKey === 'zhuyuan') { // zhuyuan checks /api/health — if we got a response, the server is alive if (data.status === 'alive' || data.status === 'ok') { connected = true; } else if (data.services && data.services.github) { connected = data.services.github.connected === true; if (!connected) reason = data.services.github.error || 'GitHub Agent 未连接'; } } if (connected) { activePersonas.push(personaKey); btn.classList.remove('persona-link-connecting'); btn.classList.add('persona-link-active'); statusEl.textContent = '已连接'; statusEl.className = 'link-status link-status-connected'; appendMessage('system', '✅ ' + persona.icon + ' ' + persona.name + ' 人格体链接成功 · ' + persona.description); updateRouteStatus({ persona: activePersonas.join('+') }); } else { btn.classList.remove('persona-link-connecting'); statusEl.textContent = '离线'; statusEl.className = 'link-status link-status-error'; appendMessage('system', '⚠️ ' + persona.icon + ' ' + persona.name + ' 链接状态:' + reason); setTimeout(function() { statusEl.textContent = '未连接'; statusEl.className = 'link-status'; }, 8000); } }) .catch(function(err) { clearTimeout(timeoutId); btn.classList.remove('persona-link-connecting'); statusEl.textContent = '离线'; statusEl.className = 'link-status link-status-error'; var errMsg = persona.offlineReason || ((err.name === 'TimeoutError' || err.name === 'AbortError') ? '服务超时,请检查服务是否运行' : (err.message || '网络异常')); appendMessage('system', '⚠️ ' + persona.icon + ' ' + persona.name + ' 链接状态:' + errMsg); setTimeout(function() { statusEl.textContent = '未连接'; statusEl.className = 'link-status'; }, 8000); }); } /* ---- Route Status Bar ---- */ function updateRouteStatus(info) { if (info.engine) { var engineEl = document.getElementById('routeEngine'); if (engineEl) engineEl.innerHTML = '引擎: ' + escapeHtml(info.engine) + ''; } if (info.model) { var modelEl = document.getElementById('routeModel'); if (modelEl) modelEl.innerHTML = '模型: ' + escapeHtml(info.model) + ''; } if (info.line) { var lineEl = document.getElementById('routeLine'); if (lineEl) lineEl.innerHTML = '线路: ' + escapeHtml(info.line) + ''; } if (info.persona) { var personaEl = document.getElementById('routePersona'); if (personaEl) personaEl.innerHTML = '人格: ' + escapeHtml(info.persona) + ''; } } // Initialize route status (function() { updateRouteStatus({ engine: USE_SERVER_PROXY ? 'server-proxy' : 'api-key', model: SELECTED_MODEL || '--', line: USE_SERVER_PROXY ? 'proxy' : 'direct', persona: '铸渊' }); })(); /* ---- Configure marked.js for safe rendering ---- */ (function() { if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); } })();