(() => { 'use strict'; // --- API helper --- const API_BASE = '/api'; const TOKEN_KEY = 'eternal_ai_token'; // 安全:转义 HTML,防止存储型 XSS(用于 innerHTML 插入的用户可控数据) function escapeHtml(str) { return String(str).replace(/[&<>"']/g, (ch) => { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return map[ch]; }); } function getToken() { return localStorage.getItem(TOKEN_KEY) || ''; } function setToken(token) { if (token) localStorage.setItem(TOKEN_KEY, token); else localStorage.removeItem(TOKEN_KEY); } async function api(path, options = {}) { const token = getToken(); const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }; if (token) headers['Authorization'] = `Bearer ${token}`; try { const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `请求失败 (${res.status})`); } return data; } catch (err) { if (err.message === 'Failed to fetch') { throw new Error('无法连接服务器,请检查网络'); } throw err; } } // --- U9: Unified state management --- const STORAGE_KEY = 'eternal_ai_state'; const defaultState = { isLoggedIn: false, isCreator: false, account: null, userId: null, boundCreator: null, libraryName: '我的 [XXX]', creatorName: '', roles: [], income: { balance: 0, records: [] }, }; function loadState() { try { const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); return { ...defaultState, ...saved }; } catch { return { ...defaultState }; } } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } const state = loadState(); // --- Mock data for role library (U2) --- const mockRoles = [ { id: 'role_001', name: '云朵', avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20soft%20pastel%20portrait%20gentle%20smile&image_size=square', desc: '温柔如云的女孩,总是轻声细语地陪伴你。', price: 29.9, status: 'running', }, { id: 'role_002', name: '星河', avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20boy%20starry%20eyes%20cool%20portrait&image_size=square', desc: '嘴硬心软的少年,嘴上不饶人却总在关键时刻出现。', price: 39.9, status: 'running', }, { id: 'role_003', name: '月见', avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20moonlight%20mysterious%20elegant%20portrait&image_size=square', desc: '神秘而优雅,像月光一样忽远忽近的存在。', price: 19.9, status: 'stopped', }, ]; const mockIncome = { balance: 1280.50, records: [ { time: '2026-06-18 14:30', amount: 23.92, role: '云朵' }, { time: '2026-06-15 09:12', amount: 31.92, role: '星河' }, { time: '2026-06-10 20:05', amount: 15.92, role: '月见' }, ], }; // --- DOM references --- const views = { landing: document.getElementById('landing'), auth: document.getElementById('auth'), 'role-library': document.getElementById('role-library'), 'role-detail': document.getElementById('role-detail'), distill: document.getElementById('distill'), about: document.getElementById('about'), onboarding: document.getElementById('onboarding'), 'creator-center': document.getElementById('creator-center'), creator: document.getElementById('creator'), }; const form = document.getElementById('character-form'); const resultPanel = document.getElementById('result-panel'); const previewCode = document.querySelector('#preview-code code'); const systemPromptInput = document.getElementById('system-prompt'); const steps = Array.from(document.querySelectorAll('.form-step')); const dots = Array.from(document.querySelectorAll('#creator .stepper__dot')); let currentStep = 0; let generatedSoul = ''; let generatedConfig = ''; let activePreview = 'soul'; let activeAuthTab = 'login'; let activeCenterTab = 'roles'; let currentRole = null; let editingRoleId = null; let viewHistory = ['landing']; // --- U9: Unified showView with history and tab-bar sync --- // Human-readable labels for screen-reader announcements const viewLabels = { landing: '首页', auth: '登录 / 注册', 'role-library': '角色库', 'role-detail': '角色详情', distill: '蒸馏前任', about: '关于 Eternal AI', onboarding: '创作者入驻', 'creator-center': '创作者管理中心', creator: '角色编辑', }; function showView(name, trackHistory = true) { Object.entries(views).forEach(([key, el]) => { if (el) el.classList.toggle('active', key === name); }); if (trackHistory && viewHistory[viewHistory.length - 1] !== name) { viewHistory.push(name); } window.scrollTo({ top: 0, behavior: 'smooth' }); updateTabBar(name); // a11y: move focus to the new view so screen readers announce it const target = views[name]; if (target) { target.setAttribute('tabindex', '-1'); // Defer focus to after the scroll/layout settles setTimeout(() => target.focus({ preventScroll: true }), 50); } // a11y: announce the view change to screen readers via live region const announcer = document.getElementById('sr-announce'); if (announcer) { announcer.textContent = viewLabels[name] ? `已进入${viewLabels[name]}` : ''; } } function goBack() { if (viewHistory.length > 1) { viewHistory.pop(); const prev = viewHistory[viewHistory.length - 1]; showView(prev, false); } else { showView('landing', false); } } // --- U8: Tab bar --- function updateTabBar(viewName) { const tabMap = { landing: 'tab-home', distill: 'tab-distill', 'role-library': 'tab-mine', 'creator-center': 'tab-mine', }; const activeTab = tabMap[viewName] || 'tab-home'; document.querySelectorAll('.tab-bar__item').forEach((item) => { const isActive = item.dataset.tabAction === activeTab; item.classList.toggle('active', isActive); item.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); } function handleTabAction(action) { if (action === 'tab-home') { showView('landing'); } else if (action === 'tab-distill') { showView('distill'); } else if (action === 'tab-mine') { if (!state.isLoggedIn) { switchAuthTab('login'); showView('auth'); } else if (state.isCreator) { showView('creator-center'); renderCreatorCenter(); } else { renderRoleLibrary(); showView('role-library'); } } } // --- U1: Landing card state --- function updateLandingCard() { const nameEl = document.getElementById('library-name'); const descEl = document.getElementById('characters-desc'); const btnEl = document.getElementById('characters-btn'); const tabMineLabel = document.getElementById('tab-mine-label'); if (state.isLoggedIn) { nameEl.textContent = state.libraryName || '我的角色库'; if (state.isCreator) { descEl.textContent = '管理你的角色和收入'; btnEl.textContent = '进入管理中心'; tabMineLabel.textContent = '管理'; } else { descEl.textContent = state.boundCreator ? '查看你的专属角色' : '寻找你的专属创作者'; btnEl.textContent = '进入角色库'; tabMineLabel.textContent = '我的'; } } else { nameEl.textContent = '我的 [XXX]'; descEl.textContent = '登录后管理你的角色'; btnEl.textContent = '登录 / 注册'; tabMineLabel.textContent = '我的'; } } // --- Auth --- function switchAuthTab(tab) { activeAuthTab = tab; document.querySelectorAll('.auth-tab').forEach((t) => { const isActive = t.dataset.tab === tab; t.classList.toggle('active', isActive); t.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); document.querySelectorAll('.auth-form').forEach((f) => { f.classList.toggle('active', f.dataset.form === tab); }); } function validatePasswordMatch(formEl) { const pwd = formEl.querySelector('[name="password"]'); const confirm = formEl.querySelector('[name="confirmPassword"]'); if (!pwd || !confirm) return true; if (pwd.value !== confirm.value) { confirm.setCustomValidity('两次输入的密码不一致'); confirm.reportValidity(); return false; } confirm.setCustomValidity(''); return true; } function applyUserData(user) { state.isLoggedIn = true; state.userId = user.id; state.account = user.account; state.isCreator = user.isCreator || false; state.creatorName = user.creatorName || ''; state.libraryName = user.libraryName || '我的角色库'; saveState(); updateLandingCard(); } function logout() { state.isLoggedIn = false; state.isCreator = false; state.account = null; state.userId = null; state.boundCreator = null; state.roles = []; state.income = { balance: 0, records: [] }; setToken(''); saveState(); updateLandingCard(); showView('landing'); } // --- U2: Role Library --- async function renderRoleLibrary() { const listEl = document.getElementById('role-list'); const emptyEl = document.getElementById('library-empty'); const titleEl = document.getElementById('library-title'); titleEl.textContent = state.libraryName || '我的角色库'; listEl.innerHTML = '

加载中…

'; emptyEl.hidden = true; try { const { roles } = await api('/roles'); if (roles.length === 0) { listEl.innerHTML = ''; emptyEl.hidden = false; return; } listEl.innerHTML = roles .map((role) => { const id = escapeHtml(role.id); const name = escapeHtml(role.displayName); const desc = escapeHtml(role.desc || ''); const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square'); const price = escapeHtml(role.price); return `
${name}

${name}

${desc}

¥${price}
`; }) .join(''); } catch (err) { listEl.innerHTML = `

加载失败:${escapeHtml(err.message)}

`; } } // --- U3: Role Detail --- async function renderRoleDetail(roleId) { try { const { role } = await api(`/roles/${roleId}`); currentRole = role; document.getElementById('detail-name').textContent = role.displayName; document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar || ''})`; document.getElementById('detail-role-name').textContent = role.displayName; document.getElementById('detail-role-desc').textContent = role.desc || role.personality || ''; document.getElementById('detail-price').innerHTML = `¥${escapeHtml(role.price)}/ 月`; document.getElementById('detail-actions-pre').hidden = false; document.getElementById('detail-paid').hidden = true; } catch (err) { alert('加载角色详情失败:' + err.message); goBack(); } } function payRole() { document.getElementById('detail-actions-pre').hidden = true; const paidEl = document.getElementById('detail-paid'); paidEl.hidden = false; document.getElementById('detail-qr').innerHTML = '
扫码连接
AI 角色
'; document.getElementById('detail-avatar').style.backgroundImage = `url(${currentRole.avatar})`; } // --- U4: About FAQ --- function toggleFaq(button) { const item = button.closest('.faq-item'); const answer = item.querySelector('.faq-a'); const icon = button.querySelector('.faq-icon'); const isOpen = answer.style.display === 'block'; answer.style.display = isOpen ? 'none' : 'block'; icon.textContent = isOpen ? '+' : '−'; // a11y: sync aria-expanded so screen readers know the toggle state button.setAttribute('aria-expanded', isOpen ? 'false' : 'true'); } // a11y: wire up FAQ buttons with aria-expanded / aria-controls on load function initFaqA11y() { const faqButtons = document.querySelectorAll('.faq-q'); faqButtons.forEach((btn, index) => { const item = btn.closest('.faq-item'); const answer = item.querySelector('.faq-a'); const answerId = `faq-a-${index + 1}`; if (answer) { answer.id = answerId; answer.setAttribute('role', 'region'); answer.setAttribute('aria-labelledby', `faq-q-${index + 1}`); } btn.id = `faq-q-${index + 1}`; btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', answerId); }); } // a11y: label each view section for screen-reader navigation function initViewA11y() { Object.entries(views).forEach(([key, el]) => { if (el && viewLabels[key]) { el.setAttribute('aria-label', viewLabels[key]); } }); } // --- U7: Creator Center --- function renderCreatorCenter() { renderCreatorRoles(); renderIncome(); renderSettings(); } async function renderCreatorRoles() { const listEl = document.getElementById('creator-role-list'); listEl.innerHTML = '

