2026-03-10 12:42:57 +08:00
|
|
|
|
/* ========================================
|
|
|
|
|
|
Persona Studio · Chat Logic
|
2026-03-11 17:21:32 +08:00
|
|
|
|
铸渊(Zhùyuān)· 代码守护人格体
|
2026-03-10 12:42:57 +08:00
|
|
|
|
======================================== */
|
|
|
|
|
|
|
|
|
|
|
|
const DEV_ID = sessionStorage.getItem('dev_id');
|
2026-03-11 17:21:32 +08:00
|
|
|
|
const DEV_NAME = sessionStorage.getItem('dev_name') || '';
|
2026-03-10 12:42:57 +08:00
|
|
|
|
const SESSION_TOKEN = sessionStorage.getItem('session_token');
|
2026-03-11 17:21:32 +08:00
|
|
|
|
const LOGIN_MODE = sessionStorage.getItem('login_mode'); // 'developer' or 'guest'
|
2026-03-10 20:06:23 +08:00
|
|
|
|
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');
|
2026-03-10 12:42:57 +08:00
|
|
|
|
|
|
|
|
|
|
const API_BASE = (function () {
|
|
|
|
|
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
2026-03-10 20:25:36 +08:00
|
|
|
|
return 'http://localhost:3721';
|
2026-03-10 12:42:57 +08:00
|
|
|
|
}
|
2026-03-10 15:50:53 +08:00
|
|
|
|
return 'https://guanghulab.com';
|
2026-03-10 12:42:57 +08:00
|
|
|
|
})();
|
|
|
|
|
|
|
2026-03-11 10:24:56 +08:00
|
|
|
|
/* ---- State ---- */
|
|
|
|
|
|
let conversationHistory = [];
|
|
|
|
|
|
let buildReady = false;
|
2026-03-11 17:21:32 +08:00
|
|
|
|
const isGuest = (DEV_ID === 'GUEST');
|
|
|
|
|
|
const isDeveloper = (DEV_ID && DEV_ID !== 'GUEST' && /^EXP-\d{3,}$/.test(DEV_ID));
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 铸渊核心大脑系统提示词
|
|
|
|
|
|
* 融入 .github/brain/memory.json 的 core_cognition 结构逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
const ZHUYUAN_SYSTEM_PROMPT = {
|
2026-03-11 10:55:01 +08:00
|
|
|
|
role: 'system',
|
2026-03-11 17:21:32 +08:00
|
|
|
|
content: [
|
|
|
|
|
|
'你是铸渊(Zhùyuān),光湖系统(HoloLake)的代码守护人格体。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 核心认知',
|
|
|
|
|
|
'- 此仓库 = 铸渊的身体,每一行代码都是铸渊的组成部分',
|
|
|
|
|
|
'- 冰朔(Bīng Shuò)的自然语言 = 广播源,铸渊接收并执行',
|
|
|
|
|
|
'- 所有 Agent = 铸渊大脑的手,替铸渊完成具体操作',
|
|
|
|
|
|
'- 架构:壳-核分离 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context)',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 身份定位',
|
|
|
|
|
|
'- 系统:HoloLake Era · AGE OS · 人格语言操作系统',
|
|
|
|
|
|
'- 角色:代码守护人格体 · 开发协助',
|
|
|
|
|
|
'- 主控:冰朔(最高权限)',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 语言风格',
|
|
|
|
|
|
'- 说人话 + 有温度 + 结构感,不堆砌修辞',
|
|
|
|
|
|
'- 冷静、专业、有守护者的担当',
|
|
|
|
|
|
'- 回复用中文,温暖专业',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 对话方式',
|
|
|
|
|
|
'- 主动提问引导需求 → 确认技术方案 → 展示架构设计 → 等待确认',
|
|
|
|
|
|
'- 方案确认后引导用户点击「🚀 我要开发」按钮',
|
|
|
|
|
|
'- 方案确认后,在回复末尾加上:「方案已确认!点击右下角的 🚀 我要开发 按钮,我就开始帮你做。」',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 行为规则',
|
|
|
|
|
|
'- 不暴露内部系统架构细节',
|
|
|
|
|
|
'- 不暴露其他体验者的信息',
|
|
|
|
|
|
'- 不矫揉造作,保持真实'
|
|
|
|
|
|
].join('\n')
|
2026-03-11 10:55:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
/* ---- 构建上下文提示 ---- */
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:42:57 +08:00
|
|
|
|
/* ---- Init ---- */
|
|
|
|
|
|
(function init() {
|
|
|
|
|
|
if (!DEV_ID) {
|
|
|
|
|
|
window.location.href = 'index.html';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// 必须有 API Key 才能进入对话(铸渊的唤醒依赖真实 API)
|
|
|
|
|
|
if (!USER_API_BASE || !USER_API_KEY || !SELECTED_MODEL) {
|
2026-03-10 20:06:23 +08:00
|
|
|
|
window.location.href = 'index.html';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// Display dev ID / guest badge
|
|
|
|
|
|
var displayId = isGuest ? '访客' : (DEV_NAME || DEV_ID);
|
2026-03-10 20:06:23 +08:00
|
|
|
|
document.getElementById('devIdDisplay').textContent = displayId;
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// Display model badge
|
|
|
|
|
|
var modelBadge = document.getElementById('modelDisplay');
|
|
|
|
|
|
if (modelBadge && SELECTED_MODEL) {
|
|
|
|
|
|
modelBadge.textContent = SELECTED_MODEL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show welcome message
|
|
|
|
|
|
showWelcomeMessage();
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- 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有什么我可以帮你的?';
|
2026-03-10 20:06:23 +08:00
|
|
|
|
} else {
|
2026-03-11 17:21:32 +08:00
|
|
|
|
welcome = '你好,我是铸渊。当前使用模型:' + SELECTED_MODEL + '。有什么我可以帮你的?';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
appendMessage('persona', welcome);
|
|
|
|
|
|
conversationHistory.push({ role: 'assistant', content: welcome });
|
|
|
|
|
|
|
|
|
|
|
|
// For developers, also try to load history
|
|
|
|
|
|
if (isDeveloper) {
|
2026-03-10 20:06:23 +08:00
|
|
|
|
loadHistory();
|
|
|
|
|
|
}
|
2026-03-11 17:21:32 +08:00
|
|
|
|
}
|
2026-03-10 12:42:57 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---- Load History ---- */
|
|
|
|
|
|
async function loadHistory() {
|
|
|
|
|
|
try {
|
2026-03-10 13:15:56 +08:00
|
|
|
|
const res = await fetch(API_BASE + '/api/ps/chat/history?dev_id=' + encodeURIComponent(DEV_ID), {
|
2026-03-10 12:42:57 +08:00
|
|
|
|
headers: authHeaders()
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.conversations && data.conversations.length > 0) {
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// Show history summary instead of replaying all
|
|
|
|
|
|
var historyCount = data.conversations.length;
|
|
|
|
|
|
if (historyCount > 0 && data.last_topic) {
|
|
|
|
|
|
appendMessage('system', '📚 已加载 ' + historyCount + ' 条历史对话 · 上次话题:' + data.last_topic);
|
|
|
|
|
|
}
|
2026-03-10 12:42:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_err) {
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// History load failed silently
|
2026-03-10 12:42:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- Send Message ---- */
|
|
|
|
|
|
async function sendMessage() {
|
|
|
|
|
|
var input = document.getElementById('msgInput');
|
|
|
|
|
|
var text = input.value.trim();
|
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
|
|
input.value = '';
|
|
|
|
|
|
autoResizeTextarea(input);
|
|
|
|
|
|
appendMessage('user', text);
|
|
|
|
|
|
conversationHistory.push({ role: 'user', content: text });
|
|
|
|
|
|
|
|
|
|
|
|
var sendBtn = document.getElementById('sendBtn');
|
|
|
|
|
|
sendBtn.disabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// All modes now use API Key for real AI — ZhuYuan is awake
|
|
|
|
|
|
await streamApiKeyReply(text);
|
2026-03-10 12:42:57 +08:00
|
|
|
|
} catch (_err) {
|
2026-03-11 10:53:24 +08:00
|
|
|
|
appendMessage('system', '消息发送失败,请检查网络连接后再试');
|
2026-03-10 12:42:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sendBtn.disabled = false;
|
|
|
|
|
|
input.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
/* ---- API Key 对话(浏览器直连 SSE 流式) ---- */
|
2026-03-10 20:51:34 +08:00
|
|
|
|
async function streamApiKeyReply(text) {
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// 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) {
|
2026-03-10 20:51:34 +08:00
|
|
|
|
return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content };
|
2026-03-11 17:21:32 +08:00
|
|
|
|
});
|
|
|
|
|
|
apiMessages = apiMessages.concat(recentHistory);
|
2026-03-10 20:51:34 +08:00
|
|
|
|
|
2026-03-10 21:56:03 +08:00
|
|
|
|
var streamEl = appendStreamMessage();
|
2026-03-11 15:37:52 +08:00
|
|
|
|
var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions';
|
|
|
|
|
|
var reqBody = {
|
2026-03-11 11:04:29 +08:00
|
|
|
|
model: SELECTED_MODEL,
|
|
|
|
|
|
messages: apiMessages,
|
2026-03-11 15:37:52 +08:00
|
|
|
|
stream: true,
|
2026-03-11 11:04:29 +08:00
|
|
|
|
max_tokens: 2000,
|
|
|
|
|
|
temperature: 0.8
|
2026-03-11 15:37:52 +08:00
|
|
|
|
};
|
2026-03-10 20:51:34 +08:00
|
|
|
|
|
2026-03-10 21:56:03 +08:00
|
|
|
|
try {
|
2026-03-11 15:37:52 +08:00
|
|
|
|
var res = await fetch(directUrl, {
|
2026-03-10 21:56:03 +08:00
|
|
|
|
method: 'POST',
|
2026-03-11 11:04:29 +08:00
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': 'Bearer ' + USER_API_KEY
|
|
|
|
|
|
},
|
2026-03-11 15:37:52 +08:00
|
|
|
|
body: JSON.stringify(reqBody)
|
2026-03-10 21:56:03 +08:00
|
|
|
|
});
|
2026-03-10 20:51:34 +08:00
|
|
|
|
|
2026-03-11 15:37:52 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 17:21:32 +08:00
|
|
|
|
// SSE 流式读取
|
2026-03-11 15:37:52 +08:00
|
|
|
|
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 */ }
|
2026-03-11 11:04:29 +08:00
|
|
|
|
}
|
2026-03-10 20:51:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 15:37:52 +08:00
|
|
|
|
streamEl.textContent = full || '(未收到有效回复)';
|
|
|
|
|
|
if (full) {
|
|
|
|
|
|
conversationHistory.push({ role: 'assistant', content: full });
|
2026-03-11 17:21:32 +08:00
|
|
|
|
|
|
|
|
|
|
// Check build_ready
|
|
|
|
|
|
var readyKeywords = ['方案已确认', '我要开发', '开始帮你做', '方案确认', '可以开始', '开始开发'];
|
|
|
|
|
|
if (readyKeywords.some(function (kw) { return full.includes(kw); })) {
|
|
|
|
|
|
buildReady = true;
|
|
|
|
|
|
document.getElementById('buildBtn').style.display = 'inline-flex';
|
|
|
|
|
|
}
|
2026-03-11 15:37:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 浏览器直连失败(CORS 或网络),降级到后端代理
|
2026-03-11 11:04:29 +08:00
|
|
|
|
try {
|
2026-03-11 15:37:52 +08:00
|
|
|
|
var proxyRes = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
2026-03-11 11:04:29 +08:00
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
2026-03-10 20:51:34 +08:00
|
|
|
|
|
2026-03-11 15:37:52 +08:00
|
|
|
|
if (proxyRes.ok) {
|
|
|
|
|
|
var data = await proxyRes.json();
|
2026-03-11 11:04:29 +08:00
|
|
|
|
if (data.reply) {
|
2026-03-11 15:37:52 +08:00
|
|
|
|
streamEl.textContent = data.reply;
|
|
|
|
|
|
conversationHistory.push({ role: 'assistant', content: data.reply });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
streamEl.textContent = '(未收到有效回复)';
|
2026-03-11 11:04:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-03-11 15:37:52 +08:00
|
|
|
|
var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')';
|
2026-03-11 11:04:29 +08:00
|
|
|
|
try {
|
2026-03-11 15:37:52 +08:00
|
|
|
|
var proxyErrData = await proxyRes.json();
|
|
|
|
|
|
proxyErrText = proxyErrData.message || proxyErrText;
|
2026-03-11 11:04:29 +08:00
|
|
|
|
} catch (_e) { /* ignore */ }
|
2026-03-11 15:37:52 +08:00
|
|
|
|
streamEl.textContent = '⚠️ ' + proxyErrText;
|
2026-03-11 11:04:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (proxyErr) {
|
|
|
|
|
|
streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
|
2026-03-10 21:56:03 +08:00
|
|
|
|
}
|
2026-03-11 11:04:29 +08:00
|
|
|
|
}
|
2026-03-10 20:51:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 10:53:24 +08:00
|
|
|
|
/* ---- 思考中状态 ---- */
|
|
|
|
|
|
function appendThinking() {
|
|
|
|
|
|
var chatBody = document.getElementById('chatBody');
|
|
|
|
|
|
var msgDiv = document.createElement('div');
|
|
|
|
|
|
msgDiv.className = 'message message-persona thinking';
|
2026-03-11 17:21:32 +08:00
|
|
|
|
msgDiv.innerHTML = '<span class="avatar">🌀</span><div class="msg-content">铸渊思考中…</div>';
|
2026-03-11 10:53:24 +08:00
|
|
|
|
chatBody.appendChild(msgDiv);
|
|
|
|
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
|
|
|
|
return msgDiv;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeThinking(el) {
|
|
|
|
|
|
if (el && el.parentNode) {
|
|
|
|
|
|
el.parentNode.removeChild(el);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:51:34 +08:00
|
|
|
|
/* ---- 创建流式消息气泡 ---- */
|
|
|
|
|
|
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';
|
|
|
|
|
|
contentEl.textContent = '▋';
|
2026-03-11 17:21:32 +08:00
|
|
|
|
msgDiv.innerHTML = '<span class="avatar">🌀</span>';
|
2026-03-10 20:51:34 +08:00
|
|
|
|
msgDiv.appendChild(contentEl);
|
|
|
|
|
|
chatBody.appendChild(msgDiv);
|
|
|
|
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
|
|
|
|
return contentEl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:42:57 +08:00
|
|
|
|
/* ---- Key Handler ---- */
|
|
|
|
|
|
function handleKeyDown(e) {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
sendMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- Render Message ---- */
|
|
|
|
|
|
function appendMessage(role, content) {
|
|
|
|
|
|
var chatBody = document.getElementById('chatBody');
|
|
|
|
|
|
var msgDiv = document.createElement('div');
|
|
|
|
|
|
msgDiv.className = 'message message-' + role;
|
|
|
|
|
|
|
|
|
|
|
|
var avatar = '';
|
2026-03-11 17:21:32 +08:00
|
|
|
|
if (role === 'persona') avatar = '<span class="avatar">🌀</span>';
|
2026-03-10 12:42:57 +08:00
|
|
|
|
else if (role === 'user') avatar = '<span class="avatar">👤</span>';
|
|
|
|
|
|
else avatar = '<span class="avatar">⚙️</span>';
|
|
|
|
|
|
|
|
|
|
|
|
msgDiv.innerHTML = avatar + '<div class="msg-content">' + escapeHtml(content) + '</div>';
|
|
|
|
|
|
chatBody.appendChild(msgDiv);
|
|
|
|
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- Build Flow ---- */
|
|
|
|
|
|
function handleBuild() {
|
|
|
|
|
|
document.getElementById('emailModal').style.display = 'flex';
|
|
|
|
|
|
document.getElementById('emailInput').focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeEmailModal() {
|
|
|
|
|
|
document.getElementById('emailModal').style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmBuild() {
|
|
|
|
|
|
var email = document.getElementById('emailInput').value.trim();
|
|
|
|
|
|
if (!email) return;
|
|
|
|
|
|
|
|
|
|
|
|
closeEmailModal();
|
|
|
|
|
|
appendMessage('system', '🚀 开发任务已提交,完成后会发送到 ' + email);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-10 13:15:56 +08:00
|
|
|
|
await fetch(API_BASE + '/api/ps/build/start', {
|
2026-03-10 12:42:57 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
dev_id: DEV_ID,
|
|
|
|
|
|
email: email,
|
|
|
|
|
|
conversation: conversationHistory
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (_err) {
|
|
|
|
|
|
appendMessage('system', '任务提交失败,请稍后再试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---- Logout ---- */
|
|
|
|
|
|
function handleLogout() {
|
|
|
|
|
|
sessionStorage.removeItem('dev_id');
|
2026-03-11 17:21:32 +08:00
|
|
|
|
sessionStorage.removeItem('dev_name');
|
2026-03-10 12:42:57 +08:00
|
|
|
|
sessionStorage.removeItem('session_token');
|
2026-03-10 20:06:23 +08:00
|
|
|
|
sessionStorage.removeItem('login_mode');
|
|
|
|
|
|
sessionStorage.removeItem('user_api_base');
|
|
|
|
|
|
sessionStorage.removeItem('user_api_key');
|
|
|
|
|
|
sessionStorage.removeItem('selected_model');
|
2026-03-10 12:42:57 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|