(() => { '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(); // --- 管理员认证状态管理 --- const ADMIN_TOKEN_KEY = 'eternal_ai_admin_token'; const ADMIN_STATE_KEY = 'eternal_ai_admin_state'; function getAdminToken() { return localStorage.getItem(ADMIN_TOKEN_KEY) || ''; } function setAdminToken(token) { if (token) localStorage.setItem(ADMIN_TOKEN_KEY, token); else localStorage.removeItem(ADMIN_TOKEN_KEY); } const defaultAdminState = { isLoggedIn: false, account: null }; function loadAdminState() { try { return { ...defaultAdminState, ...JSON.parse(localStorage.getItem(ADMIN_STATE_KEY)) }; } catch { return { ...defaultAdminState }; } } function saveAdminState() { localStorage.setItem(ADMIN_STATE_KEY, JSON.stringify(adminState)); } const adminState = loadAdminState(); // 管理员 API 封装(使用管理员 token) async function adminApi(path, options = {}) { const token = getAdminToken(); 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; } } // --- 管理员审核列表 --- let adminCurrentFilter = 'pending_review'; async function renderAdminReviews() { const listEl = document.getElementById('admin-review-list'); if (!listEl) return; listEl.innerHTML = '

加载中…

'; try { const query = adminCurrentFilter ? `?status=${adminCurrentFilter}` : ''; const { roles } = await adminApi(`/admin/reviews${query}`); 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 || ''); const status = escapeHtml(role.reviewStatus); const statusText = getReviewStatusText(role.reviewStatus); const statusClass = getReviewStatusClass(role.reviewStatus); const created = new Date(role.createdAt).toLocaleString('zh-CN'); return `
${name}

${name}

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

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

`; } } function getReviewStatusText(status) { const map = { pending_review: '待审核', approved: '已通过', rejected: '已驳回', syncing: '同步中', synced: '已同步', failed: '同步失败', }; return map[status] || status; } function getReviewStatusClass(status) { const map = { pending_review: 'pending', approved: 'approved', rejected: 'rejected', syncing: 'syncing', synced: 'synced', failed: 'failed', }; return map[status] || 'pending'; } // --- 管理员审核详情 --- let adminCurrentRoleId = null; async function renderAdminReviewDetail(roleId) { adminCurrentRoleId = roleId; const contentEl = document.getElementById('admin-detail-content'); if (!contentEl) return; contentEl.innerHTML = '

加载中…

'; try { const { role } = await adminApi(`/admin/reviews/${roleId}`); const status = role.reviewStatus; let actionsHtml = ''; if (status === 'pending_review') { actionsHtml = `
`; } else if (status === 'approved') { actionsHtml = `

发起 Hermes 同步

`; } else if (status === 'synced') { actionsHtml = `

同步成功

${role.qrCodeUrl ? `二维码` : ''}

Profile ID: ${escapeHtml(role.id)}

同步时间: ${role.syncedAt ? new Date(role.syncedAt).toLocaleString('zh-CN') : '未知'}

`; } else if (status === 'rejected') { actionsHtml = `
驳回原因:${escapeHtml(role.reviewNote || '未提供')}
`; } else if (status === 'failed') { actionsHtml = `
同步失败

角色状态已标记为失败,可重新发起同步。

`; } contentEl.innerHTML = `
${escapeHtml(role.displayName)}

${escapeHtml(role.displayName)}

${getReviewStatusText(status)}

描述:${escapeHtml(role.desc || role.personality || '无')}

性格:${escapeHtml(role.personality || '无')}

背景:${escapeHtml(role.background || '无')}

问候语:${escapeHtml(role.greeting || '无')}

价格:¥${escapeHtml(String(role.price || 0))}

${actionsHtml} `; // 绑定同步表单提交 const syncForm = document.getElementById('admin-sync-form'); if (syncForm) { syncForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(syncForm); const data = Object.fromEntries(formData.entries()); data.enableSchedule = !!formData.get('enableSchedule'); if (!data.webhookUrl) delete data.webhookUrl; const submitBtn = syncForm.querySelector('button[type="submit"]'); submitBtn.disabled = true; submitBtn.textContent = '同步中…'; try { const result = await adminApi(`/admin/sync/${roleId}`, { method: 'POST', body: JSON.stringify(data), }); alert('同步成功!Profile ID: ' + result.profileId); renderAdminReviewDetail(roleId); } catch (err) { alert('同步失败:' + err.message); submitBtn.disabled = false; submitBtn.textContent = '发起同步'; } }); } } catch (err) { contentEl.innerHTML = `

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

`; } } // --- 管理员同步状态列表 --- async function renderAdminSyncStatus() { const listEl = document.getElementById('admin-sync-list'); if (!listEl) return; listEl.innerHTML = '

加载中…

'; try { const { roles } = await adminApi('/admin/sync-status'); if (roles.length === 0) { listEl.innerHTML = '

暂无同步记录

'; return; } listEl.innerHTML = roles.map((role) => { const id = escapeHtml(role.id); const name = escapeHtml(role.displayName); const status = escapeHtml(role.reviewStatus); const statusText = getReviewStatusText(role.reviewStatus); const statusClass = getReviewStatusClass(role.reviewStatus); const syncedAt = role.syncedAt ? new Date(role.syncedAt).toLocaleString('zh-CN') : '未同步'; return `

${name}

${statusText} 同步时间:${syncedAt}
`; }).join(''); } catch (err) { listEl.innerHTML = `

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

`; } } // --- 管理员系统配置 --- async function renderAdminConfig() { const listEl = document.getElementById('admin-config-list'); if (!listEl) return; listEl.innerHTML = '

加载中…

'; try { const { configs } = await adminApi('/admin/config'); listEl.innerHTML = configs.map((c) => { const key = escapeHtml(c.key); const value = escapeHtml(c.value); const updated = c.updatedAt ? new Date(c.updatedAt).toLocaleString('zh-CN') : ''; const isProtected = key === 'SYNC_SECRET'; return `
${isProtected ? '' : ``}
${updated ? `更新于 ${updated}` : ''}
`; }).join(''); } catch (err) { listEl.innerHTML = `

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

`; } } // --- 管理员 Tab 切换 --- function switchAdminTab(tab) { document.querySelectorAll('.center-tab').forEach((t) => { if (t.closest('#admin-reviews')) { const isActive = t.dataset.centerTab === tab; t.classList.toggle('active', isActive); } }); document.getElementById('admin-reviews-panel').hidden = tab !== 'admin-reviews'; document.getElementById('admin-sync-panel').hidden = tab !== 'admin-sync'; document.getElementById('admin-config-panel').hidden = tab !== 'admin-config'; if (tab === 'admin-reviews') renderAdminReviews(); else if (tab === 'admin-sync') renderAdminSyncStatus(); else if (tab === 'admin-config') renderAdminConfig(); } // --- 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'), 'admin-login': document.getElementById('admin-login'), 'admin-reviews': document.getElementById('admin-reviews'), 'admin-review-detail': document.getElementById('admin-review-detail'), }; 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: '角色编辑', 'admin-login': '管理员登录', 'admin-reviews': '管理员后台', 'admin-review-detail': '角色审核详情', }; 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(); } } async function payRole() { if (!currentRole) return; const btn = document.querySelector('[data-action="pay"]'); if (btn) { btn.disabled = true; btn.textContent = '处理中…'; } try { const result = await api('/orders', { method: 'POST', body: JSON.stringify({ roleId: currentRole.id, amount: currentRole.price }), }); // 付款成功,展示二维码 document.getElementById('detail-actions-pre').hidden = true; const paidEl = document.getElementById('detail-paid'); paidEl.hidden = false; const qrEl = document.getElementById('detail-qr'); if (result.role && result.role.qrCodeUrl) { qrEl.innerHTML = `角色二维码`; } else { qrEl.innerHTML = '
扫码连接
AI 角色
'; } document.getElementById('detail-avatar').style.backgroundImage = `url(${currentRole.avatar})`; } catch (err) { alert('付款失败:' + err.message); if (btn) { btn.disabled = false; btn.textContent = '立即订阅'; } } } // --- 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 reviewStatus = role.reviewStatus || 'pending_review'; const statusText = getReviewStatusText(reviewStatus); const statusClass = getReviewStatusClass(reviewStatus); // 审核状态标签 let statusBadge = `${statusText}`; // 已同步角色额外显示运行状态 if (reviewStatus === 'synced') { const runStatus = role.status === 'running' ? '运行中' : '已停止'; statusBadge += ` ${runStatus}`; } // 驳回原因 let rejectNote = ''; if (reviewStatus === 'rejected' && role.reviewNote) { rejectNote = `

驳回原因:${escapeHtml(role.reviewNote)}

`; } // 二维码展示(已同步角色) let qrCodeHtml = ''; if (reviewStatus === 'synced' && role.qrCodeUrl) { qrCodeHtml = `
二维码
`; } // 操作按钮 let actionButtons = ''; if (reviewStatus === 'rejected' || reviewStatus === 'pending_review') { actionButtons = ``; } else if (reviewStatus === 'synced') { actionButtons = ` `; } return `
${name}

${name}

${statusBadge}
${rejectNote} ${qrCodeHtml}
${actionButtons}
`; }).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 { apiKeys } = await api('/apikeys'); if (apiKeys.length === 0) { listEl.innerHTML = '

还没有 API Key

'; return; } listEl.innerHTML = apiKeys .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 showQrCodeModal(qrUrl) { const modal = document.createElement('div'); modal.id = 'qr-modal'; modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000;'; modal.innerHTML = `

角色二维码

二维码
`; 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 之前处理) const adminReviewCard = e.target.closest('.admin-review-card'); if (adminReviewCard && !e.target.closest('[data-action]')) { e.preventDefault(); const roleId = adminReviewCard.dataset.roleId; renderAdminReviewDetail(roleId); showView('admin-review-detail'); 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; } // 管理员筛选 Tab const adminFilter = e.target.closest('.admin-filter'); if (adminFilter) { e.preventDefault(); document.querySelectorAll('.admin-filter').forEach((f) => f.classList.remove('active')); adminFilter.classList.add('active'); adminCurrentFilter = adminFilter.dataset.filter; renderAdminReviews(); return; } const target = e.target.closest('[data-action], [data-tab], [data-download], [data-center-tab]'); if (!target) return; const action = target.dataset.action; // 管理员 Tab 切换(需在通用 center tab 之前处理) const adminCenterTab = target.dataset.centerTab; if (adminCenterTab && adminCenterTab.startsWith('admin-')) { e.preventDefault(); switchAdminTab(adminCenterTab); return; } // 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 === 'admin-logout') { e.preventDefault(); setAdminToken(''); adminState.isLoggedIn = false; saveAdminState(); window.location.hash = ''; showView('landing'); return; } if (action === 'back-to-admin') { e.preventDefault(); showView('admin-reviews'); renderAdminReviews(); return; } // 管理员审核操作 if (action === 'admin-approve') { e.preventDefault(); const roleId = target.dataset.roleId; try { await adminApi(`/admin/reviews/${roleId}/approve`, { method: 'POST' }); alert('审核通过'); renderAdminReviewDetail(roleId); } catch (err) { alert('操作失败:' + err.message); } return; } if (action === 'admin-reject') { e.preventDefault(); const roleId = target.dataset.roleId; const reason = prompt('请输入驳回原因:'); if (!reason) return; try { await adminApi(`/admin/reviews/${roleId}/reject`, { method: 'POST', body: JSON.stringify({ reviewNote: reason }), }); alert('已驳回'); renderAdminReviewDetail(roleId); } catch (err) { alert('操作失败:' + err.message); } return; } // 管理员保存配置 if (action === 'admin-save-config') { e.preventDefault(); const key = target.dataset.configKey; const input = document.querySelector(`input[data-config-key="${key}"]`); if (!input) return; try { await adminApi(`/admin/config/${key}`, { method: 'PUT', body: JSON.stringify({ value: input.value }), }); alert('配置已保存'); renderAdminConfig(); } catch (err) { alert('保存失败:' + err.message); } return; } // --- 二维码操作 --- if (action === 'show-qrcode') { e.preventDefault(); const qrUrl = target.dataset.qrUrl; showQrCodeModal(qrUrl); return; } if (action === 'copy-qrcode') { e.preventDefault(); const qrUrl = target.dataset.qrUrl; try { await navigator.clipboard.writeText(qrUrl); alert('二维码链接已复制'); } catch { alert('复制失败:' + qrUrl); } return; } if (action === 'close-qr-modal') { e.preventDefault(); const modal = document.getElementById('qr-modal'); if (modal) modal.remove(); 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); } }); }); // --- 管理员登录表单 --- const adminLoginForm = document.getElementById('admin-login-form'); if (adminLoginForm) { adminLoginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); try { const result = await adminApi('/admin-auth/login', { method: 'POST', body: JSON.stringify({ account: data.account, password: data.password }), }); setAdminToken(result.token); adminState.isLoggedIn = true; adminState.account = result.admin.account; saveAdminState(); showView('admin-reviews'); renderAdminReviews(); } 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(''); } })(); // --- 管理员后台路由 --- async function handleAdminRoute() { if (window.location.hash !== '#admin') return; const token = getAdminToken(); if (!token) { showView('admin-login'); return; } // 验证 token 有效性 try { await adminApi('/admin-auth/me'); showView('admin-reviews'); renderAdminReviews(); } catch { setAdminToken(''); adminState.isLoggedIn = false; saveAdminState(); showView('admin-login'); } } window.addEventListener('hashchange', handleAdminRoute); // 页面加载时检查 hash handleAdminRoute(); })();