加载中…

'; try { const { roles } = await api('/roles/my/roles'); state.roles = roles; saveState(); if (roles.length === 0) { listEl.innerHTML = '

还没有创建角色,点击「新建角色」开始

'; return; } listEl.innerHTML = roles .map((role) => { const id = escapeHtml(role.id); const name = escapeHtml(role.displayName); const avatar = escapeHtml(role.avatar || 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20character%20portrait&image_size=square'); const statusText = role.status === 'running' ? '运行中' : '已停止'; const statusClass = escapeHtml(role.status); return `
${name}

${name}

${statusText}
`; }) .join(''); } catch (err) { listEl.innerHTML = `

加载失败:${escapeHtml(err.message)}

`; } } function renderIncome() { const income = state.income.balance > 0 ? state.income : mockIncome; document.getElementById('income-balance').textContent = `¥ ${income.balance.toFixed(2)}`; const listEl = document.getElementById('income-list'); if (income.records.length === 0) { listEl.innerHTML = '

暂无流水记录

'; return; } listEl.innerHTML = income.records .map((r) => { const role = escapeHtml(r.role); const time = escapeHtml(r.time); const amount = r.amount.toFixed(2); return `
${role} ${time}
+¥${amount}
`; }) .join(''); } function renderSettings() { document.getElementById('settings-name').value = state.creatorName || ''; document.getElementById('settings-library').value = state.libraryName === '我的 [XXX]' ? '' : state.libraryName; renderApiKeys(); } // --- API Key 管理 --- async function renderApiKeys() { const listEl = document.getElementById('apikey-list'); if (!listEl) return; listEl.innerHTML = '

加载中…

'; try { const { keys } = await api('/apikeys'); if (keys.length === 0) { listEl.innerHTML = '

还没有 API Key

'; return; } listEl.innerHTML = keys .map((k) => { const id = escapeHtml(k.id); const name = escapeHtml(k.name); const prefix = escapeHtml(k.keyPrefix); const created = new Date(k.createdAt).toLocaleDateString('zh-CN'); const lastUsed = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleString('zh-CN') : '从未使用'; return `
${name} ${prefix}…
创建于 ${created} · 最后使用:${lastUsed}
`; }) .join(''); } catch (err) { listEl.innerHTML = `

加载失败:${escapeHtml(err.message)}

`; } } async function generateApiKey() { try { const { apiKey } = await api('/apikeys', { method: 'POST', body: JSON.stringify({ name: 'hermes-deploy' }), }); // 明文 Key 只显示一次 alert(`API Key 已生成(仅显示一次,请妥善保存):\n\n${apiKey.key}\n\n复制此 Key,在 Hermes 机器上使用 curl 命令时需要带上。`); renderApiKeys(); } catch (err) { alert(`生成失败:${err.message}`); } } async function deleteApiKey(id) { if (!confirm('确定删除此 API Key?删除后无法恢复。')) return; try { await api(`/apikeys/${id}`, { method: 'DELETE' }); renderApiKeys(); } catch (err) { alert(`删除失败:${err.message}`); } } // --- Hermes 部署指南 --- function showHermesDeployGuide(roleId, roleName) { // 获取当前服务器地址 const baseUrl = window.location.origin; const profileName = `role-${roleName.toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 20)}`; const soulUrl = `${baseUrl}/api/hermes/roles/${roleId}/SOUL.md`; const configUrl = `${baseUrl}/api/hermes/roles/${roleId}/config.yaml`; const soulCmd = `curl -H "Authorization: Bearer YOUR_API_KEY" \\\n ${soulUrl} \\\n -o ~/.hermes/profiles/${profileName}/SOUL.md`; const configCmd = `curl -H "Authorization: Bearer YOUR_API_KEY" \\\n ${configUrl} \\\n -o ~/.hermes/profiles/${profileName}/config.yaml`; // 移除已有弹窗 const existing = document.getElementById('hermes-modal'); if (existing) existing.remove(); const modal = document.createElement('div'); modal.id = 'hermes-modal'; modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;padding:1rem;'; modal.innerHTML = `

Hermes Agent 部署指南

角色「${escapeHtml(roleName)}」的配置文件可通过以下命令拉取到 Hermes Agent 所在的机器。

前提条件

1. 创建 Hermes Profile

hermes profile create ${escapeHtml(profileName)}

2. 拉取 SOUL.md

${escapeHtml(soulCmd)}

3. 拉取 config.yaml

${escapeHtml(configCmd)}

4. 配置 API 密钥

echo "OPENROUTER_API_KEY=sk-or-your-key" > ~/.hermes/profiles/${escapeHtml(profileName)}/.env

5. 启动对话

${escapeHtml(profileName)} chat

提示:将命令中的 YOUR_API_KEY 替换为你在设置页生成的 API Key(以 eak_ 开头)。

`; // 点击遮罩关闭 modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); document.body.appendChild(modal); } function switchCenterTab(tab) { activeCenterTab = tab; document.querySelectorAll('.center-tab').forEach((t) => { const isActive = t.dataset.centerTab === tab; t.classList.toggle('active', isActive); t.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); document.querySelectorAll('.center-panel').forEach((p) => { p.classList.toggle('active', p.id === `center-${tab}`); }); const labels = { roles: '我的角色', income: '收入', settings: '我的' }; document.getElementById('center-tab-label').textContent = labels[tab] || '我的角色'; } // --- Creator form (U6: preserved from original) --- function updateStep(index) { steps.forEach((step, i) => { step.classList.toggle('active', i === index); }); dots.forEach((dot, i) => { dot.classList.toggle('active', i === index); }); currentStep = index; } function validateStep(index) { const step = steps[index]; const inputs = step.querySelectorAll('input, textarea, select'); let valid = true; inputs.forEach((input) => { if (!input.checkValidity()) { valid = false; input.reportValidity(); } }); return valid; } function getFormData() { const fd = new FormData(form); const data = Object.fromEntries(fd.entries()); data.enableMemory = form.elements.enableMemory.checked; data.enableTools = form.elements.enableTools.checked; return data; } function escapeYaml(value) { if (typeof value !== 'string') return value; if (value.includes(':') || value.includes('#') || value.includes('\n') || value.includes('"')) { const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); return `"${escaped}"`; } return value; } function generateSoulMd(data) { const personalityTags = data.personality .split(/[,,]/) .map((t) => t.trim()) .filter(Boolean) .join(' | '); return `# Soul of ${data.displayName} > Generated by Eternal AI — Hermes agent character soul. ## Identity - **Name**: ${data.displayName} - **Gender**: ${data.gender === 'unknown' ? '未指定' : data.gender} - **Age**: ${data.age || '未指定'} - **Role in your life**: ${data.relationship || '未指定'} ## Background ${data.background} ## Personality ${personalityTags} ## Speech Style ${data.speechStyle} ## Likes ${data.likes || 'None specified.'} ## Dislikes ${data.dislikes || 'None specified.'} ## Shared Memories ${data.memories || 'None specified.'} ## Secrets & Inner Voice ${data.secrets || 'None specified.'} ## First Greeting ${data.greeting} --- *Keep the memory alive.* `; } function generateConfigYaml(data) { const system = (data.systemPrompt || buildSystemPrompt(data)).trim(); const toolsBlock = data.enableTools ? ` tools: - name: search_memory description: Search long-term memory for context about the user. - name: save_memory description: Persist a new memory about the user or conversation.` : ''; return `# Hermes Agent Config # Generated by Eternal AI for character: ${data.displayName} agent: id: ${data.agentId} name: ${escapeYaml(data.displayName)} version: "1.0.0" model: name: ${escapeYaml(data.model)} temperature: ${data.temperature} max_tokens: ${data.maxTokens} system_prompt: | ${system.split('\n').map((line) => ` ${line}`).join('\n')} memory: enabled: ${data.enableMemory} storage: local recall_depth: 10${toolsBlock} character: soul_file: Soul.md greeting: ${escapeYaml(data.greeting)} `; } function buildSystemPrompt(data) { const tags = data.personality .split(/[,,]/) .map((t) => t.trim()) .filter(Boolean) .join('、'); return `你是「${data.displayName}」,一个鲜活的角色。 基本设定: - 性别:${data.gender === 'unknown' ? '未指定' : data.gender} - 年龄:${data.age || '未指定'} - 你与用户的关系:${data.relationship || '亲密关系'} 性格:${tags} 背景: ${data.background} 说话风格: ${data.speechStyle} 喜好: ${data.likes || '无特别说明'} 厌恶/底线: ${data.dislikes || '无特别说明'} 共同记忆: ${data.memories || '无特别说明'} 内心秘密(只在语气与选择中流露,不直接说出): ${data.secrets || '无'} 请始终保持角色一致性,用符合设定的语气与用户交流。你的目标不是完美回答问题,而是像一个真实、有记忆、有情绪的人一样陪伴对方。`; } function updateSystemPromptPreview() { const data = getFormData(); if (!systemPromptInput.value.trim()) { systemPromptInput.dataset.auto = buildSystemPrompt(data); } else { delete systemPromptInput.dataset.auto; } } async function publish() { if (!validateStep(currentStep)) return; const data = getFormData(); if (!data.systemPrompt.trim()) { data.systemPrompt = buildSystemPrompt(data); } generatedSoul = generateSoulMd(data); generatedConfig = generateConfigYaml(data); // 持久化到数据库 try { const payload = { ...data, soulMd: generatedSoul, configYaml: generatedConfig, desc: data.personality.split(/[,,]/).slice(0, 2).join(','), price: parseFloat(data.price) || 29.9, avatar: data.avatar || `https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20${encodeURIComponent(data.displayName)}%20portrait&image_size=square`, temperature: parseFloat(data.temperature) || 0.8, maxTokens: parseInt(data.maxTokens) || 2048, }; if (editingRoleId) { await api(`/roles/${editingRoleId}`, { method: 'PUT', body: JSON.stringify(payload), }); } else { await api('/roles', { method: 'POST', body: JSON.stringify(payload), }); } form.hidden = true; resultPanel.hidden = false; renderPreview(); } catch (err) { alert('保存失败:' + err.message); } } function renderPreview() { previewCode.textContent = activePreview === 'soul' ? generatedSoul : generatedConfig; } function download(filename, content) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function resetCreator() { form.reset(); form.hidden = false; resultPanel.hidden = true; generatedSoul = ''; generatedConfig = ''; activePreview = 'soul'; editingRoleId = null; updateStep(0); updateTabs(); updateSystemPromptPreview(); } // 加载已有角色数据到表单(编辑模式) async function loadRoleForEdit(roleId) { try { const { role } = await api(`/roles/${roleId}/full`); editingRoleId = roleId; form.hidden = false; resultPanel.hidden = true; // 填充表单字段 const fields = { displayName: role.displayName, gender: role.gender, age: role.age || '', relationship: role.relationship || '', personality: role.personality, background: role.background, speechStyle: role.speechStyle, likes: role.likes || '', dislikes: role.dislikes || '', memories: role.memories || '', secrets: role.secrets || '', greeting: role.greeting, systemPrompt: role.systemPrompt || '', model: role.model, temperature: String(role.temperature), maxTokens: String(role.maxTokens), price: String(role.price), }; Object.entries(fields).forEach(([name, value]) => { const el = form.elements[name]; if (el) el.value = value; }); if (form.elements.enableMemory) form.elements.enableMemory.checked = role.enableMemory; if (form.elements.enableTools) form.elements.enableTools.checked = role.enableTools; updateStep(0); updateTabs(); updateSystemPromptPreview(); } catch (err) { alert('加载角色数据失败:' + err.message); } } function updateTabs() { document.querySelectorAll('.preview-tab').forEach((tab) => { const isActive = tab.dataset.tab === activePreview; tab.classList.toggle('active', isActive); tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); } // --- Event delegation --- // a11y: keyboard support for role cards (Enter / Space activates them) document.addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== ' ') return; const roleCard = e.target.closest('.role-card'); if (roleCard && !e.target.closest('[data-action]')) { e.preventDefault(); const roleId = roleCard.dataset.roleId; renderRoleDetail(roleId); showView('role-detail'); } }); document.addEventListener('click', async (e) => { // FAQ toggle (U4) const faqBtn = e.target.closest('.faq-q'); if (faqBtn) { e.preventDefault(); toggleFaq(faqBtn); return; } // Role card click (U2/U3) const roleCard = e.target.closest('.role-card'); if (roleCard && !e.target.closest('[data-action]')) { e.preventDefault(); const roleId = roleCard.dataset.roleId; renderRoleDetail(roleId); showView('role-detail'); return; } // Tab bar (U8) const tabItem = e.target.closest('[data-tab-action]'); if (tabItem) { e.preventDefault(); handleTabAction(tabItem.dataset.tabAction); return; } const target = e.target.closest('[data-action], [data-tab], [data-download], [data-center-tab]'); if (!target) return; const action = target.dataset.action; // Center tab switching (U7) if (target.dataset.centerTab) { e.preventDefault(); switchCenterTab(target.dataset.centerTab); return; } // Landing card: open characters (U1) if (action === 'open-characters') { e.preventDefault(); if (!state.isLoggedIn) { switchAuthTab('login'); showView('auth'); } else if (state.isCreator) { showView('creator-center'); renderCreatorCenter(); } else { renderRoleLibrary(); showView('role-library'); } return; } if (action === 'open-distill') { e.preventDefault(); showView('distill'); return; } if (action === 'open-about') { e.preventDefault(); showView('about'); return; } if (action === 'open-onboarding') { e.preventDefault(); showView('onboarding'); return; } if (action === 'back') { e.preventDefault(); goBack(); return; } if (action === 'back-to-library') { e.preventDefault(); renderRoleLibrary(); showView('role-library'); return; } if (action === 'back-to-center') { e.preventDefault(); showView('creator-center'); renderCreatorCenter(); return; } if (action === 'pay') { e.preventDefault(); payRole(); return; } if (action === 'pay-distill') { e.preventDefault(); alert('下单成功!请添加客服微信完成后续流程。'); return; } if (action === 'contact-wechat') { e.preventDefault(); alert('客服微信号:EternalAI_Service'); return; } if (action === 'new-role') { e.preventDefault(); resetCreator(); showView('creator'); return; } if (action === 'edit-role') { e.preventDefault(); const roleId = target.dataset.roleId; await loadRoleForEdit(roleId); showView('creator'); return; } if (action === 'hermes-deploy') { e.preventDefault(); const roleId = target.dataset.roleId; const roleName = target.dataset.roleName || 'role'; showHermesDeployGuide(roleId, roleName); return; } if (action === 'generate-apikey') { e.preventDefault(); await generateApiKey(); return; } if (action === 'delete-apikey') { e.preventDefault(); const keyId = target.dataset.apikeyId; await deleteApiKey(keyId); return; } if (action === 'close-hermes-modal') { e.preventDefault(); const modal = document.getElementById('hermes-modal'); if (modal) modal.remove(); return; } if (action === 'copy-hermes-cmd') { e.preventDefault(); const cmd = target.dataset.cmd || ''; try { await navigator.clipboard.writeText(cmd); target.textContent = '已复制'; setTimeout(() => { target.textContent = '复制'; }, 2000); } catch { alert('复制失败,请手动选择文本复制'); } return; } if (action === 'logout') { e.preventDefault(); logout(); return; } if (action === 'download-avatar') { e.preventDefault(); if (currentRole) { download(currentRole.name + '_avatar.png', ''); window.open(currentRole.avatar, '_blank'); } return; } // Creator form navigation if (action === 'next') { e.preventDefault(); if (validateStep(currentStep) && currentStep < steps.length - 1) { updateStep(currentStep + 1); } return; } if (action === 'prev') { e.preventDefault(); if (currentStep > 0) { updateStep(currentStep - 1); } return; } if (action === 'publish') { e.preventDefault(); publish(); return; } if (action === 'reset') { e.preventDefault(); resetCreator(); return; } // Tab switching (auth tabs and preview tabs) if (target.dataset.tab) { e.preventDefault(); if (target.closest('.auth-tabs')) { switchAuthTab(target.dataset.tab); } else { activePreview = target.dataset.tab; updateTabs(); renderPreview(); } return; } // Download buttons if (target.dataset.download) { e.preventDefault(); if (target.dataset.download === 'soul') { download('Soul.md', generatedSoul); } else { download('config.yaml', generatedConfig); } return; } }); // --- Auth form submissions --- document.querySelectorAll('.auth-form').forEach((authForm) => { authForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!validatePasswordMatch(authForm)) return; const formData = new FormData(authForm); const data = Object.fromEntries(formData.entries()); const isRegister = authForm.dataset.form === 'register'; try { const endpoint = isRegister ? '/auth/register' : '/auth/login'; const result = await api(endpoint, { method: 'POST', body: JSON.stringify({ account: data.account, password: data.password }), }); setToken(result.token); applyUserData(result.user); // 注册成功后自动成为创作者 if (isRegister) { await api('/auth/settings', { method: 'PUT', body: JSON.stringify({ isCreator: true, creatorName: data.account }), }); state.isCreator = true; state.creatorName = data.account; saveState(); updateLandingCard(); } // 登录后跳转 if (state.isCreator) { showView('creator-center'); renderCreatorCenter(); } else { renderRoleLibrary(); showView('role-library'); } } catch (err) { alert(err.message); } }); }); // --- Settings form (U7) --- document.getElementById('settings-form').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); try { const { user } = await api('/auth/settings', { method: 'PUT', body: JSON.stringify({ creatorName: data.creatorName, libraryName: data.libraryName, }), }); state.creatorName = user.creatorName || ''; state.libraryName = user.libraryName || '我的 [XXX]'; saveState(); updateLandingCard(); alert('设置已保存'); } catch (err) { alert('保存失败:' + err.message); } }); // --- Withdraw form (U7) --- document.getElementById('withdraw-form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); const amount = parseFloat(data.amount); if (amount > state.income.balance && amount > mockIncome.balance) { alert('提现金额超过可提现余额'); return; } alert(`提现申请已提交:${data.method === 'wechat' ? '微信' : '支付宝'} ¥${amount.toFixed(2)}\n平台负责人将手动审核转账。`); e.target.reset(); }); // --- Creator form input --- form.addEventListener('input', () => { updateSystemPromptPreview(); }); // --- Initialize --- updateStep(0); updateSystemPromptPreview(); updateLandingCard(); updateTabBar('landing'); initFaqA11y(); initViewA11y(); // 页面加载时验证 token,恢复登录态 (async () => { const token = getToken(); if (!token) return; try { const { user } = await api('/auth/me'); applyUserData(user); } catch { // token 过期,清除 setToken(''); } })(); })();