1295 lines
42 KiB
JavaScript
1295 lines
42 KiB
JavaScript
(() => {
|
||
'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 = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||
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 `
|
||
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name},${desc},每月${price}元">
|
||
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
|
||
<div class="role-card__info">
|
||
<h3 class="role-card__name">${name}</h3>
|
||
<p class="role-card__desc">${desc}</p>
|
||
<span class="role-card__price">¥${price}</span>
|
||
</div>
|
||
</article>`;
|
||
})
|
||
.join('');
|
||
} catch (err) {
|
||
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
|
||
}
|
||
}
|
||
|
||
// --- 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 = `<span class="detail-price__value">¥${escapeHtml(role.price)}</span><span class="detail-price__unit">/ 月</span>`;
|
||
|
||
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 = '<div class="qr-placeholder">扫码连接<br/>AI 角色</div>';
|
||
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 = '<p style="text-align:center;color:var(--text-muted)">加载中…</p>';
|
||
try {
|
||
const { roles } = await api('/roles/my/roles');
|
||
state.roles = roles;
|
||
saveState();
|
||
if (roles.length === 0) {
|
||
listEl.innerHTML = '<p style="text-align:center;color:var(--text-muted)">还没有创建角色,点击「新建角色」开始</p>';
|
||
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 `
|
||
<article class="role-card" data-role-id="${id}" role="button" tabindex="0" aria-label="${name},${statusText}">
|
||
<img class="role-card__avatar" src="${avatar}" alt="${name}" />
|
||
<div class="role-card__info">
|
||
<h3 class="role-card__name">${name}</h3>
|
||
<span class="role-card__status role-card__status--${statusClass}">${statusText}</span>
|
||
</div>
|
||
<div style="display:flex;gap:0.5rem;">
|
||
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${id}">编辑</button>
|
||
<button class="btn btn--small btn--outline" type="button" data-action="hermes-deploy" data-role-id="${id}" data-role-name="${name}">Hermes 部署</button>
|
||
</div>
|
||
</article>`;
|
||
})
|
||
.join('');
|
||
} catch (err) {
|
||
listEl.innerHTML = `<p style="text-align:center;color:var(--text-muted)">加载失败:${escapeHtml(err.message)}</p>`;
|
||
}
|
||
}
|
||
|
||
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 = '<p class="income-empty">暂无流水记录</p>';
|
||
return;
|
||
}
|
||
listEl.innerHTML = income.records
|
||
.map((r) => {
|
||
const role = escapeHtml(r.role);
|
||
const time = escapeHtml(r.time);
|
||
const amount = r.amount.toFixed(2);
|
||
return `
|
||
<div class="income-record">
|
||
<div class="income-record__info">
|
||
<span class="income-record__role">${role}</span>
|
||
<span class="income-record__time">${time}</span>
|
||
</div>
|
||
<span class="income-record__amount">+¥${amount}</span>
|
||
</div>`;
|
||
})
|
||
.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 = '<p style="color:var(--text-muted);font-size:0.875rem;">加载中…</p>';
|
||
try {
|
||
const { keys } = await api('/apikeys');
|
||
if (keys.length === 0) {
|
||
listEl.innerHTML = '<p style="color:var(--text-muted);font-size:0.875rem;">还没有 API Key</p>';
|
||
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 `
|
||
<div class="apikey-item" style="padding:0.75rem 0;border-bottom:1px solid var(--border);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||
<div>
|
||
<strong>${name}</strong>
|
||
<code style="margin-left:0.5rem;color:var(--text-muted);">${prefix}…</code>
|
||
</div>
|
||
<button class="btn btn--small btn--ghost" type="button" data-action="delete-apikey" data-apikey-id="${id}">删除</button>
|
||
</div>
|
||
<div style="font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem;">
|
||
创建于 ${created} · 最后使用:${lastUsed}
|
||
</div>
|
||
</div>`;
|
||
})
|
||
.join('');
|
||
} catch (err) {
|
||
listEl.innerHTML = `<p style="color:var(--text-muted);font-size:0.875rem;">加载失败:${escapeHtml(err.message)}</p>`;
|
||
}
|
||
}
|
||
|
||
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 = `
|
||
<div style="background:var(--bg-card,#fff);border-radius:12px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;padding:1.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.2);">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||
<h3 style="margin:0;">Hermes Agent 部署指南</h3>
|
||
<button class="btn btn--small btn--ghost" type="button" data-action="close-hermes-modal">✕</button>
|
||
</div>
|
||
|
||
<p style="color:var(--text-muted);font-size:0.875rem;margin-bottom:1rem;">
|
||
角色「${escapeHtml(roleName)}」的配置文件可通过以下命令拉取到 Hermes Agent 所在的机器。
|
||
</p>
|
||
|
||
<div style="background:var(--bg-muted,#f5f5f5);border-radius:8px;padding:0.75rem;margin-bottom:1rem;">
|
||
<strong style="font-size:0.875rem;">前提条件</strong>
|
||
<ul style="margin:0.5rem 0 0 1.25rem;padding:0;font-size:0.8125rem;color:var(--text-muted);">
|
||
<li>已安装 Hermes Agent(<code>curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash</code>)</li>
|
||
<li>已在 EternalAI 设置页生成 API Key</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">1. 创建 Hermes Profile</h4>
|
||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>hermes profile create ${escapeHtml(profileName)}</code></pre>
|
||
|
||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">2. 拉取 SOUL.md</h4>
|
||
<div style="position:relative;margin-bottom:1rem;">
|
||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin:0;"><code>${escapeHtml(soulCmd)}</code></pre>
|
||
<button class="btn btn--small btn--outline" type="button" data-action="copy-hermes-cmd" data-cmd="${escapeHtml(soulCmd)}" style="position:absolute;top:0.5rem;right:0.5rem;">复制</button>
|
||
</div>
|
||
|
||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">3. 拉取 config.yaml</h4>
|
||
<div style="position:relative;margin-bottom:1rem;">
|
||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin:0;"><code>${escapeHtml(configCmd)}</code></pre>
|
||
<button class="btn btn--small btn--outline" type="button" data-action="copy-hermes-cmd" data-cmd="${escapeHtml(configCmd)}" style="position:absolute;top:0.5rem;right:0.5rem;">复制</button>
|
||
</div>
|
||
|
||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">4. 配置 API 密钥</h4>
|
||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>echo "OPENROUTER_API_KEY=sk-or-your-key" > ~/.hermes/profiles/${escapeHtml(profileName)}/.env</code></pre>
|
||
|
||
<h4 style="font-size:0.9375rem;margin-bottom:0.5rem;">5. 启动对话</h4>
|
||
<pre style="background:var(--bg-muted,#1e1e1e);color:var(--text-light,#eee);border-radius:8px;padding:0.75rem;overflow-x:auto;font-size:0.8125rem;margin-bottom:1rem;"><code>${escapeHtml(profileName)} chat</code></pre>
|
||
|
||
<div style="border-top:1px solid var(--border);padding-top:1rem;margin-top:0.5rem;">
|
||
<p style="font-size:0.8125rem;color:var(--text-muted);margin:0;">
|
||
提示:将命令中的 <code>YOUR_API_KEY</code> 替换为你在设置页生成的 API Key(以 <code>eak_</code> 开头)。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 点击遮罩关闭
|
||
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('');
|
||
}
|
||
})();
|
||
})();
|