${role.name}
-${role.desc}
+${role.displayName}
+${role.desc || ''}
¥${role.price}加载失败:${err.message}
`; + } } // --- U3: Role Detail --- - function renderRoleDetail(roleId) { - const role = mockRoles.find((r) => r.id === roleId); - if (!role) return; - currentRole = role; + async function renderRoleDetail(roleId) { + try { + const { role } = await api(`/roles/${roleId}`); + currentRole = role; - document.getElementById('detail-name').textContent = role.name; - document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar})`; - document.getElementById('detail-role-name').textContent = role.name; - document.getElementById('detail-role-desc').textContent = role.desc; - document.getElementById('detail-price').innerHTML = `¥${role.price}/ 月`; + 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 = `¥${role.price}/ 月`; - document.getElementById('detail-actions-pre').hidden = false; - document.getElementById('detail-paid').hidden = true; + document.getElementById('detail-actions-pre').hidden = false; + document.getElementById('detail-paid').hidden = true; + } catch (err) { + alert('加载角色详情失败:' + err.message); + goBack(); + } } function payRole() { @@ -348,22 +399,33 @@ renderSettings(); } - function renderCreatorRoles() { + async function renderCreatorRoles() { const listEl = document.getElementById('creator-role-list'); - const roles = state.roles.length > 0 ? state.roles : mockRoles; - listEl.innerHTML = roles - .map( - (role) => ` -加载中…
'; + try { + const { roles } = await api('/roles/my/roles'); + state.roles = roles; + saveState(); + if (roles.length === 0) { + listEl.innerHTML = '还没有创建角色,点击「新建角色」开始
'; + return; + } + listEl.innerHTML = roles + .map( + (role) => ` +${role.name}
+${role.displayName}
${role.status === 'running' ? '运行中' : '已停止'}加载失败:${err.message}
`; + } } function renderIncome() { @@ -588,7 +650,7 @@ ${data.secrets || '无'} } } - function publish() { + async function publish() { if (!validateStep(currentStep)) return; const data = getFormData(); @@ -599,9 +661,37 @@ ${data.secrets || '无'} generatedSoul = generateSoulMd(data); generatedConfig = generateConfigYaml(data); - form.hidden = true; - resultPanel.hidden = false; - renderPreview(); + // 持久化到数据库 + 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() { @@ -627,8 +717,53 @@ ${data.secrets || '无'} 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() { @@ -652,7 +787,7 @@ ${data.secrets || '无'} } }); - document.addEventListener('click', (e) => { + document.addEventListener('click', async (e) => { // FAQ toggle (U4) const faqBtn = e.target.closest('.faq-q'); if (faqBtn) { @@ -772,7 +907,8 @@ ${data.secrets || '无'} if (action === 'edit-role') { e.preventDefault(); - resetCreator(); + const roleId = target.dataset.roleId; + await loadRoleForEdit(roleId); showView('creator'); return; } @@ -848,34 +984,70 @@ ${data.secrets || '无'} // --- Auth form submissions --- document.querySelectorAll('.auth-form').forEach((authForm) => { - authForm.addEventListener('submit', (e) => { + authForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!validatePasswordMatch(authForm)) return; const formData = new FormData(authForm); const data = Object.fromEntries(formData.entries()); - login(data.account); + const isRegister = authForm.dataset.form === 'register'; - // After login, go to role library or creator center - if (state.isCreator) { - showView('creator-center'); - renderCreatorCenter(); - } else { - renderRoleLibrary(); - showView('role-library'); + 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', (e) => { + document.getElementById('settings-form').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); - state.creatorName = data.creatorName || ''; - state.libraryName = data.libraryName || '我的 [XXX]'; - saveState(); - updateLandingCard(); - alert('设置已保存'); + 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) --- @@ -904,4 +1076,17 @@ ${data.secrets || '无'} updateTabBar('landing'); initFaqA11y(); initViewA11y(); + + // 页面加载时验证 token,恢复登录态 + (async () => { + const token = getToken(); + if (!token) return; + try { + const { user } = await api('/auth/me'); + applyUserData(user); + } catch { + // token 过期,清除 + setToken(''); + } + })(); })(); diff --git a/package-lock.json b/package-lock.json index 64f1ff4..c7d3896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,76 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "express": "^5.2.1" + "@prisma/client": "^5.22.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "prisma": "^5.22.0" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" } }, "node_modules/accepts": { @@ -25,6 +94,15 @@ "node": ">= 0.6" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz", @@ -62,6 +140,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", @@ -140,6 +224,23 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -166,6 +267,18 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -180,6 +293,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -322,6 +444,20 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", @@ -461,6 +597,91 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -531,6 +752,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", @@ -583,6 +813,25 @@ "url": "https://opencollective.com/express" } }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -651,12 +900,44 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", diff --git a/package.json b/package.json index 4bc6b0b..db3603a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "main": "server.js", "scripts": { "start": "node server.js", + "dev": "node --watch server.js", + "db:push": "prisma db push", + "db:studio": "prisma studio", "test": "jest" }, "keywords": ["ai", "eternal", "agent", "hermes"], @@ -15,6 +18,12 @@ "url": "http://gitea.fischerai.cn/chigulong/eternalai.git" }, "dependencies": { - "express": "^5.2.1" + "@prisma/client": "^5.22.0", + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^17.2.0", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.2", + "prisma": "^5.22.0" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..e0ffd10 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,70 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + account String @unique + password String + isCreator Boolean @default(false) + creatorName String? + libraryName String? + boundCreator String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + roles Role[] + orders Order[] +} + +model Role { + id String @id @default(uuid()) + creatorId String + displayName String + gender String @default("unknown") + age String? + relationship String? + personality String + background String + speechStyle String + likes String? + dislikes String? + memories String? + secrets String? + greeting String + systemPrompt String? + model String @default("gpt-4o") + temperature Float @default(0.8) + maxTokens Int @default(2048) + enableMemory Boolean @default(true) + enableTools Boolean @default(false) + agentId String? + soulMd String? + configYaml String? + avatar String? + desc String? + price Float @default(0) + status String @default("running") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + creator User @relation(fields: [creatorId], references: [id]) + orders Order[] +} + +model Order { + id String @id @default(uuid()) + userId String + roleId String + amount Float + status String @default("paid") + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) +} diff --git a/server.js b/server.js index a12f63e..644d262 100644 --- a/server.js +++ b/server.js @@ -1,17 +1,27 @@ +require('dotenv').config(); const express = require('express'); const path = require('path'); -const app = express(); -const PORT = 3001; +const cors = require('cors'); -// Serve static files from the root directory +const app = express(); +const PORT = process.env.PORT || 3001; + +// 中间件 +app.use(cors()); +app.use(express.json()); + +// API 路由 +app.use('/api/auth', require('./src/routes/auth')); +app.use('/api/roles', require('./src/routes/roles')); + +// 静态文件 app.use(express.static('.')); -// Route for the main page +// 主页 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); -// Start the server app.listen(PORT, '0.0.0.0', () => { - console.log(`EternalAI server is running on http://0.0.0.0:${PORT}`); + console.log(`EternalAI server running on http://0.0.0.0:${PORT}`); }); diff --git a/src/lib/auth.js b/src/lib/auth.js new file mode 100644 index 0000000..1a110de --- /dev/null +++ b/src/lib/auth.js @@ -0,0 +1,53 @@ +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); + +const JWT_SECRET = process.env.JWT_SECRET || 'eternalai_jwt_secret_2026_change_in_prod'; +const JWT_EXPIRES_IN = '7d'; + +// 哈希密码 +function hashPassword(password) { + return bcrypt.hashSync(password, 10); +} + +// 验证密码 +function verifyPassword(password, hash) { + return bcrypt.compareSync(password, hash); +} + +// 生成 JWT +function signToken(userId) { + return jwt.sign({ userId }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); +} + +// 验证 JWT 并提取 userId +function verifyToken(token) { + try { + const decoded = jwt.verify(token, JWT_SECRET); + return decoded.userId; + } catch { + return null; + } +} + +// Express 中间件:验证 JWT +function authMiddleware(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: '未登录' }); + } + const token = authHeader.slice(7); + const userId = verifyToken(token); + if (!userId) { + return res.status(401).json({ error: '登录已过期,请重新登录' }); + } + req.userId = userId; + next(); +} + +module.exports = { + hashPassword, + verifyPassword, + signToken, + verifyToken, + authMiddleware, +}; diff --git a/src/lib/prisma.js b/src/lib/prisma.js new file mode 100644 index 0000000..0dbc802 --- /dev/null +++ b/src/lib/prisma.js @@ -0,0 +1,5 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +module.exports = prisma; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..bb76bfd --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,128 @@ +const express = require('express'); +const prisma = require('../lib/prisma'); +const { hashPassword, verifyPassword, signToken, authMiddleware } = require('../lib/auth'); + +const router = express.Router(); + +// 注册 +router.post('/register', async (req, res) => { + try { + const { account, password } = req.body; + if (!account || !password) { + return res.status(400).json({ error: '账号和密码不能为空' }); + } + if (password.length < 6) { + return res.status(400).json({ error: '密码至少 6 位' }); + } + + const existing = await prisma.user.findUnique({ where: { account } }); + if (existing) { + return res.status(409).json({ error: '该账号已注册' }); + } + + const user = await prisma.user.create({ + data: { + account, + password: hashPassword(password), + libraryName: '我的角色库', + }, + }); + + const token = signToken(user.id); + res.json({ + token, + user: { + id: user.id, + account: user.account, + isCreator: user.isCreator, + creatorName: user.creatorName, + libraryName: user.libraryName, + }, + }); + } catch (err) { + console.error('注册失败:', err); + res.status(500).json({ error: '注册失败,请稍后重试' }); + } +}); + +// 登录 +router.post('/login', async (req, res) => { + try { + const { account, password } = req.body; + if (!account || !password) { + return res.status(400).json({ error: '账号和密码不能为空' }); + } + + const user = await prisma.user.findUnique({ where: { account } }); + if (!user || !verifyPassword(password, user.password)) { + return res.status(401).json({ error: '账号或密码错误' }); + } + + const token = signToken(user.id); + res.json({ + token, + user: { + id: user.id, + account: user.account, + isCreator: user.isCreator, + creatorName: user.creatorName, + libraryName: user.libraryName, + }, + }); + } catch (err) { + console.error('登录失败:', err); + res.status(500).json({ error: '登录失败,请稍后重试' }); + } +}); + +// 获取当前用户信息 +router.get('/me', authMiddleware, async (req, res) => { + try { + const user = await prisma.user.findUnique({ + where: { id: req.userId }, + select: { + id: true, + account: true, + isCreator: true, + creatorName: true, + libraryName: true, + boundCreator: true, + }, + }); + if (!user) { + return res.status(404).json({ error: '用户不存在' }); + } + res.json({ user }); + } catch (err) { + console.error('获取用户信息失败:', err); + res.status(500).json({ error: '服务器错误' }); + } +}); + +// 更新用户设置 +router.put('/settings', authMiddleware, async (req, res) => { + try { + const { creatorName, libraryName, isCreator } = req.body; + const user = await prisma.user.update({ + where: { id: req.userId }, + data: { + ...(creatorName !== undefined && { creatorName }), + ...(libraryName !== undefined && { libraryName }), + ...(isCreator !== undefined && { isCreator }), + }, + select: { + id: true, + account: true, + isCreator: true, + creatorName: true, + libraryName: true, + }, + }); + res.json({ user }); + } catch (err) { + console.error('更新设置失败:', err); + res.status(500).json({ error: '更新失败' }); + } +}); + +module.exports = router; diff --git a/src/routes/roles.js b/src/routes/roles.js new file mode 100644 index 0000000..96b5c40 --- /dev/null +++ b/src/routes/roles.js @@ -0,0 +1,187 @@ +const express = require('express'); +const prisma = require('../lib/prisma'); +const { authMiddleware } = require('../lib/auth'); + +const router = express.Router(); + +// 获取角色库(所有已上架角色) +router.get('/', async (req, res) => { + try { + const roles = await prisma.role.findMany({ + where: { status: 'running' }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + displayName: true, + avatar: true, + desc: true, + price: true, + status: true, + }, + }); + res.json({ roles }); + } catch (err) { + console.error('获取角色列表失败:', err); + res.status(500).json({ error: '服务器错误' }); + } +}); + +// 获取角色详情 +router.get('/:id', async (req, res) => { + try { + const role = await prisma.role.findUnique({ + where: { id: req.params.id }, + select: { + id: true, + displayName: true, + avatar: true, + desc: true, + price: true, + status: true, + gender: true, + age: true, + relationship: true, + personality: true, + background: true, + speechStyle: true, + greeting: true, + creatorId: true, + }, + }); + if (!role) { + return res.status(404).json({ error: '角色不存在' }); + } + res.json({ role }); + } catch (err) { + console.error('获取角色详情失败:', err); + res.status(500).json({ error: '服务器错误' }); + } +}); + +// 获取当前用户创建的角色 +router.get('/my/roles', authMiddleware, async (req, res) => { + try { + const roles = await prisma.role.findMany({ + where: { creatorId: req.userId }, + orderBy: { createdAt: 'desc' }, + }); + res.json({ roles }); + } catch (err) { + console.error('获取我的角色失败:', err); + res.status(500).json({ error: '服务器错误' }); + } +}); + +// 发布新角色 +router.post('/', authMiddleware, async (req, res) => { + try { + const data = req.body; + if (!data.displayName || !data.greeting || !data.personality || !data.background || !data.speechStyle) { + return res.status(400).json({ error: '必填字段缺失' }); + } + + const role = await prisma.role.create({ + data: { + creatorId: req.userId, + displayName: data.displayName, + gender: data.gender || 'unknown', + age: data.age || null, + relationship: data.relationship || null, + personality: data.personality, + background: data.background, + speechStyle: data.speechStyle, + likes: data.likes || null, + dislikes: data.dislikes || null, + memories: data.memories || null, + secrets: data.secrets || null, + greeting: data.greeting, + systemPrompt: data.systemPrompt || null, + model: data.model || 'gpt-4o', + temperature: parseFloat(data.temperature) || 0.8, + maxTokens: parseInt(data.maxTokens) || 2048, + enableMemory: data.enableMemory ?? true, + enableTools: data.enableTools ?? false, + agentId: data.agentId || null, + soulMd: data.soulMd || null, + configYaml: data.configYaml || null, + avatar: data.avatar || null, + desc: data.desc || data.personality.slice(0, 50), + price: parseFloat(data.price) || 0, + status: 'running', + }, + }); + + res.json({ role }); + } catch (err) { + console.error('发布角色失败:', err); + res.status(500).json({ error: '发布失败' }); + } +}); + +// 编辑角色 +router.put('/:id', authMiddleware, async (req, res) => { + try { + const existing = await prisma.role.findUnique({ where: { id: req.params.id } }); + if (!existing) { + return res.status(404).json({ error: '角色不存在' }); + } + if (existing.creatorId !== req.userId) { + return res.status(403).json({ error: '无权编辑他人角色' }); + } + + const data = req.body; + const role = await prisma.role.update({ + where: { id: req.params.id }, + data: { + displayName: data.displayName ?? existing.displayName, + gender: data.gender ?? existing.gender, + age: data.age ?? existing.age, + relationship: data.relationship ?? existing.relationship, + personality: data.personality ?? existing.personality, + background: data.background ?? existing.background, + speechStyle: data.speechStyle ?? existing.speechStyle, + likes: data.likes ?? existing.likes, + dislikes: data.dislikes ?? existing.dislikes, + memories: data.memories ?? existing.memories, + secrets: data.secrets ?? existing.secrets, + greeting: data.greeting ?? existing.greeting, + systemPrompt: data.systemPrompt ?? existing.systemPrompt, + model: data.model ?? existing.model, + temperature: parseFloat(data.temperature) || existing.temperature, + maxTokens: parseInt(data.maxTokens) || existing.maxTokens, + enableMemory: data.enableMemory ?? existing.enableMemory, + enableTools: data.enableTools ?? existing.enableTools, + soulMd: data.soulMd ?? existing.soulMd, + configYaml: data.configYaml ?? existing.configYaml, + avatar: data.avatar ?? existing.avatar, + desc: data.desc ?? existing.desc, + price: parseFloat(data.price) || existing.price, + status: data.status ?? existing.status, + }, + }); + + res.json({ role }); + } catch (err) { + console.error('编辑角色失败:', err); + res.status(500).json({ error: '编辑失败' }); + } +}); + +// 获取角色详情(含 Soul.md 和 config.yaml,仅创建者可访问) +router.get('/:id/full', authMiddleware, async (req, res) => { + try { + const role = await prisma.role.findUnique({ where: { id: req.params.id } }); + if (!role) { + return res.status(404).json({ error: '角色不存在' }); + } + if (role.creatorId !== req.userId) { + return res.status(403).json({ error: '无权查看' }); + } + res.json({ role }); + } catch (err) { + console.error('获取角色完整信息失败:', err); + res.status(500).json({ error: '服务器错误' }); + } +}); + +module.exports = router;