(() => {
'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}
${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)}
${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}
${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}
${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 `
创建于 ${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 所在的机器。
前提条件
- 已安装 Hermes Agent(
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash)
- 已在 EternalAI 设置页生成 API Key
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();
})();