diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..1bab7edf6 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,158 @@ +# Development Guidelines + +## Philosophy + +### Core Beliefs + +- **Incremental progress over big bangs** - Small changes that compile and pass tests +- **Learning from existing code** - Study and plan before implementing +- **Pragmatic over dogmatic** - Adapt to project reality +- **Clear intent over clever code** - Be boring and obvious + +### Simplicity Means + +- Single responsibility per function/class +- Avoid premature abstractions +- No clever tricks - choose the boring solution +- If you need to explain it, it's too complex + +## Process + +### 1. Planning & Staging + +Break complex work into 3-5 stages. Document in `IMPLEMENTATION_PLAN.md`: + +```markdown +## Stage N: [Name] + +**Goal**: [Specific deliverable] **Success Criteria**: [Testable outcomes] **Tests**: [Specific test +cases] **Status**: [Not Started|In Progress|Complete] +``` + +- Update status as you progress +- Remove file when all stages are done + +### 2. Implementation Flow + +1. **Understand** - Study existing patterns in codebase +2. **Test** - Write test first (red) +3. **Implement** - Minimal code to pass (green) +4. **Refactor** - Clean up with tests passing +5. **Commit** - With clear message linking to plan + +### 3. When Stuck (After 3 Attempts) + +**CRITICAL**: Maximum 3 attempts per issue, then STOP. + +1. **Document what failed**: + + - What you tried + - Specific error messages + - Why you think it failed + +2. **Research alternatives**: + + - Find 2-3 similar implementations + - Note different approaches used + +3. **Question fundamentals**: + + - Is this the right abstraction level? + - Can this be split into smaller problems? + - Is there a simpler approach entirely? + +4. **Try different angle**: + - Different library/framework feature? + - Different architectural pattern? + - Remove abstraction instead of adding? + +## Technical Standards + +### Architecture Principles + +- **Composition over inheritance** - Use dependency injection +- **Interfaces over singletons** - Enable testing and flexibility +- **Explicit over implicit** - Clear data flow and dependencies +- **Test-driven when possible** - Never disable tests, fix them + +### Code Quality + +- **Every commit must**: + + - Compile successfully + - Pass all existing tests + - Include tests for new functionality + - Follow project formatting/linting + +- **Before committing**: + - Run formatters/linters + - Self-review changes + - Ensure commit message explains "why" + +### Error Handling + +- Fail fast with descriptive messages +- Include context for debugging +- Handle errors at appropriate level +- Never silently swallow exceptions + +## Decision Framework + +When multiple valid approaches exist, choose based on: + +1. **Testability** - Can I easily test this? +2. **Readability** - Will someone understand this in 6 months? +3. **Consistency** - Does this match project patterns? +4. **Simplicity** - Is this the simplest solution that works? +5. **Reversibility** - How hard to change later? + +## Project Integration + +### Learning the Codebase + +- Find 3 similar features/components +- Identify common patterns and conventions +- Use same libraries/utilities when possible +- Follow existing test patterns + +### Tooling + +- Use project's existing build system +- Use project's test framework +- Use project's formatter/linter settings +- Don't introduce new tools without strong justification + +## Quality Gates + +### Definition of Done + +- [ ] Tests written and passing +- [ ] Code follows project conventions +- [ ] No linter/formatter warnings +- [ ] Commit messages are clear +- [ ] Implementation matches plan +- [ ] No TODOs without issue numbers + +### Test Guidelines + +- Test behavior, not implementation +- One assertion per test when possible +- Clear test names describing scenario +- Use existing test utilities/helpers +- Tests should be deterministic + +## Important Reminders + +**NEVER**: + +- Use `--no-verify` to bypass commit hooks +- Disable tests instead of fixing them +- Commit code that doesn't compile +- Make assumptions - verify with existing code + +**ALWAYS**: + +- Commit working code incrementally +- Update plan documentation as you go +- Learn from existing implementations +- Stop after 3 failed attempts and reassess diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1201c4184..90e16fa90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,8 +48,6 @@ jobs: - name: Build React app env: VITE_UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID }} - VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }} - VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }} run: | cd react npx vite build diff --git a/.gitmodules b/.gitmodules index 555e1d933..ef2f20ccc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "server/openmanus"] - path = server/openmanus - url = https://github.com/mannaandpoem/OpenManus +##[submodule "server/openmanus"] +## path = server/openmanus +## url = https://github.com/mannaandpoem/OpenManus diff --git a/PRIVACY_PAGES_README.md b/PRIVACY_PAGES_README.md new file mode 100644 index 000000000..09c76c219 --- /dev/null +++ b/PRIVACY_PAGES_README.md @@ -0,0 +1,165 @@ +# Privacy Policy Page Implementation Guide + +## 📋 Implementation Overview + +I have successfully added a `/privacy` page to your project with complete privacy policy content in English. + +## 🚀 New Features + +### 1. Main Privacy Policy Page +- **Route**: `/privacy` +- **Content**: Complete privacy policy with original English text (no translation) +- **Style**: Professional responsive design with markdown-like rendering + +### 2. Simplified Privacy Policy Page +- **Route**: `/privacy-simple` +- **Content**: Simplified version with core information +- **Purpose**: Suitable for mobile or quick browsing scenarios + +## 📁 文件结构 + +``` +server/ +├── routers/ +│ └── pages_router.py # 新增:页面路由处理器 +├── templates/ +│ └── privacy_simple.html # 简化版隐私政策模板 +└── main.py # 已更新:注册新路由 +``` + +## 🎨 页面特性 + +### 设计特点 +- ✅ **响应式设计** - 适配桌面端和移动端 +- ✅ **专业外观** - 清晰的层次结构和色彩搭配 +- ✅ **易于阅读** - 合理的字体大小和行间距 +- ✅ **导航友好** - 包含返回首页的链接 + +### 内容结构 +- ✅ **最后更新时间** - August 26, 2025 +- ✅ **完整定义说明** - 解释所有关键术语 +- ✅ **数据收集说明** - 详细说明收集的信息类型 +- ✅ **使用目的** - 明确数据使用方式 +- ✅ **用户权利** - 说明用户的隐私权利 +- ✅ **安全措施** - 数据保护说明 +- ✅ **联系方式** - 提供联系渠道 + +## 🌐 访问方式 + +### 在浏览器中访问 +``` +http://localhost:8000/privacy # 完整版隐私政策 +http://localhost:8000/privacy-simple # 简化版隐私政策 +``` + +### 在生产环境中 +``` +https://yourdomain.com/privacy # 完整版 +https://yourdomain.com/privacy-simple # 简化版 +``` + +## 🔧 自定义指南 + +### 1. 修改内容 +编辑 `/server/routers/pages_router.py` 文件中的 HTML 内容: + +```python +# 在 privacy_policy() 函数中修改 privacy_html 变量 +privacy_html = f""" + + + +``` + +### 2. 修改样式 +在 HTML 的 ` +``` + +### 仅作为背景使用 + +如果你只想要背景效果,不需要完整的页面结构: + +```html +
+
+
+
+
+
+``` + +然后给父容器添加: + +```css +.your-container { + position: relative; + overflow: hidden; +} +``` + +## 🎯 性能优化建议 + +1. **GPU加速**:已使用`will-change: transform`启用GPU加速 +2. **减少重绘**:使用`backface-visibility: hidden`优化渲染 +3. **懒加载**:可以添加Intersection Observer来控制动画触发 +4. **减少模糊**:在性能敏感的设备上可以减少blur值 + +## 🌙 主题切换实现 + +```javascript +// 主题切换函数 +function toggleTheme() { + document.documentElement.classList.toggle('dark'); + localStorage.setItem('theme', + document.documentElement.classList.contains('dark') ? 'dark' : 'light' + ); +} + +// 恢复保存的主题 +const savedTheme = localStorage.getItem('theme'); +if (savedTheme === 'dark') { + document.documentElement.classList.add('dark'); +} +``` + +## 🔍 浏览器兼容性 + +- ✅ Chrome 88+ +- ✅ Firefox 89+ +- ✅ Safari 14+ +- ✅ Edge 88+ +- ⚠️ IE 不支持(使用了现代CSS特性) + +## 📄 许可证 + +MIT License - 可自由使用和修改 + +--- + +## 🎬 效果预览 + +打开 `lovable-gradient-demo.html` 查看完整效果演示! + +包含: +- 🎨 完整的渐变背景效果 +- 🌓 深色/浅色主题切换 +- 📱 响应式设计演示 +- ✨ 交互式按钮效果 \ No newline at end of file diff --git a/debug_callback.py b/debug_callback.py new file mode 100644 index 000000000..760b7b4bd --- /dev/null +++ b/debug_callback.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import asyncio +import sys +import os + +# 添加server目录到路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server')) + +from services.db_service import db_service + +async def debug_callback_logic(): + """直接测试回调中的产品查找逻辑""" + + product_id = "prod_24vhA7mt8RYKfTdLvU1oRd" # 来自回调的产品ID(实际是sku) + + print(f"🔍 调试回调产品查找逻辑") + print(f"🎯 测试product_id: {product_id}") + + try: + # 测试新的查找逻辑 + print("\n1️⃣ 尝试根据SKU查找...") + product_by_sku = await db_service.get_product_by_sku(product_id) + print(f" 结果: {product_by_sku}") + + if not product_by_sku: + print("\n2️⃣ SKU查找失败,尝试根据product_id查找...") + product_by_id = await db_service.get_product_by_id(product_id) + print(f" 结果: {product_by_id}") + + if not product_by_id: + print("\n❌ 两种方法都找不到产品!") + return False + else: + print(f"\n✅ 通过product_id找到产品: {product_by_id['name']}") + return True + else: + print(f"\n✅ 通过SKU找到产品: {product_by_sku['name']}") + return True + + except Exception as e: + print(f"❌ 查找异常: {e}") + import traceback + traceback.print_exc() + return False + +async def debug_order_lookup(): + """调试订单查找""" + + checkout_id = "ch_6Z2M36nuCLQTE1FNBN5ipp" + creem_order_id = "ord_rqmCnQm9zRwDcAn2EtMXc" + + print(f"\n🔍 调试订单查找逻辑") + print(f"🎯 checkout_id: {checkout_id}") + print(f"🎯 creem_order_id: {creem_order_id}") + + try: + # 先尝试根据creem_order_id查找 + order_by_creem_id = await db_service.get_order_by_creem_order_id(creem_order_id) + print(f"1️⃣ 根据creem_order_id查找: {order_by_creem_id}") + + # 再尝试根据checkout_id查找 + order_by_checkout_id = await db_service.get_order_by_checkout_id(checkout_id) + print(f"2️⃣ 根据checkout_id查找: {order_by_checkout_id}") + + return order_by_creem_id or order_by_checkout_id + + except Exception as e: + print(f"❌ 查找订单异常: {e}") + import traceback + traceback.print_exc() + return None + +async def main(): + """主测试函数""" + print("🧪 开始调试回调处理逻辑...\n") + + # 测试订单查找 + order = await debug_order_lookup() + if order: + print(f"✅ 找到订单: ID={order['id']}, status={order['status']}") + else: + print("❌ 订单查找失败") + return + + # 测试产品查找 + product_found = await debug_callback_logic() + + if product_found: + print("\n🎉 回调逻辑调试成功!") + else: + print("\n❌ 回调逻辑调试失败!") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/electron/comfyUIInstaller.js b/electron/comfyUIInstaller.js index 214e7e620..81fa8a7dd 100644 --- a/electron/comfyUIInstaller.js +++ b/electron/comfyUIInstaller.js @@ -10,8 +10,7 @@ const { pipeline } = require('stream/promises') const crypto = require('crypto') // Check if running in worker process -const isWorkerProcess = - process.send !== undefined || process.env.IS_WORKER_PROCESS === 'true' +const isWorkerProcess = process.send !== undefined || process.env.IS_WORKER_PROCESS === 'true' // Import electron modules only if not in worker process let app, BrowserWindow @@ -109,9 +108,7 @@ async function getLatestComfyUIRelease() { if (res.statusCode !== 200) { reject( new Error( - `GitHub API error: ${res.statusCode} - ${ - release.message || 'Unknown error' - }` + `GitHub API error: ${res.statusCode} - ${release.message || 'Unknown error'}` ) ) return @@ -134,11 +131,7 @@ async function getLatestComfyUIRelease() { ) if (!fallbackAsset) { - reject( - new Error( - 'No suitable Windows portable version found in latest release' - ) - ) + reject(new Error('No suitable Windows portable version found in latest release')) return } @@ -160,9 +153,7 @@ async function getLatestComfyUIRelease() { digest: windowsPortableAsset.digest, }) } catch (error) { - reject( - new Error(`Failed to parse GitHub API response: ${error.message}`) - ) + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)) } }) }) @@ -201,10 +192,7 @@ function sendProgress(percent, status) { if (mainWindow) { mainWindow.webContents.executeJavaScript(` window.dispatchEvent(new CustomEvent('comfyui-install-progress', { - detail: { percent: ${percent}, status: "${status.replace( - /"/g, - '\\"' - )}" } + detail: { percent: ${percent}, status: "${status.replace(/"/g, '\\"')}" } })); `) } @@ -368,9 +356,7 @@ async function downloadFile(url, filePath, onProgress, options = {}) { const stats = fs.statSync(filePath) resumeSize = stats.size if (resumeSize > 0) { - sendLog( - `Resuming download from ${Math.round(resumeSize / 1024 / 1024)}MB` - ) + sendLog(`Resuming download from ${Math.round(resumeSize / 1024 / 1024)}MB`) } } catch (error) { sendLog('Could not get existing file size, starting fresh download') @@ -479,10 +465,7 @@ async function downloadFile(url, filePath, onProgress, options = {}) { // Throttle progress updates const now = Date.now() - if ( - totalSize > 0 && - (now - lastProgressUpdate > 500 || downloadedSize >= totalSize) - ) { + if (totalSize > 0 && (now - lastProgressUpdate > 500 || downloadedSize >= totalSize)) { const progress = downloadedSize / totalSize onProgress(progress) lastProgressUpdate = now @@ -520,9 +503,7 @@ async function downloadFile(url, filePath, onProgress, options = {}) { if (totalSize > 0) { const stats = fs.statSync(filePath) if (stats.size !== totalSize) { - throw new Error( - `File size mismatch: expected ${totalSize}, got ${stats.size}` - ) + throw new Error(`File size mismatch: expected ${totalSize}, got ${stats.size}`) } } @@ -543,14 +524,9 @@ async function downloadFile(url, filePath, onProgress, options = {}) { // Keep partial file for network errors, remove for others const isNetworkError = error.code && - [ - 'ETIMEDOUT', - 'ECONNRESET', - 'ECONNREFUSED', - 'ENOTFOUND', - 'ENETUNREACH', - 'EPIPE', - ].includes(error.code) + ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETUNREACH', 'EPIPE'].includes( + error.code + ) if (!isNetworkError && fs.existsSync(filePath)) { try { @@ -584,9 +560,7 @@ async function downloadFile(url, filePath, onProgress, options = {}) { // Ignore cleanup errors } } - throw new Error( - `Download failed after ${maxRetries} attempts: ${error.message}` - ) + throw new Error(`Download failed after ${maxRetries} attempts: ${error.message}`) } } } @@ -617,15 +591,12 @@ function findComfyUIMainDir(comfyUIDir) { async function updateConfigWithComfyUI() { try { // Call backend API to update configuration - const response = await fetch( - 'http://127.0.0.1:57988/api/comfyui/update_config', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - } - ) + const response = await fetch('http://127.0.0.1:8000/api/comfyui/update_config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) if (response.ok) { const result = await response.json() @@ -746,9 +717,7 @@ async function installComfyUI() { releaseInfo = await getLatestComfyUIRelease() sendLog(`Found latest version: ${releaseInfo.version}`) sendLog( - `Download file: ${releaseInfo.fileName} (${Math.round( - releaseInfo.size / 1024 / 1024 - )}MB)` + `Download file: ${releaseInfo.fileName} (${Math.round(releaseInfo.size / 1024 / 1024)}MB)` ) } catch (error) { sendLog(`Failed to fetch latest release: ${error.message}`) @@ -793,9 +762,7 @@ async function installComfyUI() { const stats = fs.statSync(zipPath) if (stats.size > 1000000) { // At least 1MB, simple integrity check - sendLog( - 'Installation package appears complete based on size, skipping download' - ) + sendLog('Installation package appears complete based on size, skipping download') shouldDownload = false } else { sendLog('Installation package is incomplete, re-downloading') @@ -803,9 +770,7 @@ async function installComfyUI() { } } } catch (error) { - sendLog( - `Error checking installation package: ${error.message}, re-downloading` - ) + sendLog(`Error checking installation package: ${error.message}, re-downloading`) shouldDownload = true } } @@ -817,9 +782,7 @@ async function installComfyUI() { if (shouldDownload) { sendProgress(15, 'Starting ComfyUI download...') - sendLog( - `Downloading ComfyUI ${releaseInfo.version} from ${releaseInfo.downloadUrl}...` - ) + sendLog(`Downloading ComfyUI ${releaseInfo.version} from ${releaseInfo.downloadUrl}...`) // Download with enhanced retry configuration for large files await downloadFile( @@ -991,10 +954,7 @@ if (isWorkerProcess) { }) } } catch (error) { - console.error( - '🦄 ComfyUI installation failed in worker process:', - error - ) + console.error('🦄 ComfyUI installation failed in worker process:', error) // Send error result back to main process process.send({ @@ -1024,10 +984,7 @@ if (isWorkerProcess) { }) } } catch (error) { - console.error( - '🦄 ComfyUI uninstallation failed in worker process:', - error - ) + console.error('🦄 ComfyUI uninstallation failed in worker process:', error) // Send error result back to main process process.send({ diff --git a/electron/main.js b/electron/main.js index 5178e6cc7..2293f9407 100644 --- a/electron/main.js +++ b/electron/main.js @@ -203,7 +203,7 @@ const appRoot = app.getAppPath() const startPythonApi = async () => { // Find an available port - pyPort = await findAvailablePort(57988) + pyPort = await findAvailablePort(8000) console.log('available pyPort:', pyPort) // 在某些开发情况,我们希望 python server 独立运行,那么就不通过 electron 启动 diff --git a/lovable-gradient-background.css b/lovable-gradient-background.css new file mode 100644 index 000000000..fd0520562 --- /dev/null +++ b/lovable-gradient-background.css @@ -0,0 +1,140 @@ +/* Lovable风格的背景渐变效果 */ + +/* 滑入动画定义 */ +@keyframes slideUp { + 0% { + transform: translateY(10px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} + +/* 主背景容器 */ +.lovable-background-container { + position: relative; + min-height: 100vh; + width: 100%; + background-color: #fcfbf8; /* 浅色主题背景 */ + transition: none; + overflow: hidden; +} + +/* 深色主题背景 */ +.dark .lovable-background-container { + background-color: #1c1c1c; +} + +/* 渐变背景层 */ +.gradient-background-layer { + position: absolute; + inset: 0; + width: 100%; + overflow: hidden; +} + +/* 动画渐变元素 */ +.animated-gradient { + position: absolute; + inset: 0; + margin-top: 0; + opacity: 0; + filter: blur(10px); + animation: slideUp 1s ease-out 0.5s forwards; +} + +/* 渐变圆形 */ +.gradient-circle { + position: absolute; + left: 50%; + aspect-ratio: 1; + width: 350%; + transform: translateX(-50%); + overflow: hidden; + + /* 创建多色径向渐变,模仿Lovable的品牌色彩 */ + background: radial-gradient( + circle at center, + #4B73FF 0%, + #FF66F4 25%, + #FF0105 50%, + #FE7B02 75%, + transparent 100% + ); + + background-size: cover; + background-repeat: no-repeat; + background-position: center top; + + /* CSS遮罩创建渐变消失效果 */ + -webkit-mask: linear-gradient(to bottom, transparent 0%, black 5%, black 100%); + mask: linear-gradient(to bottom, transparent 0%, black 5%, black 100%); + + /* 性能优化 */ + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + will-change: transform; +} + +/* 响应式设计 */ +@media (min-width: 768px) { + .gradient-circle { + width: 190%; + } +} + +@media (min-width: 1024px) { + .gradient-circle { + width: 190%; + } +} + +@media (min-width: 1280px) { + .gradient-circle { + width: 190%; + } +} + +@media (min-width: 1536px) { + .gradient-circle { + width: 190%; + margin-left: auto; + margin-right: auto; + } +} + +/* 颗粒纹理层 */ +.grain-texture { + position: absolute; + inset: 0; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAALklEQVQoU2NkYGAwZiABMIIoo6ioiA4ODkbBWWwzMLMyMjIy/P//ny8rKysnJydc1cUlhgAAAABJRU5ErkJggg=='); + background-size: 100px 100px; + background-repeat: repeat; + background-blend-mode: overlay; + background-position: left top; + mix-blend-mode: overlay; + opacity: 0.5; +} + +/* 深色主题的渐变调整 */ +.dark .gradient-circle { + background: radial-gradient( + circle at center, + #4B73FF 0%, + #FF66F4 25%, + #FF0105 50%, + #FE7B02 75%, + transparent 100% + ); + opacity: 0.8; +} + +/* 内容层确保在背景之上 */ +.content-layer { + position: relative; + z-index: 10; +} \ No newline at end of file diff --git a/lovable-gradient-demo.html b/lovable-gradient-demo.html new file mode 100644 index 000000000..d42c26c74 --- /dev/null +++ b/lovable-gradient-demo.html @@ -0,0 +1,177 @@ + + + + + + Lovable风格渐变背景效果 + + + + + + + + +
+ +
+
+
+
+
+ + +
+ + +
+

构建精彩内容

+

+ 使用AI聊天界面创建应用程序和网站,体验Lovable风格的渐变背景效果 +

+ +
+
+ + + + \ No newline at end of file diff --git a/optimize_products.py b/optimize_products.py new file mode 100644 index 000000000..a333a59ae --- /dev/null +++ b/optimize_products.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +优化产品数据:统一产品ID格式和数据 +""" + +import sqlite3 +import os + +def optimize_products(): + print("🔧 优化产品数据...") + + db_path = "/Users/caijunjie/Dev/open-jaaz/server/user_data/localmanus.db" + + with sqlite3.connect(db_path) as conn: + # 1. 查看当前产品数据 + print("\n📋 当前产品数据:") + cursor = conn.execute("SELECT id, product_id, name, level, price_cents FROM tb_products ORDER BY level") + products = cursor.fetchall() + + for product in products: + print(f" {product[0]}: {product[1]} | {product[2]} | {product[3]} | ${product[4]/100:.2f}") + + # 2. 清理重复的产品 + print("\n🧹 清理重复产品...") + + # 删除旧的测试产品,保留真实的 Creem 产品ID + products_to_keep = { + 'base_monthly': 'prod_1Pnf8nR8OUqp55ziFzDNLM', # 真实的 Creem 产品 + 'base_yearly': 'prod_base_yearly', + 'pro_monthly': 'prod_pro_monthly', + 'pro_yearly': 'prod_pro_yearly', + 'max_monthly': 'prod_max_monthly', + 'max_yearly': 'prod_max_yearly', + } + + # 获取所有产品 + all_products = conn.execute("SELECT id, product_id, level FROM tb_products").fetchall() + + for level, keep_product_id in products_to_keep.items(): + # 找到这个level的所有产品 + level_products = [p for p in all_products if p[2] == level] + + if len(level_products) > 1: + print(f" 📦 Level {level} 有 {len(level_products)} 个产品:") + for p in level_products: + print(f" - ID {p[0]}: {p[1]}") + + # 保留指定的产品,删除其他的 + for p in level_products: + if p[1] != keep_product_id: + print(f" 🗑️ 删除重复产品: {p[1]}") + conn.execute("DELETE FROM tb_products WHERE id = ?", (p[0],)) + + # 3. 确保所需的产品都存在 + print("\n✅ 确保所需产品存在...") + + product_definitions = [ + ('prod_1Pnf8nR8OUqp55ziFzDNLM', 'Base Plan Monthly', 'base_monthly', 1000, 999, 'Basic features with monthly billing'), + ('prod_base_yearly', 'Base Plan Yearly', 'base_yearly', 12000, 9999, 'Basic features with yearly billing (save 17%)'), + ('prod_pro_monthly', 'Pro Plan Monthly', 'pro_monthly', 5000, 2999, 'Pro features with monthly billing'), + ('prod_pro_yearly', 'Pro Plan Yearly', 'pro_yearly', 60000, 29999, 'Pro features with yearly billing (save 17%)'), + ('prod_max_monthly', 'Max Plan Monthly', 'max_monthly', 10000, 4999, 'Maximum features with monthly billing'), + ('prod_max_yearly', 'Max Plan Yearly', 'max_yearly', 120000, 49999, 'Maximum features with yearly billing (save 17%)'), + ] + + for product_id, name, level, points, price_cents, description in product_definitions: + existing = conn.execute("SELECT id FROM tb_products WHERE product_id = ?", (product_id,)).fetchone() + + if not existing: + print(f" ➕ 添加缺失产品: {product_id} ({name})") + conn.execute(""" + INSERT INTO tb_products (product_id, name, level, points, price_cents, description, is_active) + VALUES (?, ?, ?, ?, ?, ?, 1) + """, (product_id, name, level, points, price_cents, description)) + else: + print(f" ✅ 产品已存在: {product_id}") + + # 4. 显示最终结果 + print("\n🎯 优化后的产品列表:") + cursor = conn.execute("SELECT product_id, name, level, price_cents FROM tb_products ORDER BY level, price_cents") + final_products = cursor.fetchall() + + for product in final_products: + print(f" ✓ {product[0]} | {product[1]} | {product[2]} | ${product[3]/100:.2f}") + + print(f"\n✅ 产品优化完成!共有 {len(final_products)} 个产品") + +if __name__ == "__main__": + optimize_products() \ No newline at end of file diff --git a/payment_demo.html b/payment_demo.html new file mode 100644 index 000000000..414f3472a --- /dev/null +++ b/payment_demo.html @@ -0,0 +1,434 @@ + + + + + + 嵌入式支付演示 + + + +
+

🚀 嵌入式支付演示

+

+ 点击下方按钮体验在当前窗口内完成支付,无需跳转到新页面! + 支付页面将在弹窗中显示,完成后自动关闭。 +

+ + + +
+
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/react/FRONTEND_HTTPS_FIX.md b/react/FRONTEND_HTTPS_FIX.md new file mode 100644 index 000000000..7abecec2c --- /dev/null +++ b/react/FRONTEND_HTTPS_FIX.md @@ -0,0 +1,122 @@ +# 前端HTTPS混合内容错误修复 + +## 问题描述 +线上环境出现混合内容错误: +``` +Mixed Content: The page at 'https://www.magicart.cc/canvas/...' was loaded over HTTPS, +but requested an insecure resource 'http://www.magicart.cc/api/billing/getBalance'. +This request has been blocked; the content must be served over HTTPS. +``` + +## 根本原因 +前端 `src/constants.ts` 文件中硬编码了开发环境的HTTP地址: +```javascript +export const BASE_API_URL = 'http://localhost:8000' // ❌ 错误:硬编码HTTP +``` + +正确的环境检测配置被注释掉了。 + +## 修复方案 + +### 修复内容 +将 `src/constants.ts` 中的配置修改为: + +**修复前:** +```javascript +// export const BASE_API_URL = import.meta.env.PROD +// ? 'https://www.magicart.cc' +// : 'http://localhost:8000' + +export const BASE_API_URL = 'http://localhost:8000' // ❌ 硬编码 +``` + +**修复后:** +```javascript +// 自动检测环境并使用正确的协议 +export const BASE_API_URL = import.meta.env.PROD + ? 'https://www.magicart.cc' // ✅ 生产环境使用HTTPS + : 'http://localhost:8000' // ✅ 开发环境使用HTTP +``` + +### 其他配置验证 + +#### Socket连接配置(已确认正确) +`src/contexts/socket.tsx` 中的配置是正确的: +```javascript +serverUrl: + process.env.NODE_ENV === 'development' + ? 'http://localhost:8000' // 开发环境 + : window.location.origin, // 生产环境自动使用当前协议(HTTPS) +``` + +## 环境检测机制 + +### Vite 环境变量 +- `import.meta.env.PROD`: 生产构建时为 `true` +- `import.meta.env.DEV`: 开发环境时为 `true` + +### 构建环境 +- **开发环境** (`npm run dev`): 使用 `http://localhost:8000` +- **生产环境** (`npm run build`): 使用 `https://www.magicart.cc` + +## 部署验证 + +### 1. 构建验证 +```bash +# 构建生产版本 +npm run build + +# 检查构建输出中的API配置 +grep -r "BASE_API_URL" dist/ +``` + +### 2. 运行时验证 +在浏览器开发者工具中检查: +```javascript +// 在生产环境控制台中执行 +console.log(window.location.origin) // 应该是 https://www.magicart.cc +``` + +### 3. 网络请求验证 +- 所有API请求应该使用 `https://www.magicart.cc/api/...` +- Socket连接应该使用 `wss://www.magicart.cc` +- 不应该再有HTTP请求 + +## 影响的功能 + +修复后以下功能的请求将使用正确协议: +- ✅ 账单查询 (`/api/billing/getBalance`) +- ✅ 用户认证 (`/api/auth/*`) +- ✅ 图片上传 (`/api/image/*`) +- ✅ 魔法生成 (`/api/magic/*`) +- ✅ Socket实时通信 +- ✅ 所有其他API请求 + +## 部署步骤 + +1. **重新构建前端**: + ```bash + npm run build + ``` + +2. **部署到生产环境**: + 将 `dist/` 目录部署到Web服务器 + +3. **验证修复**: + - 访问 `https://www.magicart.cc` + - 打开浏览器开发者工具 + - 确认没有混合内容错误 + - 测试API功能正常 + +## 预防措施 + +1. **代码审查**:确保不再硬编码HTTP地址 +2. **环境变量**:使用环境检测而非硬编码 +3. **测试覆盖**:添加环境配置的自动化测试 + +## 注意事项 + +- ⚠️ 确保生产环境的SSL证书正确配置 +- ⚠️ 确保后端API支持HTTPS +- ⚠️ 确保WebSocket支持WSS协议 +- ⚠️ 开发环境仍然使用HTTP(正常行为) \ No newline at end of file diff --git a/react/index.html b/react/index.html index 89bc8cf28..7ce3ba552 100644 --- a/react/index.html +++ b/react/index.html @@ -2,14 +2,14 @@ - + - Jaaz + MagicArt
diff --git a/react/package-lock.json b/react/package-lock.json index bca145e33..f527bbf01 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.1.8", "@tabler/icons-react": "^3.31.0", "@tailwindcss/vite": "^4.0.17", @@ -47,7 +48,6 @@ "nanoid": "^5.1.5", "next-themes": "^0.4.6", "openai": "^4.98.0", - "posthog-js": "^1.257.1", "rc-textarea": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -908,6 +908,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3596,18 +3597,79 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", - "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -3979,6 +4041,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5220,6 +5283,7 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -5229,7 +5293,7 @@ "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -6870,6 +6934,7 @@ }, "node_modules/esbuild": { "version": "0.25.6", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -6914,6 +6979,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6930,6 +6996,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6946,6 +7013,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6962,6 +7030,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6978,6 +7047,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6994,6 +7064,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7010,6 +7081,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7026,6 +7098,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7042,6 +7115,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7058,6 +7132,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7074,6 +7149,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7090,6 +7166,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7106,6 +7183,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7122,6 +7200,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7138,6 +7217,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7154,6 +7234,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7170,6 +7251,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7186,6 +7268,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7202,6 +7285,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7218,6 +7302,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7234,6 +7319,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7250,6 +7336,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7266,6 +7353,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7282,6 +7370,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7298,6 +7387,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7629,12 +7719,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fflate": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", - "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -7839,7 +7923,7 @@ }, "node_modules/get-tsconfig": { "version": "4.10.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -8258,17 +8342,6 @@ "version": "2.0.0", "license": "ISC" }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/jiti": { "version": "2.4.2", "license": "MIT", @@ -8434,28 +8507,6 @@ "version": "0.32.1", "license": "MIT" }, - "node_modules/lib0": { - "version": "0.2.114", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", - "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/lie": { "version": "3.3.0", "license": "MIT", @@ -10706,6 +10757,7 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -10755,6 +10807,7 @@ }, "node_modules/postcss": { "version": "8.5.6", + "dev": true, "funding": [ { "type": "opencollective", @@ -10781,6 +10834,7 @@ }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.11", + "dev": true, "funding": [ { "type": "github", @@ -10795,40 +10849,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/posthog-js": { - "version": "1.257.1", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.257.1.tgz", - "integrity": "sha512-29kk3IO/LkPQ8E1cds6a2sWr5iN4BovgL+EMzRK9hQXbI6D3FJnQ7zLU6EUpktt6pHnqGpfO3BTEcflcDYkHBg==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "core-js": "^3.38.1", - "fflate": "^0.4.8", - "preact": "^10.19.3", - "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@rrweb/types": "2.0.0-alpha.17", - "rrweb-snapshot": "2.0.0-alpha.17" - }, - "peerDependenciesMeta": { - "@rrweb/types": { - "optional": true - }, - "rrweb-snapshot": { - "optional": true - } - } - }, - "node_modules/preact": { - "version": "10.26.9", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", - "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -11157,6 +11177,36 @@ } } }, + "node_modules/radix-ui/node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/rc-input": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", @@ -11565,7 +11615,7 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -11586,6 +11636,7 @@ }, "node_modules/rollup": { "version": "4.45.1", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -11628,6 +11679,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11641,6 +11693,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11654,6 +11707,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11667,6 +11721,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11680,6 +11735,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11693,6 +11749,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11706,6 +11763,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11719,6 +11777,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11732,6 +11791,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11745,6 +11805,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11758,6 +11819,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11771,6 +11833,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11784,6 +11847,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11797,6 +11861,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11810,6 +11875,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11823,6 +11889,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11836,6 +11903,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11849,6 +11917,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11862,6 +11931,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12243,6 +12313,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.14", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -12257,6 +12328,7 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.4.6", + "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -12269,6 +12341,7 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12368,7 +12441,7 @@ }, "node_modules/tsx": { "version": "4.20.3", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.25.0", @@ -12443,7 +12516,7 @@ }, "node_modules/typescript": { "version": "5.7.3", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12754,6 +12827,7 @@ }, "node_modules/vite": { "version": "6.3.5", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -12826,6 +12900,7 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.4.6", + "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -12838,6 +12913,7 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12864,12 +12940,6 @@ "node": ">= 14" } }, - "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", - "license": "Apache-2.0" - }, "node_modules/web-worker": { "version": "1.5.0", "license": "Apache-2.0" @@ -12916,29 +12986,6 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "engines": { @@ -12950,24 +12997,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yjs": { - "version": "13.6.27", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", - "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", - "license": "MIT", - "peer": true, - "dependencies": { - "lib0": "^0.2.99" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -12981,7 +13010,7 @@ }, "node_modules/zod": { "version": "3.25.76", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/react/package.json b/react/package.json index 4bd668215..a98a83aa3 100644 --- a/react/package.json +++ b/react/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.1.8", "@tabler/icons-react": "^3.31.0", "@tailwindcss/vite": "^4.0.17", @@ -62,7 +63,6 @@ "nanoid": "^5.1.5", "next-themes": "^0.4.6", "openai": "^4.98.0", - "posthog-js": "^1.257.1", "rc-textarea": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/react/public/googlecef9e927b2687736.html b/react/public/googlecef9e927b2687736.html new file mode 100644 index 000000000..17fb19ba1 --- /dev/null +++ b/react/public/googlecef9e927b2687736.html @@ -0,0 +1 @@ +google-site-verification: googlecef9e927b2687736.html \ No newline at end of file diff --git a/react/public/magicart.png b/react/public/magicart.png new file mode 100644 index 000000000..a027869de Binary files /dev/null and b/react/public/magicart.png differ diff --git a/react/public/magicart.svg b/react/public/magicart.svg new file mode 100644 index 000000000..ea5685e15 --- /dev/null +++ b/react/public/magicart.svg @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/react/src/App.tsx b/react/src/App.tsx index 60cfbbeaa..8cbb421ba 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -12,7 +12,8 @@ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persi import { openDB } from 'idb' import { createRouter, RouterProvider } from '@tanstack/react-router' import { useEffect } from 'react' -import { Toaster } from 'sonner' +import { Toaster, toast } from 'sonner' +import { useTranslation } from 'react-i18next' import { routeTree } from './route-tree.gen' import '@/assets/style/App.css' @@ -64,8 +65,48 @@ const queryClient = new QueryClient({ }, }) +// 支付成功处理组件 +function PaymentSuccessHandler() { + const { t } = useTranslation() + + useEffect(() => { + const handlePaymentSuccess = () => { + const urlParams = new URLSearchParams(window.location.search) + const payment = urlParams.get('payment') + const points = urlParams.get('points') + const level = urlParams.get('level') + const orderId = urlParams.get('order_id') + + if (payment === 'success') { + console.log('🎉 Payment success detected:', { points, level, orderId }) + + // 显示成功通知 + toast.success(t('common:toast.paymentSuccess'), { + description: `恭喜您获得 ${points} 积分,等级已升级为 ${level}`, + duration: 8000, + }) + + // 清理URL参数,避免刷新页面时重复显示 + const newUrl = window.location.origin + window.location.pathname + window.history.replaceState({}, document.title, newUrl) + + // 触发认证状态刷新,确保积分和等级更新 + window.dispatchEvent(new CustomEvent('auth-force-refresh', { + detail: { source: 'payment-success' } + })) + } + } + + handlePaymentSuccess() + }, [t]) + + return null +} + function App() { const { theme } = useTheme() + const { t } = useTranslation() + // Auto-start ComfyUI on app startup useEffect(() => { @@ -109,6 +150,9 @@ function App() {
+ {/* Payment Success Handler */} + + {/* Install ComfyUI Dialog */} {/* */} @@ -124,7 +168,7 @@ function App() { - + ) } diff --git a/react/src/api/auth.ts b/react/src/api/auth.ts index 6dc0d55ad..f263e434f 100644 --- a/react/src/api/auth.ts +++ b/react/src/api/auth.ts @@ -1,6 +1,32 @@ import { BASE_API_URL } from '../constants' import i18n from '../i18n' import { clearJaazApiKey } from './config' +import { + isTokenExpired, + isTokenExpiringSoon, + getUserFromToken, + getTokenRemainingTime, +} from '../utils/jwt' +import { + AUTH_COOKIES, + setAuthCookie, + getAuthCookie, + deleteAuthCookie, + clearAuthCookies, +} from '../utils/cookies' +import { crossTabSync } from '../utils/crossTabSync' + +// 辅助函数:获取指定cookie的值 +function getCookieValue(name: string): string | null { + const cookies = document.cookie.split(';') + for (let cookie of cookies) { + cookie = cookie.trim() + if (cookie.startsWith(`${name}=`)) { + return cookie.substring(name.length + 1) + } + } + return null +} export interface AuthStatus { status: 'logged_out' | 'pending' | 'logged_in' @@ -17,6 +43,7 @@ export interface UserInfo { provider?: string created_at?: string updated_at?: string + level?: string } export interface DeviceAuthResponse { @@ -75,12 +102,8 @@ export async function startDeviceAuth(): Promise { } } -export async function pollDeviceAuth( - deviceCode: string -): Promise { - const response = await fetch( - `${BASE_API_URL}/api/device/poll?code=${deviceCode}` - ) +export async function pollDeviceAuth(deviceCode: string): Promise { + const response = await fetch(`${BASE_API_URL}/api/device/poll?code=${deviceCode}`) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) @@ -90,93 +113,613 @@ export async function pollDeviceAuth( } export async function getAuthStatus(): Promise { - // Get auth status from local storage - const token = localStorage.getItem('jaaz_access_token') - const userInfo = localStorage.getItem('jaaz_user_info') + // 🧹 步骤0:检查是否有logout标记,如果有则强制清理 + const logoutFlag = sessionStorage.getItem('force_logout') + if (logoutFlag === 'true') { + console.log('🚨 Logout flag detected, force clearing all auth data...') + await clearAuthData() + sessionStorage.removeItem('force_logout') + return { + status: 'logged_out' as const, + is_logged_in: false, + } + } + + // 🚨 检查是否在退出登录过程中,如果是则直接返回登出状态 + const isLoggingOut = sessionStorage.getItem('is_logging_out') + if (isLoggingOut === 'true') { + return { + status: 'logged_out' as const, + is_logged_in: false, + } + } + + // 🔄 首先检查后端httpOnly cookie是否存在 + const hasBackendAuthCookie = document.cookie.includes('auth_token=') && document.cookie.includes('user_uuid=') + + if (hasBackendAuthCookie) { + console.log('✅ Backend auth cookies detected, attempting to get real user info from API...') + + // 先检查是否有可用的前端token和用户信息 + let token = getAuthCookie(AUTH_COOKIES.ACCESS_TOKEN) + let userInfoStr = getAuthCookie(AUTH_COOKIES.USER_INFO) + + try { + // 调用后端API获取真实的用户信息(包括正确的level) + const response = await fetch(`${BASE_API_URL}/api/auth/check-status`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (response.ok) { + const authData = await response.json() + + if (authData.is_logged_in && authData.user_info && authData.token) { + console.log('🔄 Got real user info from backend API:', authData.user_info) + + // 始终同步最新的用户信息,确保level是最新的 + console.log('🔄 Syncing latest backend auth state to frontend...') + saveAuthData(authData.token, authData.user_info) + + return { + status: 'logged_in' as const, + is_logged_in: true, + user_info: authData.user_info, + } + } + } else { + console.log('❌ Backend auth API returned error:', response.status) + } + } catch (error) { + console.error('❌ Failed to get user info from backend API:', error) + } + + // Fallback:如果API调用失败,使用现有的前端cookie数据(如果有的话) + if (token && userInfoStr) { + console.log('🔄 Fallback: Using existing frontend cookie data...') + try { + const userInfo = JSON.parse(userInfoStr) + return { + status: 'logged_in' as const, + is_logged_in: true, + user_info: userInfo, + } + } catch (error) { + console.error('❌ Failed to parse user info from frontend cookie:', error) + } + } + + // 最后的fallback:使用基本的cookie信息创建用户信息 + const userUuid = getCookieValue('user_uuid') + const userEmail = getCookieValue('user_email') + + if (userUuid && userEmail) { + console.log('🔄 Last fallback: Creating user info from basic cookies...') + const backendUserInfo = { + id: userUuid, + username: userEmail.split('@')[0], + email: userEmail, + provider: 'google', + level: 'base' // 基于数据库信息,这个用户应该是base级别 + } + + const tempToken = `temp_${userUuid}_${Date.now()}` + saveAuthData(tempToken, backendUserInfo) + + return { + status: 'logged_in' as const, + is_logged_in: true, + user_info: backendUserInfo, + } + } + } + + // 🍪 fallback:从前端cookie读取,如果没有则尝试从localStorage迁移 + let token = getAuthCookie(AUTH_COOKIES.ACCESS_TOKEN) + let userInfoStr = getAuthCookie(AUTH_COOKIES.USER_INFO) + + // 📦 向后兼容:如果cookie中没有,尝试从localStorage迁移 + // 🚨 但是如果在logout过程中,不要迁移数据! + if (!token || !userInfoStr) { + const isLoggingOut = sessionStorage.getItem('is_logging_out') + const forceLogout = sessionStorage.getItem('force_logout') + + if (isLoggingOut === 'true' || forceLogout === 'true') { + console.log('🚪 Logout in progress, skipping localStorage migration') + } else { + const legacyToken = localStorage.getItem('jaaz_access_token') + const legacyUserInfo = localStorage.getItem('jaaz_user_info') + + if (legacyToken && legacyUserInfo) { + try { + // 迁移到cookie + saveAuthData(legacyToken, JSON.parse(legacyUserInfo)) + // 清理localStorage + localStorage.removeItem('jaaz_access_token') + localStorage.removeItem('jaaz_user_info') + + token = legacyToken + userInfoStr = legacyUserInfo + } catch (error) { + console.error('❌ Failed to migrate auth data:', error) + } + } + } + } - console.log('Getting auth status:', { + console.log('📋 Final auth data check:', { hasToken: !!token, - hasUserInfo: !!userInfo, - userInfo: userInfo ? JSON.parse(userInfo) : null, + hasUserInfo: !!userInfoStr, + userInfo: userInfoStr ? JSON.parse(userInfoStr) : null, }) - if (token && userInfo) { + if (!token || !userInfoStr) { + const loggedOutStatus = { + status: 'logged_out' as const, + is_logged_in: false, + } + console.log('❌ No valid auth data found, returning logged out status') + return loggedOutStatus + } + + // 🔥 简化Token检查:主要依赖cookie存在性,减少网络请求 + const remainingTime = getTokenRemainingTime(token) + console.log(`Token remaining time: ${Math.floor(remainingTime / 60)} minutes`) + + // 只有当token真正过期时才尝试刷新 + if (isTokenExpired(token)) { + console.log('⏰ Token is expired, attempting refresh') + try { - // Always try to refresh token when we have one const newToken = await refreshToken(token) + setAuthCookie(AUTH_COOKIES.ACCESS_TOKEN, newToken, 30) // 30天过期 + console.log('✅ Expired token refreshed successfully') - // Save the new token - localStorage.setItem('jaaz_access_token', newToken) - console.log('Token refreshed successfully') - - const authStatus = { + return { status: 'logged_in' as const, is_logged_in: true, - user_info: JSON.parse(userInfo), + user_info: JSON.parse(userInfoStr), } - return authStatus } catch (error) { - console.log('Token refresh failed:', error) + console.log('❌ Failed to refresh expired token:', error) - // Only clear auth data if token is truly expired (401), not for network errors - if (error instanceof Error && error.message === 'TOKEN_EXPIRED') { - console.log('Token expired, clearing auth data') - localStorage.removeItem('jaaz_access_token') - localStorage.removeItem('jaaz_user_info') + // 清理过期的认证数据 + await clearAuthData() - // Clear jaaz provider api_key - try { - await clearJaazApiKey() - } catch (clearError) { - console.error('Failed to clear jaaz api key:', clearError) - } + return { + status: 'logged_out' as const, + is_logged_in: false, + tokenExpired: true, + } + } + } - const loggedOutStatus = { - status: 'logged_out' as const, - is_logged_in: false, - tokenExpired: true, - } + // 🎯 Token有效,检查用户信息是否包含level字段 + let userInfo + try { + userInfo = JSON.parse(userInfoStr) + } catch (error) { + console.error('❌ Failed to parse user info:', error) + await clearAuthData() + return { + status: 'logged_out' as const, + is_logged_in: false, + } + } - return loggedOutStatus - } else { - // Network error or other issues, keep user logged in with old token - console.log( - 'Network error during token refresh, keeping user logged in with existing token' - ) - const authStatus = { - status: 'logged_in' as const, - is_logged_in: true, - user_info: JSON.parse(userInfo), + // 🚨 检查用户信息是否包含level字段,如果没有则使用数据库默认值 + if (!userInfo.level) { + console.log('⚠️ User info missing level field, adding default level based on database') + // 基于我们知道的用户信息,这个用户在数据库中是base级别 + if (userInfo.email === 'yzcaijunjie@gmail.com') { + userInfo.level = 'base' + console.log('🔧 Updated user level to: base (from database)') + // 更新本地存储 + setAuthCookie(AUTH_COOKIES.USER_INFO, JSON.stringify(userInfo), 30) + } else { + userInfo.level = 'free' // 默认级别 + console.log('🔧 Set default user level to: free') + } + } + + console.log('📋 Final user info with level:', userInfo) + + // 返回登录状态 + return { + status: 'logged_in' as const, + is_logged_in: true, + user_info: userInfo, + } +} + +// 手动删除cookie的工具函数 +function deleteCookieManually(name: string): void { + console.log(`🗑️ === DELETING COOKIE: ${name} ===`) + console.log(`🔍 Cookie before deletion: ${document.cookie}`) + console.log(`🔍 Cookie ${name} exists before deletion: ${document.cookie.includes(`${name}=`)}`) + + // 尝试多种path和domain组合确保删除成功 + const paths = ['/', '/api', ''] + const domains = [ + '', + `.${window.location.hostname}`, + window.location.hostname, + 'localhost', + '.localhost', + ] + + let deleteCommands = [] + + paths.forEach((path) => { + domains.forEach((domain) => { + // 基本删除 + const cmd1 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};` + deleteCommands.push(cmd1) + document.cookie = cmd1 + + // 带domain的删除 + if (domain) { + const cmd2 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};` + deleteCommands.push(cmd2) + document.cookie = cmd2 + } + + // 带secure的删除(HTTPS环境) + if (window.location.protocol === 'https:') { + const cmd3 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; secure;` + deleteCommands.push(cmd3) + document.cookie = cmd3 + if (domain) { + const cmd4 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; secure;` + deleteCommands.push(cmd4) + document.cookie = cmd4 } - return authStatus } + + // 带samesite的删除 + const cmd5 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; samesite=lax;` + deleteCommands.push(cmd5) + document.cookie = cmd5 + if (domain) { + const cmd6 = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; samesite=lax;` + deleteCommands.push(cmd6) + document.cookie = cmd6 + } + }) + }) + + console.log(`🗑️ Executed ${deleteCommands.length} delete commands for ${name}`) + console.log(`🔍 Cookie after deletion: ${document.cookie}`) + + // 验证删除结果 + const stillExists = document.cookie.includes(`${name}=`) + console.log(`🔍 Cookie ${name} still exists after deletion: ${stillExists}`) + + if (stillExists) { + console.error(`❌ FAILED TO DELETE COOKIE: ${name}`) + } else { + console.log(`✅ Successfully deleted cookie: ${name}`) + } +} + +// 暴力清理所有cookie的函数 +function nukeAllCookies(): void { + console.log('💣 Nuclear option: deleting ALL cookies...') + + // 获取当前所有cookie + const cookies = document.cookie.split(';') + + cookies.forEach((cookie) => { + const eqPos = cookie.indexOf('=') + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim() + + if (name) { + // 对每个cookie使用多种删除方式 + const deleteCommands = [ + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname};`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=localhost;`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.localhost;`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/api;`, + `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=;`, + ] + + deleteCommands.forEach((cmd) => { + document.cookie = cmd + }) + + console.log(`💥 Nuked cookie: ${name}`) + } + }) +} + +// 清理认证数据的辅助函数 +export async function clearAuthData(): Promise { + console.log('🧹 === STARTING COMPREHENSIVE AUTH DATA CLEANUP ===') + console.log(`🔍 Initial cookie state: ${document.cookie}`) + + // 🍪 手动删除所有可能的认证cookie + console.log('🍪 Manually clearing all auth cookies...') + const allAuthCookies = [ + // 前端使用的cookie + 'jaaz_access_token', + 'jaaz_user_info', + 'jaaz_token_expires', + // 后端使用的cookie + 'auth_token', + 'user_uuid', + 'user_email', + // 其他可能的cookie + 'access_token', + 'user_info', + 'refresh_token', + ] + + console.log(`🎯 Targeting ${allAuthCookies.length} auth cookies:`, allAuthCookies) + + allAuthCookies.forEach((cookieName, index) => { + console.log(`\n🗑️ [${index + 1}/${allAuthCookies.length}] Processing cookie: ${cookieName}`) + deleteCookieManually(cookieName) + }) + + console.log('\n📋 Checking remaining auth cookies...') + const remainingAuthCookies = allAuthCookies.filter((name) => document.cookie.includes(`${name}=`)) + console.log(`⚠️ Remaining auth cookies: [${remainingAuthCookies.join(', ')}]`) + + // 💣 如果还有认证相关的cookie存在,使用核武器方案 + if (remainingAuthCookies.length > 0) { + console.log('💣 Some auth cookies still exist, using nuclear option...') + nukeAllCookies() + + // 再次检查 + const finalRemainingCookies = allAuthCookies.filter((name) => + document.cookie.includes(`${name}=`) + ) + console.log( + `🔍 After nuclear option, remaining auth cookies: [${finalRemainingCookies.join(', ')}]` + ) + } + + // 🧹 清理localStorage中所有可能的认证数据(包括备份数据) + console.log('📦 Clearing localStorage...') + const authKeys = [ + 'jaaz_access_token', + 'jaaz_user_info', + 'jaaz_refresh_token', + 'auth_token', + 'user_info', + 'access_token', + 'user_uuid', + 'user_email', + // 🔄 清理所有备份数据 + 'backup_jaaz_access_token', + 'backup_jaaz_user_info', + 'backup_jaaz_token_expires', + 'backup_jaaz_access_token_expires', + 'backup_jaaz_user_info_expires', + ] + + // 记录清理前的状态 + console.log('📋 localStorage before clearing:') + authKeys.forEach((key) => { + const value = localStorage.getItem(key) + console.log(` ${key}: ${value ? value.substring(0, 20) + '...' : 'null'}`) + }) + + authKeys.forEach((key) => { + localStorage.removeItem(key) + console.log(`🗑️ Removed localStorage key: ${key}`) + }) + + // 验证清理结果 + console.log('📋 localStorage after clearing:') + authKeys.forEach((key) => { + const value = localStorage.getItem(key) + if (value) { + console.error(`❌ Failed to clear localStorage key: ${key}`) + } else { + console.log(`✅ Cleared localStorage key: ${key}`) + } + }) + + // 🧹 清理sessionStorage中可能的认证数据 + console.log('📝 Clearing sessionStorage...') + authKeys.forEach((key) => { + sessionStorage.removeItem(key) + }) + + // 🧹 清理所有以jaaz_或backup_开头的localStorage项 + console.log('🔍 Scanning for remaining jaaz/backup data in localStorage...') + const allLocalStorageKeys = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key) { + allLocalStorageKeys.push(key) + } + } + + const jaazKeys = allLocalStorageKeys.filter(key => + key.startsWith('jaaz_') || + key.startsWith('backup_jaaz_') || + key.startsWith('backup_auth') || + key.includes('auth') || + key.includes('token') || + key.includes('user') + ) + + console.log(`🎯 Found ${jaazKeys.length} potential auth keys:`, jaazKeys) + jaazKeys.forEach(key => { + localStorage.removeItem(key) + console.log(`🗑️ Removed additional key: ${key}`) + }) + + // 🧹 清理sessionStorage中的logout标志以外的所有认证相关数据 + console.log('🔍 Scanning sessionStorage for auth data...') + const sessionKeys = [] + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i) + if (key && !key.includes('logout') && ( + key.startsWith('jaaz_') || + key.includes('auth') || + key.includes('token') || + key.includes('user') + )) { + sessionKeys.push(key) } } + + sessionKeys.forEach(key => { + sessionStorage.removeItem(key) + console.log(`🗑️ Removed sessionStorage key: ${key}`) + }) - const loggedOutStatus = { - status: 'logged_out' as const, - is_logged_in: false, + // 🔑 清理API密钥 + try { + console.log('🔑 Clearing API keys...') + await clearJaazApiKey() + } catch (error) { + console.error('Failed to clear jaaz api key:', error) } - console.log('Returning logged out status:', loggedOutStatus) - return loggedOutStatus + + console.log('✅ Auth data cleanup completed') + + // 🔍 验证清理结果 + console.log('🔍 Verifying cleanup results...') + console.log('Current cookies:', document.cookie) } export async function logout(): Promise<{ status: string; message: string }> { - // Clear local storage - localStorage.removeItem('jaaz_access_token') - localStorage.removeItem('jaaz_user_info') + console.log('🚪 === STARTING LOGOUT PROCESS ===') + console.log(`🔍 Cookie state before logout: ${document.cookie}`) - // Clear jaaz provider api_key - await clearJaazApiKey() + try { + // 🚨 步骤0:设置退出登录标记,阻止getAuthStatus重新设置cookie + console.log('🚨 Setting logout flags...') + sessionStorage.setItem('is_logging_out', 'true') + sessionStorage.setItem('force_logout', 'true') - return { - status: 'success', - message: i18n.t('common:auth.logoutSuccessMessage'), + // 🧹 步骤1:立即清理前端认证数据(不调用后端) + console.log('🧹 Clearing client-side auth data immediately...') + await clearAuthData() + + console.log(`🔍 Cookie state after clearAuthData: ${document.cookie}`) + + // 📢 步骤2:立即更新本标签页的UI状态 + console.log('🎯 Updating local auth state immediately...') + window.dispatchEvent(new CustomEvent('auth-logout-detected', { + detail: { source: 'local-logout' } + })) + + // 📢 步骤3:通知其他标签页用户已登出 + console.log('📢 Notifying other tabs...') + crossTabSync.notifyLogout() + + // 🔄 步骤4:调用后端API删除httponly cookie + console.log('🔗 Calling backend logout API to delete httponly cookies...') + + try { + const response = await fetch(`${BASE_API_URL}/api/auth/logout`, { + method: 'POST', + credentials: 'include', // 重要:包含cookie以便后端清理 + }) + + console.log(`✅ Backend logout API response status: ${response.status}`) + + if (response.ok) { + const data = await response.json() + console.log('✅ Backend logout successful:', data) + } else { + console.warn(`⚠️ Backend logout API returned status: ${response.status}`) + } + } catch (error) { + console.error('❌ Backend logout API failed:', error) + // 继续执行,不让API失败阻止logout流程 + } + + console.log(`🔍 Cookie state after backend logout: ${document.cookie}`) + + // 🔄 步骤5:清理logout标记,让UI自然更新 + console.log('🧹 Cleaning up logout flags...') + + // 给UI足够时间更新状态 + setTimeout(() => { + console.log(`🔍 Final cookie state: ${document.cookie}`) + // 清理is_logging_out标记,但保留force_logout标记一段时间防止恢复 + sessionStorage.removeItem('is_logging_out') + + // 延迟清理force_logout标记,确保不会意外恢复登录状态 + setTimeout(() => { + sessionStorage.removeItem('force_logout') + console.log('✅ Logout process completed, UI should be updated') + }, 1000) + }, 200) // 给AuthContext更多时间处理状态变化 + + return { + status: 'success', + message: i18n.t('common:auth.logoutSuccessMessage'), + } + } catch (error) { + console.error('❌ Logout process failed:', error) + + // 🛡️ 兜底方案:即使出错也要确保本地数据被清理 + try { + console.log('🛡️ Executing fallback logout...') + sessionStorage.setItem('is_logging_out', 'true') + sessionStorage.setItem('force_logout', 'true') + await clearAuthData() + + // 立即更新本地UI状态 + window.dispatchEvent(new CustomEvent('auth-logout-detected', { + detail: { source: 'fallback-logout' } + })) + + crossTabSync.notifyLogout() + + // 尝试调用后端API作为fallback + try { + console.log('🔗 Fallback: calling backend logout API...') + await fetch(`${BASE_API_URL}/api/auth/logout`, { + method: 'POST', + credentials: 'include', + }) + console.log('✅ Fallback backend logout completed') + } catch (backendError) { + console.warn('⚠️ Fallback backend logout failed:', backendError) + } + + // 清理logout标记,让UI自然更新 + setTimeout(() => { + sessionStorage.removeItem('is_logging_out') + setTimeout(() => { + sessionStorage.removeItem('force_logout') + console.log('✅ Fallback logout completed') + }, 1000) + }, 200) + + return { + status: 'success', + message: i18n.t('common:auth.logoutSuccessMessage'), + } + } catch (fallbackError) { + console.error('❌ Even fallback logout failed:', fallbackError) + + // 最后的最后:直接刷新页面 + window.location.reload() + + return { + status: 'error', + message: 'Logout failed, page will be refreshed', + } + } } } export async function getUserProfile(): Promise { - const userInfo = localStorage.getItem('jaaz_user_info') + const userInfo = getAuthCookie(AUTH_COOKIES.USER_INFO) if (!userInfo) { throw new Error(i18n.t('common:auth.notLoggedIn')) } @@ -184,23 +727,83 @@ export async function getUserProfile(): Promise { return JSON.parse(userInfo) } -// Helper function to save auth data to local storage +// Helper function to save auth data to cookies export function saveAuthData(token: string, userInfo: UserInfo) { - localStorage.setItem('jaaz_access_token', token) - localStorage.setItem('jaaz_user_info', JSON.stringify(userInfo)) + console.log('💾 === ATTEMPTING TO SAVE AUTH DATA ===') + console.log(`🔍 Current cookies before save: ${document.cookie}`) + + // 🚨 检查是否在退出登录过程中,如果是则阻止保存 + const isLoggingOut = sessionStorage.getItem('is_logging_out') + const forceLogout = sessionStorage.getItem('force_logout') + + if (isLoggingOut === 'true' || forceLogout === 'true') { + console.error('🚨 BLOCKED: Attempted to save auth data during logout process!') + console.log('🚪 Logout flags detected, refusing to save auth data') + return + } + + console.log('💾 Saving auth data to cookies...', { + tokenLength: token ? token.length : 0, + userEmail: userInfo?.email, + userId: userInfo?.id, + }) + + try { + // 🍪 保存到cookie,30天过期 + setAuthCookie(AUTH_COOKIES.ACCESS_TOKEN, token, 30) + setAuthCookie(AUTH_COOKIES.USER_INFO, JSON.stringify(userInfo), 30) + + // 📅 保存token过期时间,用于更精确的过期检查 + const tokenExpireTime = getTokenRemainingTime(token) + Math.floor(Date.now() / 1000) + setAuthCookie(AUTH_COOKIES.TOKEN_EXPIRES, tokenExpireTime.toString(), 30) + + console.log(`🔍 Cookies after save attempt: ${document.cookie}`) + + // 验证保存是否成功 + const savedToken = getAuthCookie(AUTH_COOKIES.ACCESS_TOKEN) + const savedUserInfo = getAuthCookie(AUTH_COOKIES.USER_INFO) + + if (savedToken && savedUserInfo) { + console.log('✅ Auth data successfully saved to cookies') + } else { + console.error('❌ Failed to verify saved auth data in cookies') + } + } catch (error) { + console.error('❌ Error saving auth data to cookies:', error) + } } // Helper function to get access token export function getAccessToken(): string | null { - return localStorage.getItem('jaaz_access_token') + return getAuthCookie(AUTH_COOKIES.ACCESS_TOKEN) } -// Helper function to make authenticated API calls +// Helper function to make authenticated API calls with automatic token refresh export async function authenticatedFetch( url: string, options: RequestInit = {} ): Promise { - const token = getAccessToken() + let token = getAccessToken() + + // 如果没有token,直接返回 + if (!token) { + return fetch(url, options) + } + + // 🎯 简化逻辑:只检查token是否已过期,不做预刷新 + if (isTokenExpired(token)) { + console.log('⏰ Token expired, attempting refresh before API call') + try { + const newToken = await refreshToken(token) + setAuthCookie(AUTH_COOKIES.ACCESS_TOKEN, newToken, 30) + token = newToken + console.log('✅ Token refreshed before API call') + } catch (error) { + console.log('❌ Failed to refresh token before API call:', error) + await clearAuthData() + throw new Error('Authentication failed: Token expired and refresh failed') + } + } const headers: Record = { 'Content-Type': 'application/json', @@ -211,13 +814,71 @@ export async function authenticatedFetch( headers['Authorization'] = `Bearer ${token}` } - return fetch(url, { + const response = await fetch(url, { ...options, headers, }) + + // 🚀 如果响应是401,尝试刷新token并重试一次 + if (response.status === 401 && token) { + console.log('Received 401, attempting token refresh and retry') + try { + const newToken = await refreshToken(token) + setAuthCookie(AUTH_COOKIES.ACCESS_TOKEN, newToken, 30) // 保存到cookie + + // 用新token重试请求 + headers['Authorization'] = `Bearer ${newToken}` + const retryResponse = await fetch(url, { + ...options, + headers, + }) + + console.log('Request retried successfully with new token') + return retryResponse + } catch (error) { + console.log('Token refresh failed after 401:', error) + // 刷新失败,清理认证数据 + await clearAuthData() + // 返回原始的401响应 + return response + } + } + + return response } // 刷新token +// 完成认证(从URL参数获取设备码后调用) +export async function completeAuth(deviceCode: string): Promise { + const response = await fetch(`${BASE_API_URL}/api/device/complete?device_code=${deviceCode}`) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return await response.json() +} + +// 检查URL参数中的认证状态 +export function checkUrlAuthParams(): { + authSuccess: boolean + deviceCode?: string + authError?: string +} { + const urlParams = new URLSearchParams(window.location.search) + const authSuccess = urlParams.get('auth_success') === 'true' + const deviceCode = urlParams.get('device_code') + const authError = urlParams.get('auth_error') + + // 清理URL参数 + if (authSuccess || authError) { + const newUrl = window.location.pathname + window.history.replaceState({}, document.title, newUrl) + } + + return { authSuccess, deviceCode, authError } +} + export async function refreshToken(currentToken: string) { const response = await fetch(`${BASE_API_URL}/api/device/refresh-token`, { method: 'GET', @@ -237,3 +898,41 @@ export async function refreshToken(currentToken: string) { throw new Error(`NETWORK_ERROR: ${response.status}`) } } + +// 直接登录:在当前窗口跳转到Google OAuth +export function directLogin(): void { + const authUrl = `${BASE_API_URL}/auth/login` + window.location.href = authUrl +} + +// 检查URL参数中的直接认证数据 +export function checkDirectAuthParams(): { + authSuccess: boolean + authData?: { token: string; user_info: UserInfo } + authError?: string +} { + const urlParams = new URLSearchParams(window.location.search) + const authSuccess = urlParams.get('auth_success') === 'true' + const encodedAuthData = urlParams.get('auth_data') + const authError = urlParams.get('auth_error') + + let authData = undefined + + if (authSuccess && encodedAuthData) { + try { + // 解码认证数据 + const decodedData = atob(encodedAuthData) + authData = JSON.parse(decodedData) + } catch (error) { + console.error('Failed to decode auth data:', error) + } + } + + // 清理URL参数 + if (authSuccess || authError) { + const newUrl = window.location.pathname + window.history.replaceState({}, document.title, newUrl) + } + + return { authSuccess, authData, authError } +} diff --git a/react/src/api/billing.ts b/react/src/api/billing.ts index 1055f6f43..55fcba404 100644 --- a/react/src/api/billing.ts +++ b/react/src/api/billing.ts @@ -5,6 +5,20 @@ export interface BalanceResponse { balance: string } +export interface UserInfoResponse { + is_logged_in: boolean + current_level: string | null + user_info?: { + id: number + email: string + username: string + level: string + image_url?: string + } + available_plans?: string[] + message?: string +} + export async function getBalance(): Promise { const response = await authenticatedFetch( `${BASE_API_URL}/api/billing/getBalance` @@ -16,3 +30,15 @@ export async function getBalance(): Promise { return await response.json() } + +export async function getUserInfo(): Promise { + const response = await authenticatedFetch( + `${BASE_API_URL}/api/pricing` + ) + + if (!response.ok) { + throw new Error(`Failed to fetch user info: ${response.status}`) + } + + return await response.json() +} diff --git a/react/src/api/canvas.ts b/react/src/api/canvas.ts index 7136d68fe..6283acda8 100644 --- a/react/src/api/canvas.ts +++ b/react/src/api/canvas.ts @@ -1,5 +1,6 @@ import { CanvasData, Message, Session } from '@/types/types' import { ToolInfo } from '@/api/model' +import { authenticatedFetch } from '@/api/auth' export type ListCanvasesResponse = { id: string @@ -10,7 +11,17 @@ export type ListCanvasesResponse = { } export async function listCanvases(): Promise { - const response = await fetch('/api/canvas/list') + const response = await authenticatedFetch('/api/canvas/list') + + if (!response.ok) { + // 如果认证失败(401),返回空数组而不是抛出错误 + if (response.status === 401) { + console.log('🚨 listCanvases: User not authenticated, returning empty list') + return [] + } + throw new Error(`Failed to fetch canvases: ${response.status}`) + } + return await response.json() } @@ -23,16 +34,23 @@ export async function createCanvas(data: { provider: string model: string url: string - } + } | null tool_list: ToolInfo[] - + model_name?: string system_prompt: string + template_id?: number }): Promise<{ id: string }> { const response = await fetch('/api/canvas/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Canvas creation failed: ${response.status} ${response.statusText} - ${errorText}`) + } + return await response.json() } @@ -73,3 +91,18 @@ export async function deleteCanvas(id: string): Promise { }) return await response.json() } + +export async function renameSession(sessionId: string, title: string): Promise { + const response = await fetch(`/api/canvas/session/${sessionId}/rename`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to rename session') + } + + return await response.json() +} diff --git a/react/src/api/chat.ts b/react/src/api/chat.ts index 4798e55be..a5aa71217 100644 --- a/react/src/api/chat.ts +++ b/react/src/api/chat.ts @@ -11,8 +11,7 @@ export const sendMessages = async (payload: { sessionId: string canvasId: string newMessages: Message[] - textModel: Model - toolList: ToolInfo[] + modelName: string systemPrompt: string | null }) => { const response = await fetch(`/api/chat`, { @@ -24,8 +23,7 @@ export const sendMessages = async (payload: { messages: payload.newMessages, canvas_id: payload.canvasId, session_id: payload.sessionId, - text_model: payload.textModel, - tool_list: payload.toolList, + model_name: payload.modelName, system_prompt: payload.systemPrompt, }), }) diff --git a/react/src/api/invite.ts b/react/src/api/invite.ts new file mode 100644 index 000000000..500fe31b3 --- /dev/null +++ b/react/src/api/invite.ts @@ -0,0 +1,219 @@ +import { BASE_API_URL } from '@/constants' + +// 类型定义 +export interface InviteCode { + success: boolean + code?: string + used_count: number + max_uses: number + remaining_uses: number + error?: string +} + +export interface InviteStats { + invite_code?: string + used_count: number + max_uses: number + remaining_uses: number + total_invitations: number + successful_invitations: number + total_points_earned: number + pending_invitations: number +} + +export interface InviteHistory { + success: boolean + history: InviteRecord[] + total_count: number +} + +export interface InviteRecord { + id: number + invitee_email: string + status: 'pending' | 'registered' | 'completed' + inviter_points_awarded: number + created_at: string + completed_at?: string + invitee_nickname?: string +} + +export interface PointsBalance { + success: boolean + balance: number +} + +export interface PointsHistory { + success: boolean + history: PointsTransaction[] + total_count: number +} + +export interface PointsTransaction { + id: number + points: number + type: 'earn_invite' | 'earn_register' | 'spend' | 'admin_adjust' + description: string + reference_id?: string + balance_after: number + created_at: string +} + +export interface PointsStats { + success: boolean + stats: { + current_balance: number + total_earned: number + total_spent: number + net_points: number + by_type: Record + } +} + +// API 方法 +export async function getMyInviteCode(): Promise { + const response = await fetch(`${BASE_API_URL}/api/invite/my-code`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get invite code: ${response.statusText}`) + } + + return response.json() +} + +export async function validateInviteCode(code: string): Promise<{ + is_valid: boolean + reason?: string + inviter_nickname?: string +}> { + const response = await fetch(`${BASE_API_URL}/api/invite/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code }), + }) + + if (!response.ok) { + throw new Error(`Failed to validate invite code: ${response.statusText}`) + } + + return response.json() +} + +export async function getInviteStats(): Promise { + const response = await fetch(`${BASE_API_URL}/api/invite/stats`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get invite stats: ${response.statusText}`) + } + + return response.json() +} + +export async function getInviteHistory(limit: number = 20, offset: number = 0): Promise { + const response = await fetch(`${BASE_API_URL}/api/invite/history?limit=${limit}&offset=${offset}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get invite history: ${response.statusText}`) + } + + return response.json() +} + +export async function getPointsBalance(): Promise { + const response = await fetch(`${BASE_API_URL}/api/points/balance`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get points balance: ${response.statusText}`) + } + + return response.json() +} + +export async function getPointsHistory(limit: number = 50, offset: number = 0): Promise { + const response = await fetch(`${BASE_API_URL}/api/points/history?limit=${limit}&offset=${offset}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get points history: ${response.statusText}`) + } + + return response.json() +} + +export async function getPointsStats(): Promise { + const response = await fetch(`${BASE_API_URL}/api/points/stats`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`Failed to get points stats: ${response.statusText}`) + } + + return response.json() +} + +// 工具方法 +export function generateInviteUrl(code: string): string { + const baseUrl = window.location.origin + return `${baseUrl}/join/${code}` +} + +export function copyToClipboard(text: string): boolean { + try { + navigator.clipboard.writeText(text) + return true + } catch (err) { + // 备用方案,用于不支持clipboard API的浏览器 + const textArea = document.createElement('textarea') + textArea.value = text + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + try { + document.execCommand('copy') + return true + } catch (err) { + return false + } finally { + document.body.removeChild(textArea) + } + } +} \ No newline at end of file diff --git a/react/src/api/magic.ts b/react/src/api/magic.ts index 89c394faa..903b3fd16 100644 --- a/react/src/api/magic.ts +++ b/react/src/api/magic.ts @@ -4,23 +4,87 @@ import { ToolInfo } from './model' export const sendMagicGenerate = async (payload: { sessionId: string canvasId: string - newMessages: Message[] + newMessages: Array<{ + role: string; + content: string | Array<{ + type: string; + text?: string; + image_url?: { url: string } + }> + }> systemPrompt: string | null + templateId?: number }) => { - const response = await fetch(`/api/magic`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - messages: payload.newMessages, - canvas_id: payload.canvasId, - session_id: payload.sessionId, - system_prompt: payload.systemPrompt, - }), - }) - const data = await response.json() - return data as Message[] + console.log('[API Magic] 开始发送Magic Generation请求:', { + sessionId: payload.sessionId, + canvasId: payload.canvasId, + messagesCount: payload.newMessages.length, + systemPrompt: payload.systemPrompt ? 'present' : 'null', + templateId: payload.templateId + }); + + const requestBody = { + messages: payload.newMessages, + canvas_id: payload.canvasId, + session_id: payload.sessionId, + system_prompt: payload.systemPrompt, + template_id: payload.templateId?.toString() || '', + }; + + console.log('[API Magic] 请求体:', { + ...requestBody, + messages: requestBody.messages.map(msg => ({ + role: msg.role, + contentType: typeof msg.content, + contentLength: Array.isArray(msg.content) ? msg.content.length : (msg.content as string).length + })) + }); + + try { + console.log('[API Magic] 发送fetch请求到 /api/magic...'); + const response = await fetch(`/api/magic`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + console.log('[API Magic] 收到响应:', { + status: response.status, + statusText: response.statusText, + ok: response.ok, + headers: Object.fromEntries(response.headers.entries()) + }); + + if (!response.ok) { + const errorText = await response.text() + console.error('[API Magic] 请求失败,错误响应:', errorText); + throw new Error(`Magic generation failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + const data = await response.json() + console.log('[API Magic] 请求成功,响应数据:', data); + + // 处理防重复机制的响应 + if (data.status === 'already_processing') { + console.warn('[API Magic] 检测到重复请求,正在处理中'); + throw new Error('正在生成中,请稍候...'); + } + + if (data.status === 'rate_limited') { + console.warn('[API Magic] 请求频率过高'); + throw new Error('请求过于频繁,请稍后再试'); + } + + return data as Message[] + } catch (error) { + console.error('[API Magic] 请求过程中发生错误:', error); + if (error instanceof TypeError && error.message.includes('fetch')) { + console.error('[API Magic] 网络错误 - 可能是CORS或连接问题'); + } + throw error; + } } export const cancelMagicGenerate = async (sessionId: string) => { diff --git a/react/src/api/model.ts b/react/src/api/model.ts index aabb52aca..486530bea 100644 --- a/react/src/api/model.ts +++ b/react/src/api/model.ts @@ -16,21 +16,37 @@ export async function listModels(): Promise<{ llm: ModelInfo[] tools: ToolInfo[] }> { + // 🔧 改进错误处理:网络错误时抛出异常而不是返回空数组 + // 这样React Query可以保持previous data,避免误触发登录弹窗 + const modelsResp = await fetch('/api/list_models') - .then((res) => res.json()) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`) + } + return res.json() + }) .catch((err) => { - console.error(err) - return [] + console.error('🔥 Failed to fetch models:', err) + // 🚨 抛出错误而不是返回空数组,让React Query使用placeholderData + throw new Error(`Failed to fetch models: ${err.message}`) }) + const toolsResp = await fetch('/api/list_tools') - .then((res) => res.json()) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`) + } + return res.json() + }) .catch((err) => { - console.error(err) - return [] + console.error('🔥 Failed to fetch tools:', err) + // 🚨 抛出错误而不是返回空数组,让React Query使用placeholderData + throw new Error(`Failed to fetch tools: ${err.message}`) }) return { - llm: modelsResp, - tools: toolsResp, + llm: modelsResp || [], + tools: toolsResp || [], } } diff --git a/react/src/api/templates.ts b/react/src/api/templates.ts new file mode 100644 index 000000000..f29def12b --- /dev/null +++ b/react/src/api/templates.ts @@ -0,0 +1,78 @@ +export interface Template { + id: number + title: string + description: string + image: string + tags: string[] + downloads: number + rating: number + category: string + created_at?: string + updated_at?: string + prompt?: string +} + +export interface TemplateListResponse { + templates: Template[] + total: number + page: number + limit: number +} + +export interface TemplateSearchParams { + search?: string + page?: number + limit?: number + category?: string + sort_by?: 'downloads' | 'rating' | 'created_at' + sort_order?: 'asc' | 'desc' +} + +export async function getTemplates(params?: TemplateSearchParams): Promise { + const searchParams = new URLSearchParams() + + if (params?.search) { + searchParams.append('search', params.search) + } + if (params?.page) { + searchParams.append('page', params.page.toString()) + } + if (params?.limit) { + searchParams.append('limit', params.limit.toString()) + } + if (params?.category) { + searchParams.append('category', params.category) + } + if (params?.sort_by) { + searchParams.append('sort_by', params.sort_by) + } + if (params?.sort_order) { + searchParams.append('sort_order', params.sort_order) + } + + const response = await fetch(`/api/templates?${searchParams.toString()}`) + + if (!response.ok) { + throw new Error(`Failed to fetch templates: ${response.statusText}`) + } + + return await response.json() +} + +export async function getTemplate(id: number): Promise