From 53ddb7e585902b7b5ceae59f2b2e34abd87eadf4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 17 Dec 2025 20:15:18 +0800 Subject: [PATCH] Initial commit - clean repository --- .cursor/rules/ultracite.mdc | 333 + .env.example | 40 + .github/workflows/lint.yml | 25 + .github/workflows/playwright.yml | 73 + .gitignore | 48 + .vscode/extensions.json | 3 + .vscode/settings.json | 35 + 3001端口检查报告.md | 160 + AI_MODEL_CONFIG_GUIDE.md | 262 + AI供应商配置指南.md | 421 + FRPC修复完成报告.md | 142 + FRPC穿透配置说明.md | 153 + FRPC访问问题说明.md | 170 + Google Antigravity Auth Success.html | 62 + LICENSE | 13 + README.md | 71 + app/(admin)/layout.tsx | 34 + .../settings/admin-settings-client.tsx | 2293 +++++ app/(admin)/settings/page.tsx | 17 + app/(auth)/actions.ts | 110 + app/(auth)/api/auth/[...nextauth]/route.ts | 18 + app/(auth)/api/auth/guest/route.ts | 41 + app/(auth)/auth.config.ts | 49 + app/(auth)/auth.ts | 165 + app/(auth)/login/page.tsx | 452 + app/(auth)/register/page.tsx | 104 + app/(chat)/actions.ts | 58 + app/(chat)/api/chat/[id]/stream/route.ts | 113 + .../api/chat/replace-video-url/route.ts | 36 + app/(chat)/api/chat/route.ts | 1468 +++ app/(chat)/api/chat/schema.ts | 44 + app/(chat)/api/detect-object/route.ts | 3 + app/(chat)/api/document/route.ts | 126 + app/(chat)/api/files/upload/route.ts | 93 + app/(chat)/api/history/route.ts | 46 + app/(chat)/api/models/route.ts | 45 + app/(chat)/api/suggestions/route.ts | 37 + app/(chat)/api/vote/route.ts | 75 + app/(chat)/chat/[id]/page.tsx | 82 + app/(chat)/gallery/page.tsx | 347 + app/(chat)/layout.tsx | 56 + app/(chat)/opengraph-image.png | Bin 0 -> 165578 bytes app/(chat)/page.tsx | 56 + app/(chat)/twitter-image.png | Bin 0 -> 52049 bytes app/api/admin/ai-models/route.ts | 149 + app/api/admin/ai-providers/route.ts | 145 + app/api/admin/images/analytics/route.ts | 185 + app/api/admin/images/route.ts | 111 + app/api/admin/settings/route.ts | 61 + app/api/admin/storage/route.ts | 153 + app/api/admin/tools/route.ts | 66 + app/api/admin/users/route.ts | 92 + app/api/admin/volcano-video-models/route.ts | 205 + app/api/auth-settings/route.ts | 115 + app/api/auth/dingtalk/callback/route.ts | 239 + app/api/auth/dingtalk/login/route.ts | 29 + app/api/auth/logout/route.ts | 47 + app/api/detect-object/route.ts | 34 + app/api/gallery/route.ts | 76 + app/api/jobs/archive-videos/route.ts | 112 + app/api/nav-links/route.ts | 161 + app/api/site-settings/route.ts | 127 + app/api/starter-questions/route.ts | 95 + app/api/user/image-stats/route.ts | 40 + app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 328 + app/layout.tsx | 134 + artifacts/actions.ts | 8 + artifacts/code/client.tsx | 280 + artifacts/code/server.ts | 82 + artifacts/image/client.tsx | 76 + artifacts/sheet/client.tsx | 115 + artifacts/sheet/server.ts | 88 + artifacts/text/client.tsx | 179 + artifacts/text/server.ts | 80 + ...e-c41963c8-97f7-4e64-91cc-064405de2b27.png | 0 ...e-d9b2bf76-241a-4541-8999-044e832c2418.png | 0 auto-deploy.sh | 296 + auto_deploy.exp | 39 + backend-florence/app/__init__.py | 2 + backend-florence/app/main.py | 78 + backend-florence/app/services/__init__.py | 2 + .../app/services/florence_service.py | 613 ++ backend-florence/app/services/qwen_service.py | 297 + backend-florence/ecosystem.config.js | 23 + backend-florence/requirements.txt | 18 + biome.jsonc | 51 + check-gpu.sh | 102 + check_server.exp | 19 + check_status.exp | 38 + complete-deploy.sh | 93 + components.json | 20 + components/antigravity-background.tsx | 254 + components/app-sidebar.tsx | 136 + components/artifact-actions.tsx | 107 + components/artifact-close-button.tsx | 30 + components/artifact-messages.tsx | 110 + components/artifact.tsx | 530 + components/auth-form.tsx | 62 + components/chat-header.tsx | 61 + components/chat.tsx | 487 + components/code-editor.tsx | 121 + components/console.tsx | 193 + components/create-artifact.tsx | 93 + components/data-stream-handler.tsx | 83 + components/data-stream-provider.tsx | 41 + components/diffview.tsx | 100 + components/document-preview.tsx | 295 + components/document-skeleton.tsx | 39 + components/document.tsx | 161 + components/elements/actions.tsx | 65 + components/elements/branch.tsx | 215 + components/elements/code-block.tsx | 154 + components/elements/context.tsx | 190 + components/elements/conversation.tsx | 65 + components/elements/image.tsx | 26 + components/elements/inline-citation.tsx | 287 + components/elements/loader.tsx | 96 + components/elements/message.tsx | 58 + components/elements/prompt-input.tsx | 240 + components/elements/reasoning.tsx | 171 + components/elements/response.tsx | 41 + components/elements/source.tsx | 74 + components/elements/suggestion.tsx | 53 + components/elements/task.tsx | 94 + components/elements/tool.tsx | 147 + components/elements/web-preview.tsx | 252 + components/gemini-image-loading.tsx | 137 + components/greeting.tsx | 74 + components/icons.tsx | 1213 +++ components/image-editor.tsx | 49 + components/image-params-bar.tsx | 141 + components/image-preview.tsx | 365 + components/message-actions.tsx | 218 + components/message-editor.tsx | 146 + components/message-reasoning.tsx | 37 + components/message.tsx | 481 + components/messages.tsx | 271 + components/model-selector.tsx | 229 + components/multimodal-input.tsx | 1127 +++ components/preview-attachment.tsx | 346 + components/qianwen-video-loading.tsx | 137 + components/sheet-editor.tsx | 140 + components/sidebar-history-item.tsx | 120 + components/sidebar-history.tsx | 368 + components/sidebar-toggle.tsx | 35 + components/sidebar-user-nav.tsx | 130 + components/sign-out-form.tsx | 25 + components/submit-button.tsx | 38 + components/suggested-actions.tsx | 81 + components/suggestion.tsx | 77 + components/text-editor.tsx | 164 + components/theme-provider.tsx | 8 + components/toast.tsx | 75 + components/toolbar.tsx | 476 + components/top-navbar.tsx | 138 + components/ui/alert-dialog.tsx | 141 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/button.tsx | 56 + components/ui/card.tsx | 79 + components/ui/carousel.tsx | 262 + components/ui/collapsible.tsx | 11 + components/ui/dialog.tsx | 128 + components/ui/dropdown-menu.tsx | 200 + components/ui/hover-card.tsx | 29 + components/ui/input.tsx | 22 + components/ui/label.tsx | 26 + components/ui/progress.tsx | 28 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/sidebar.tsx | 772 ++ components/ui/skeleton.tsx | 15 + components/ui/switch.tsx | 31 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 22 + components/ui/tooltip.tsx | 30 + components/version-footer.tsx | 107 + components/video-params-bar.tsx | 612 ++ components/video-preview.tsx | 96 + components/visibility-selector.tsx | 110 + components/volcano-vision-loading.tsx | 143 + components/weather.tsx | 443 + create-admin.ts | 60 + deploy-fix.sh | 170 + deploy.sh | 100 + deploy_flux.sh | 23 + download_flux.py | 25 + download_model.py | 22 + dreamomni2-deploy-guide.md | 278 + drizzle.config.ts | 16 + ecosystem.config.js | 25 + final-deploy.sh | 129 + finish_setup.exp | 37 + fix-deploy.sh | 89 + fix-frpc-access.sh | 119 + fix-redirect-issue.sh | 76 + hooks/use-artifact.ts | 89 + hooks/use-auto-resume.ts | 53 + hooks/use-chat-visibility.ts | 53 + hooks/use-messages.tsx | 37 + hooks/use-mobile.ts | 19 + hooks/use-scroll-to-bottom.tsx | 127 + instrumentation.ts | 5 + lib/ai/classifier.ts | 77 + lib/ai/dynamic-provider.ts | 289 + lib/ai/entitlements.ts | 50 + lib/ai/models.mock.ts | 38 + lib/ai/models.test.ts | 84 + lib/ai/models.ts | 49 + lib/ai/prompts.ts | 121 + lib/ai/providers.ts | 54 + lib/ai/providers/gemini-image.ts | 665 ++ lib/ai/providers/local-qwen.ts | 233 + lib/ai/providers/qianwen-video.ts | 423 + lib/ai/providers/volcano-video.ts | 517 + lib/ai/providers/volcano-vision.ts | 656 ++ lib/ai/tools-registry.ts | 50 + lib/ai/tools/create-document.ts | 76 + lib/ai/tools/generate-video.ts | 360 + lib/ai/tools/get-weather.ts | 78 + lib/ai/tools/request-suggestions.ts | 97 + lib/ai/tools/update-document.ts | 62 + lib/ai/volcano-video-models.ts | 202 + lib/artifacts/server.ts | 98 + lib/constants.ts | 13 + lib/db/helpers/01-core-to-parts.ts | 253 + lib/db/index.ts | 7 + lib/db/migrate.ts | 32 + lib/db/migrations/0000_keen_devos.sql | 18 + .../migrations/0001_sparkling_blue_marvel.sql | 39 + lib/db/migrations/0002_wandering_riptide.sql | 35 + lib/db/migrations/0003_cloudy_glorian.sql | 1 + lib/db/migrations/0004_odd_slayback.sql | 1 + lib/db/migrations/0005_wooden_whistler.sql | 33 + .../migrations/0006_marvelous_frog_thor.sql | 12 + lib/db/migrations/0007_flowery_ben_parker.sql | 1 + lib/db/migrations/0008_silent_harrier.sql | 126 + lib/db/migrations/0009_sleepy_callisto.sql | 4 + .../0010_add_qianwen_video_provider.sql | 3 + .../migrations/0010_eminent_sinister_six.sql | 56 + .../0011_add_site_settings_and_nav_links.sql | 25 + lib/db/migrations/0011_add_user_name.sql | 3 + lib/db/migrations/0011_tiresome_freak.sql | 1 + lib/db/migrations/0012_add_auth_settings.sql | 10 + lib/db/migrations/0013_add_login_token.sql | 8 + lib/db/migrations/0014_add_site_logo_dark.sql | 2 + .../migrations/0016_add_show_auto_model.sql | 3 + .../0017_add_volcano_vision_provider.sql | 7 + .../0018_add_volcano_video_support.sql | 94 + .../migrations/0019_add_sub_model_support.sql | 23 + .../migrations/0020_add_usage_time_limit.sql | 6 + .../migrations/0021_add_generated_image.sql | 19 + lib/db/migrations/meta/0000_snapshot.json | 90 + lib/db/migrations/meta/0001_snapshot.json | 236 + lib/db/migrations/meta/0002_snapshot.json | 339 + lib/db/migrations/meta/0003_snapshot.json | 346 + lib/db/migrations/meta/0004_snapshot.json | 353 + lib/db/migrations/meta/0005_snapshot.json | 462 + lib/db/migrations/meta/0006_snapshot.json | 506 + lib/db/migrations/meta/0007_snapshot.json | 512 + lib/db/migrations/meta/0008_snapshot.json | 1041 ++ lib/db/migrations/meta/0009_snapshot.json | 1041 ++ lib/db/migrations/meta/0010_snapshot.json | 1300 +++ lib/db/migrations/meta/0011_snapshot.json | 1307 +++ lib/db/migrations/meta/_journal.json | 90 + lib/db/queries.ts | 1496 +++ lib/db/schema.ts | 346 + lib/db/utils.ts | 16 + lib/editor/config.ts | 49 + lib/editor/diff.js | 475 + lib/editor/functions.tsx | 62 + lib/editor/react-renderer.tsx | 13 + lib/editor/suggestions.tsx | 158 + lib/errors.ts | 151 + lib/jobs/video-archive-job.ts | 151 + lib/middleware/admin.ts | 17 + lib/storage/service.ts | 198 + lib/storage/video-archiver.ts | 189 + lib/types.ts | 58 + lib/usage.ts | 5 + lib/utils.ts | 116 + next-env.d.ts | 6 + next.config.ts | 33 + nginx-florence-config.txt | 101 + one_click_deploy.sh | 38 + package.json | 126 + playwright.config.ts | 107 + pnpm-lock.yaml | 8836 +++++++++++++++++ postcss.config.mjs | 8 + proxy.ts | 51 + public/dingtalk-callback.html | 224 + public/images/demo-thumbnail.png | Bin 0 -> 23198 bytes public/images/mouth of the seine, monet.jpg | Bin 0 -> 33497 bytes remote_deploy_dreamomni2.sh | 100 + remote_setup.sh | 106 + run_test.exp | 62 + run_test_fixed.exp | 49 + run_test_offline.exp | 50 + scripts/check_users.ts | 17 + setup-server.sh | 82 + tests/e2e/artifacts.test.ts | 77 + tests/e2e/chat.test.ts | 172 + tests/e2e/reasoning.test.ts | 63 + tests/e2e/session.test.ts | 209 + tests/fixtures.ts | 50 + tests/helpers.ts | 79 + tests/pages/artifact.ts | 120 + tests/pages/auth.ts | 69 + tests/pages/chat.ts | 265 + tests/prompts/basic.ts | 148 + tests/prompts/routes.ts | 46 + tests/prompts/utils.ts | 282 + tests/routes/chat.test.ts | 366 + tests/routes/document.test.ts | 211 + tsconfig.json | 35 + types/ai.ts | 244 + upload_and_deploy.exp | 47 + upload_flux.exp | 22 + vercel-template.json | 19 + vercel.json | 9 + 修复重定向问题说明.md | 100 + 功能发布通知.md | 418 + 功能更新-2025-12-11.md | 26 + 千问本地模型API调用文档.md | 277 + 图片标注-前后端数据流测试.md | 335 + 图片标注功能-部署完成总结.md | 524 + 图片物体智能标注系统-实施文档.md | 1240 +++ 开发文档.md | 1331 +++ 快速部署.md | 181 + 日志 | 99 + 最终部署报告.md | 146 + 本地千问模型集成指南.md | 230 + 火山引擎视频API开发文档 | 1920 ++++ 火山引擎视频主子模型验证文档.md | 311 + 火山引擎视频服务-缺失功能检查报告.md | 427 + 火山引擎视频服务-阶段一实施完成报告.md | 422 + 火山引擎视频服务开发计划.md | 2735 +++++ 火山引擎视频模型配置流程.md | 156 + 物体标注功能-集成步骤.md | 302 + 访问测试结果.md | 88 + 部署完成总结.md | 145 + 部署指南.md | 406 + 部署状态.md | 75 + 部署状态总结.md | 324 + 钉钉配置文档 | 700 ++ 问题修复说明.md | 66 + 349 files changed, 72444 insertions(+) create mode 100644 .cursor/rules/ultracite.mdc create mode 100644 .env.example create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/playwright.yml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 3001端口检查报告.md create mode 100644 AI_MODEL_CONFIG_GUIDE.md create mode 100644 AI供应商配置指南.md create mode 100644 FRPC修复完成报告.md create mode 100644 FRPC穿透配置说明.md create mode 100644 FRPC访问问题说明.md create mode 100644 Google Antigravity Auth Success.html create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/(admin)/layout.tsx create mode 100644 app/(admin)/settings/admin-settings-client.tsx create mode 100644 app/(admin)/settings/page.tsx create mode 100644 app/(auth)/actions.ts create mode 100644 app/(auth)/api/auth/[...nextauth]/route.ts create mode 100644 app/(auth)/api/auth/guest/route.ts create mode 100644 app/(auth)/auth.config.ts create mode 100644 app/(auth)/auth.ts create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(auth)/register/page.tsx create mode 100644 app/(chat)/actions.ts create mode 100644 app/(chat)/api/chat/[id]/stream/route.ts create mode 100644 app/(chat)/api/chat/replace-video-url/route.ts create mode 100644 app/(chat)/api/chat/route.ts create mode 100644 app/(chat)/api/chat/schema.ts create mode 100644 app/(chat)/api/detect-object/route.ts create mode 100644 app/(chat)/api/document/route.ts create mode 100644 app/(chat)/api/files/upload/route.ts create mode 100644 app/(chat)/api/history/route.ts create mode 100644 app/(chat)/api/models/route.ts create mode 100644 app/(chat)/api/suggestions/route.ts create mode 100644 app/(chat)/api/vote/route.ts create mode 100644 app/(chat)/chat/[id]/page.tsx create mode 100644 app/(chat)/gallery/page.tsx create mode 100644 app/(chat)/layout.tsx create mode 100644 app/(chat)/opengraph-image.png create mode 100644 app/(chat)/page.tsx create mode 100644 app/(chat)/twitter-image.png create mode 100644 app/api/admin/ai-models/route.ts create mode 100644 app/api/admin/ai-providers/route.ts create mode 100644 app/api/admin/images/analytics/route.ts create mode 100644 app/api/admin/images/route.ts create mode 100644 app/api/admin/settings/route.ts create mode 100644 app/api/admin/storage/route.ts create mode 100644 app/api/admin/tools/route.ts create mode 100644 app/api/admin/users/route.ts create mode 100644 app/api/admin/volcano-video-models/route.ts create mode 100644 app/api/auth-settings/route.ts create mode 100644 app/api/auth/dingtalk/callback/route.ts create mode 100644 app/api/auth/dingtalk/login/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/detect-object/route.ts create mode 100644 app/api/gallery/route.ts create mode 100644 app/api/jobs/archive-videos/route.ts create mode 100644 app/api/nav-links/route.ts create mode 100644 app/api/site-settings/route.ts create mode 100644 app/api/starter-questions/route.ts create mode 100644 app/api/user/image-stats/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 artifacts/actions.ts create mode 100644 artifacts/code/client.tsx create mode 100644 artifacts/code/server.ts create mode 100644 artifacts/image/client.tsx create mode 100644 artifacts/sheet/client.tsx create mode 100644 artifacts/sheet/server.ts create mode 100644 artifacts/text/client.tsx create mode 100644 artifacts/text/server.ts create mode 100644 assets/image-c41963c8-97f7-4e64-91cc-064405de2b27.png create mode 100644 assets/image-d9b2bf76-241a-4541-8999-044e832c2418.png create mode 100755 auto-deploy.sh create mode 100755 auto_deploy.exp create mode 100644 backend-florence/app/__init__.py create mode 100644 backend-florence/app/main.py create mode 100644 backend-florence/app/services/__init__.py create mode 100644 backend-florence/app/services/florence_service.py create mode 100644 backend-florence/app/services/qwen_service.py create mode 100644 backend-florence/ecosystem.config.js create mode 100644 backend-florence/requirements.txt create mode 100644 biome.jsonc create mode 100644 check-gpu.sh create mode 100644 check_server.exp create mode 100755 check_status.exp create mode 100755 complete-deploy.sh create mode 100644 components.json create mode 100644 components/antigravity-background.tsx create mode 100644 components/app-sidebar.tsx create mode 100644 components/artifact-actions.tsx create mode 100644 components/artifact-close-button.tsx create mode 100644 components/artifact-messages.tsx create mode 100644 components/artifact.tsx create mode 100644 components/auth-form.tsx create mode 100644 components/chat-header.tsx create mode 100644 components/chat.tsx create mode 100644 components/code-editor.tsx create mode 100644 components/console.tsx create mode 100644 components/create-artifact.tsx create mode 100644 components/data-stream-handler.tsx create mode 100644 components/data-stream-provider.tsx create mode 100644 components/diffview.tsx create mode 100644 components/document-preview.tsx create mode 100644 components/document-skeleton.tsx create mode 100644 components/document.tsx create mode 100644 components/elements/actions.tsx create mode 100644 components/elements/branch.tsx create mode 100644 components/elements/code-block.tsx create mode 100644 components/elements/context.tsx create mode 100644 components/elements/conversation.tsx create mode 100644 components/elements/image.tsx create mode 100644 components/elements/inline-citation.tsx create mode 100644 components/elements/loader.tsx create mode 100644 components/elements/message.tsx create mode 100644 components/elements/prompt-input.tsx create mode 100644 components/elements/reasoning.tsx create mode 100644 components/elements/response.tsx create mode 100644 components/elements/source.tsx create mode 100644 components/elements/suggestion.tsx create mode 100644 components/elements/task.tsx create mode 100644 components/elements/tool.tsx create mode 100644 components/elements/web-preview.tsx create mode 100644 components/gemini-image-loading.tsx create mode 100644 components/greeting.tsx create mode 100644 components/icons.tsx create mode 100644 components/image-editor.tsx create mode 100644 components/image-params-bar.tsx create mode 100644 components/image-preview.tsx create mode 100644 components/message-actions.tsx create mode 100644 components/message-editor.tsx create mode 100644 components/message-reasoning.tsx create mode 100644 components/message.tsx create mode 100644 components/messages.tsx create mode 100644 components/model-selector.tsx create mode 100644 components/multimodal-input.tsx create mode 100644 components/preview-attachment.tsx create mode 100644 components/qianwen-video-loading.tsx create mode 100644 components/sheet-editor.tsx create mode 100644 components/sidebar-history-item.tsx create mode 100644 components/sidebar-history.tsx create mode 100644 components/sidebar-toggle.tsx create mode 100644 components/sidebar-user-nav.tsx create mode 100644 components/sign-out-form.tsx create mode 100644 components/submit-button.tsx create mode 100644 components/suggested-actions.tsx create mode 100644 components/suggestion.tsx create mode 100644 components/text-editor.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/toast.tsx create mode 100644 components/toolbar.tsx create mode 100644 components/top-navbar.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/version-footer.tsx create mode 100644 components/video-params-bar.tsx create mode 100644 components/video-preview.tsx create mode 100644 components/visibility-selector.tsx create mode 100644 components/volcano-vision-loading.tsx create mode 100644 components/weather.tsx create mode 100644 create-admin.ts create mode 100755 deploy-fix.sh create mode 100755 deploy.sh create mode 100755 deploy_flux.sh create mode 100644 download_flux.py create mode 100644 download_model.py create mode 100644 dreamomni2-deploy-guide.md create mode 100644 drizzle.config.ts create mode 100644 ecosystem.config.js create mode 100755 final-deploy.sh create mode 100755 finish_setup.exp create mode 100755 fix-deploy.sh create mode 100755 fix-frpc-access.sh create mode 100755 fix-redirect-issue.sh create mode 100644 hooks/use-artifact.ts create mode 100644 hooks/use-auto-resume.ts create mode 100644 hooks/use-chat-visibility.ts create mode 100644 hooks/use-messages.tsx create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-scroll-to-bottom.tsx create mode 100644 instrumentation.ts create mode 100644 lib/ai/classifier.ts create mode 100644 lib/ai/dynamic-provider.ts create mode 100644 lib/ai/entitlements.ts create mode 100644 lib/ai/models.mock.ts create mode 100644 lib/ai/models.test.ts create mode 100644 lib/ai/models.ts create mode 100644 lib/ai/prompts.ts create mode 100644 lib/ai/providers.ts create mode 100644 lib/ai/providers/gemini-image.ts create mode 100644 lib/ai/providers/local-qwen.ts create mode 100644 lib/ai/providers/qianwen-video.ts create mode 100644 lib/ai/providers/volcano-video.ts create mode 100644 lib/ai/providers/volcano-vision.ts create mode 100644 lib/ai/tools-registry.ts create mode 100644 lib/ai/tools/create-document.ts create mode 100644 lib/ai/tools/generate-video.ts create mode 100644 lib/ai/tools/get-weather.ts create mode 100644 lib/ai/tools/request-suggestions.ts create mode 100644 lib/ai/tools/update-document.ts create mode 100644 lib/ai/volcano-video-models.ts create mode 100644 lib/artifacts/server.ts create mode 100644 lib/constants.ts create mode 100644 lib/db/helpers/01-core-to-parts.ts create mode 100644 lib/db/index.ts create mode 100644 lib/db/migrate.ts create mode 100644 lib/db/migrations/0000_keen_devos.sql create mode 100644 lib/db/migrations/0001_sparkling_blue_marvel.sql create mode 100644 lib/db/migrations/0002_wandering_riptide.sql create mode 100644 lib/db/migrations/0003_cloudy_glorian.sql create mode 100644 lib/db/migrations/0004_odd_slayback.sql create mode 100644 lib/db/migrations/0005_wooden_whistler.sql create mode 100644 lib/db/migrations/0006_marvelous_frog_thor.sql create mode 100644 lib/db/migrations/0007_flowery_ben_parker.sql create mode 100644 lib/db/migrations/0008_silent_harrier.sql create mode 100644 lib/db/migrations/0009_sleepy_callisto.sql create mode 100644 lib/db/migrations/0010_add_qianwen_video_provider.sql create mode 100644 lib/db/migrations/0010_eminent_sinister_six.sql create mode 100644 lib/db/migrations/0011_add_site_settings_and_nav_links.sql create mode 100644 lib/db/migrations/0011_add_user_name.sql create mode 100644 lib/db/migrations/0011_tiresome_freak.sql create mode 100644 lib/db/migrations/0012_add_auth_settings.sql create mode 100644 lib/db/migrations/0013_add_login_token.sql create mode 100644 lib/db/migrations/0014_add_site_logo_dark.sql create mode 100644 lib/db/migrations/0016_add_show_auto_model.sql create mode 100644 lib/db/migrations/0017_add_volcano_vision_provider.sql create mode 100644 lib/db/migrations/0018_add_volcano_video_support.sql create mode 100644 lib/db/migrations/0019_add_sub_model_support.sql create mode 100644 lib/db/migrations/0020_add_usage_time_limit.sql create mode 100644 lib/db/migrations/0021_add_generated_image.sql create mode 100644 lib/db/migrations/meta/0000_snapshot.json create mode 100644 lib/db/migrations/meta/0001_snapshot.json create mode 100644 lib/db/migrations/meta/0002_snapshot.json create mode 100644 lib/db/migrations/meta/0003_snapshot.json create mode 100644 lib/db/migrations/meta/0004_snapshot.json create mode 100644 lib/db/migrations/meta/0005_snapshot.json create mode 100644 lib/db/migrations/meta/0006_snapshot.json create mode 100644 lib/db/migrations/meta/0007_snapshot.json create mode 100644 lib/db/migrations/meta/0008_snapshot.json create mode 100644 lib/db/migrations/meta/0009_snapshot.json create mode 100644 lib/db/migrations/meta/0010_snapshot.json create mode 100644 lib/db/migrations/meta/0011_snapshot.json create mode 100644 lib/db/migrations/meta/_journal.json create mode 100644 lib/db/queries.ts create mode 100644 lib/db/schema.ts create mode 100644 lib/db/utils.ts create mode 100644 lib/editor/config.ts create mode 100644 lib/editor/diff.js create mode 100644 lib/editor/functions.tsx create mode 100644 lib/editor/react-renderer.tsx create mode 100644 lib/editor/suggestions.tsx create mode 100644 lib/errors.ts create mode 100644 lib/jobs/video-archive-job.ts create mode 100644 lib/middleware/admin.ts create mode 100644 lib/storage/service.ts create mode 100644 lib/storage/video-archiver.ts create mode 100644 lib/types.ts create mode 100644 lib/usage.ts create mode 100644 lib/utils.ts create mode 100644 next-env.d.ts create mode 100644 next.config.ts create mode 100644 nginx-florence-config.txt create mode 100755 one_click_deploy.sh create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 proxy.ts create mode 100644 public/dingtalk-callback.html create mode 100644 public/images/demo-thumbnail.png create mode 100644 public/images/mouth of the seine, monet.jpg create mode 100755 remote_deploy_dreamomni2.sh create mode 100755 remote_setup.sh create mode 100755 run_test.exp create mode 100755 run_test_fixed.exp create mode 100755 run_test_offline.exp create mode 100644 scripts/check_users.ts create mode 100755 setup-server.sh create mode 100644 tests/e2e/artifacts.test.ts create mode 100644 tests/e2e/chat.test.ts create mode 100644 tests/e2e/reasoning.test.ts create mode 100644 tests/e2e/session.test.ts create mode 100644 tests/fixtures.ts create mode 100644 tests/helpers.ts create mode 100644 tests/pages/artifact.ts create mode 100644 tests/pages/auth.ts create mode 100644 tests/pages/chat.ts create mode 100644 tests/prompts/basic.ts create mode 100644 tests/prompts/routes.ts create mode 100644 tests/prompts/utils.ts create mode 100644 tests/routes/chat.test.ts create mode 100644 tests/routes/document.test.ts create mode 100644 tsconfig.json create mode 100644 types/ai.ts create mode 100755 upload_and_deploy.exp create mode 100644 upload_flux.exp create mode 100644 vercel-template.json create mode 100644 vercel.json create mode 100644 修复重定向问题说明.md create mode 100644 功能发布通知.md create mode 100644 功能更新-2025-12-11.md create mode 100644 千问本地模型API调用文档.md create mode 100644 图片标注-前后端数据流测试.md create mode 100644 图片标注功能-部署完成总结.md create mode 100644 图片物体智能标注系统-实施文档.md create mode 100644 开发文档.md create mode 100644 快速部署.md create mode 100644 日志 create mode 100644 最终部署报告.md create mode 100644 本地千问模型集成指南.md create mode 100644 火山引擎视频API开发文档 create mode 100644 火山引擎视频主子模型验证文档.md create mode 100644 火山引擎视频服务-缺失功能检查报告.md create mode 100644 火山引擎视频服务-阶段一实施完成报告.md create mode 100644 火山引擎视频服务开发计划.md create mode 100644 火山引擎视频模型配置流程.md create mode 100644 物体标注功能-集成步骤.md create mode 100644 访问测试结果.md create mode 100644 部署完成总结.md create mode 100644 部署指南.md create mode 100644 部署状态.md create mode 100644 部署状态总结.md create mode 100644 钉钉配置文档 create mode 100644 问题修复说明.md diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc new file mode 100644 index 000000000..f2da237c4 --- /dev/null +++ b/.cursor/rules/ultracite.mdc @@ -0,0 +1,333 @@ +--- +description: Ultracite Rules - AI-Ready Formatter and Linter +globs: "**/*.{ts,tsx,js,jsx}" +alwaysApply: true +--- + +# Project Context +Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. + +## Key Principles +- Zero configuration required +- Subsecond performance +- Maximum type safety +- AI-friendly code generation + +## Before Writing Code +1. Analyze existing patterns in the codebase +2. Consider edge cases and error scenarios +3. Follow the rules below strictly +4. Validate accessibility requirements + +## Rules + +### Accessibility (a11y) +- Don't use `accessKey` attribute on any HTML element. +- Don't set `aria-hidden="true"` on focusable elements. +- Don't add ARIA roles, states, and properties to elements that don't support them. +- Don't use distracting elements like `` or ``. +- Only use the `scope` prop on `` elements. +- Don't assign non-interactive ARIA roles to interactive HTML elements. +- Make sure label elements have text content and are associated with an input. +- Don't assign interactive ARIA roles to non-interactive HTML elements. +- Don't assign `tabIndex` to non-interactive HTML elements. +- Don't use positive integers for `tabIndex` property. +- Don't include "image", "picture", or "photo" in img alt prop. +- Don't use explicit role property that's the same as the implicit/default role. +- Make static elements with click handlers use a valid role attribute. +- Always include a `title` element for SVG elements. +- Give all elements requiring alt text meaningful information for screen readers. +- Make sure anchors have content that's accessible to screen readers. +- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. +- Include all required ARIA attributes for elements with ARIA roles. +- Make sure ARIA properties are valid for the element's supported roles. +- Always include a `type` attribute for button elements. +- Make elements with interactive roles and handlers focusable. +- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). +- Always include a `lang` attribute on the html element. +- Always include a `title` attribute for iframe elements. +- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. +- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. +- Include caption tracks for audio and video elements. +- Use semantic elements instead of role attributes in JSX. +- Make sure all anchors are valid and navigable. +- Ensure all ARIA properties (`aria-*`) are valid. +- Use valid, non-abstract ARIA roles for elements with ARIA roles. +- Use valid ARIA state and property values. +- Use valid values for the `autocomplete` attribute on input elements. +- Use correct ISO language/country codes for the `lang` attribute. + +### Code Complexity and Quality +- Don't use consecutive spaces in regular expression literals. +- Don't use the `arguments` object. +- Don't use primitive type aliases or misleading types. +- Don't use the comma operator. +- Don't use empty type parameters in type aliases and interfaces. +- Don't write functions that exceed a given Cognitive Complexity score. +- Don't nest describe() blocks too deeply in test files. +- Don't use unnecessary boolean casts. +- Don't use unnecessary callbacks with flatMap. +- Use for...of statements instead of Array.forEach. +- Don't create classes that only have static members (like a static namespace). +- Don't use this and super in static contexts. +- Don't use unnecessary catch clauses. +- Don't use unnecessary constructors. +- Don't use unnecessary continue statements. +- Don't export empty modules that don't change anything. +- Don't use unnecessary escape sequences in regular expression literals. +- Don't use unnecessary fragments. +- Don't use unnecessary labels. +- Don't use unnecessary nested block statements. +- Don't rename imports, exports, and destructured assignments to the same name. +- Don't use unnecessary string or template literal concatenation. +- Don't use String.raw in template literals when there are no escape sequences. +- Don't use useless case statements in switch statements. +- Don't use ternary operators when simpler alternatives exist. +- Don't use useless `this` aliasing. +- Don't use any or unknown as type constraints. +- Don't initialize variables to undefined. +- Don't use the void operators (they're not familiar). +- Use arrow functions instead of function expressions. +- Use Date.now() to get milliseconds since the Unix Epoch. +- Use .flatMap() instead of map().flat() when possible. +- Use literal property access instead of computed property access. +- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. +- Use concise optional chaining instead of chained logical expressions. +- Use regular expression literals instead of the RegExp constructor when possible. +- Don't use number literal object member names that aren't base 10 or use underscore separators. +- Remove redundant terms from logical expressions. +- Use while loops instead of for loops when you don't need initializer and update expressions. +- Don't pass children as props. +- Don't reassign const variables. +- Don't use constant expressions in conditions. +- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. +- Don't return a value from a constructor. +- Don't use empty character classes in regular expression literals. +- Don't use empty destructuring patterns. +- Don't call global object properties as functions. +- Don't declare functions and vars that are accessible outside their block. +- Make sure builtins are correctly instantiated. +- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. +- Don't use variables and function parameters before they're declared. +- Don't use 8 and 9 escape sequences in string literals. +- Don't use literal numbers that lose precision. + +### React and JSX Best Practices +- Don't use the return value of React.render. +- Make sure all dependencies are correctly specified in React hooks. +- Make sure all React hooks are called from the top level of component functions. +- Don't forget key props in iterators and collection literals. +- Don't destructure props inside JSX components in Solid projects. +- Don't define React components inside other components. +- Don't use event handlers on non-interactive elements. +- Don't assign to React component props. +- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. +- Don't use dangerous JSX props. +- Don't use Array index in keys. +- Don't insert comments as text nodes. +- Don't assign JSX properties multiple times. +- Don't add extra closing tags for components without children. +- Use `<>...` instead of `...`. +- Watch out for possible "wrong" semicolons inside JSX elements. + +### Correctness and Safety +- Don't assign a value to itself. +- Don't return a value from a setter. +- Don't compare expressions that modify string case with non-compliant values. +- Don't use lexical declarations in switch clauses. +- Don't use variables that haven't been declared in the document. +- Don't write unreachable code. +- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. +- Don't use control flow statements in finally blocks. +- Don't use optional chaining where undefined values aren't allowed. +- Don't have unused function parameters. +- Don't have unused imports. +- Don't have unused labels. +- Don't have unused private class members. +- Don't have unused variables. +- Make sure void (self-closing) elements don't have children. +- Don't return a value from a function with the return type 'void' +- Use isNaN() when checking for NaN. +- Make sure "for" loop update clauses move the counter in the right direction. +- Make sure typeof expressions are compared to valid values. +- Make sure generator functions contain yield. +- Don't use await inside loops. +- Don't use bitwise operators. +- Don't use expressions where the operation doesn't change the value. +- Make sure Promise-like statements are handled appropriately. +- Don't use __dirname and __filename in the global scope. +- Prevent import cycles. +- Don't use configured elements. +- Don't hardcode sensitive data like API keys and tokens. +- Don't let variable declarations shadow variables from outer scopes. +- Don't use the TypeScript directive @ts-ignore. +- Prevent duplicate polyfills from Polyfill.io. +- Don't use useless backreferences in regular expressions that always match empty strings. +- Don't use unnecessary escapes in string literals. +- Don't use useless undefined. +- Make sure getters and setters for the same property are next to each other in class and object definitions. +- Make sure object literals are declared consistently (defaults to explicit definitions). +- Use static Response methods instead of new Response() constructor when possible. +- Make sure switch-case statements are exhaustive. +- Make sure the `preconnect` attribute is used when using Google Fonts. +- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. +- Make sure iterable callbacks return consistent values. +- Use `with { type: "json" }` for JSON module imports. +- Use numeric separators in numeric literals. +- Use object spread instead of `Object.assign()` when constructing new objects. +- Always use the radix argument when using `parseInt()`. +- Make sure JSDoc comment lines start with a single asterisk, except for the first one. +- Include a description parameter for `Symbol()`. +- Don't use spread (`...`) syntax on accumulators. +- Don't use the `delete` operator. +- Don't access namespace imports dynamically. +- Don't use namespace imports. +- Declare regex literals at the top level. +- Don't use `target="_blank"` without `rel="noopener"`. + +### TypeScript Best Practices +- Don't use TypeScript enums. +- Don't export imported variables. +- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. +- Don't use TypeScript namespaces. +- Don't use non-null assertions with the `!` postfix operator. +- Don't use parameter properties in class constructors. +- Don't use user-defined types. +- Use `as const` instead of literal types and type annotations. +- Use either `T[]` or `Array` consistently. +- Initialize each enum member value explicitly. +- Use `export type` for types. +- Use `import type` for types. +- Make sure all enum members are literal values. +- Don't use TypeScript const enum. +- Don't declare empty interfaces. +- Don't let variables evolve into any type through reassignments. +- Don't use the any type. +- Don't misuse the non-null assertion operator (!) in TypeScript files. +- Don't use implicit any type on variable declarations. +- Don't merge interfaces and classes unsafely. +- Don't use overload signatures that aren't next to each other. +- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. + +### Style and Consistency +- Don't use global `eval()`. +- Don't use callbacks in asynchronous tests and hooks. +- Don't use negation in `if` statements that have `else` clauses. +- Don't use nested ternary expressions. +- Don't reassign function parameters. +- This rule lets you specify global variable names you don't want to use in your application. +- Don't use specified modules when loaded by import or require. +- Don't use constants whose value is the upper-case version of their name. +- Use `String.slice()` instead of `String.substr()` and `String.substring()`. +- Don't use template literals if you don't need interpolation or special-character handling. +- Don't use `else` blocks when the `if` block breaks early. +- Don't use yoda expressions. +- Don't use Array constructors. +- Use `at()` instead of integer index access. +- Follow curly brace conventions. +- Use `else if` instead of nested `if` statements in `else` clauses. +- Use single `if` statements instead of nested `if` clauses. +- Use `new` for all builtins except `String`, `Number`, and `Boolean`. +- Use consistent accessibility modifiers on class properties and methods. +- Use `const` declarations for variables that are only assigned once. +- Put default function parameters and optional function parameters last. +- Include a `default` clause in switch statements. +- Use the `**` operator instead of `Math.pow`. +- Use `for-of` loops when you need the index to extract an item from the iterated array. +- Use `node:assert/strict` over `node:assert`. +- Use the `node:` protocol for Node.js builtin modules. +- Use Number properties instead of global ones. +- Use assignment operator shorthand where possible. +- Use function types instead of object types with call signatures. +- Use template literals over string concatenation. +- Use `new` when throwing an error. +- Don't throw non-Error values. +- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. +- Use standard constants instead of approximated literals. +- Don't assign values in expressions. +- Don't use async functions as Promise executors. +- Don't reassign exceptions in catch clauses. +- Don't reassign class members. +- Don't compare against -0. +- Don't use labeled statements that aren't loops. +- Don't use void type outside of generic or return types. +- Don't use console. +- Don't use control characters and escape sequences that match control characters in regular expression literals. +- Don't use debugger. +- Don't assign directly to document.cookie. +- Use `===` and `!==`. +- Don't use duplicate case labels. +- Don't use duplicate class members. +- Don't use duplicate conditions in if-else-if chains. +- Don't use two keys with the same name inside objects. +- Don't use duplicate function parameter names. +- Don't have duplicate hooks in describe blocks. +- Don't use empty block statements and static blocks. +- Don't let switch clauses fall through. +- Don't reassign function declarations. +- Don't allow assignments to native objects and read-only global variables. +- Use Number.isFinite instead of global isFinite. +- Use Number.isNaN instead of global isNaN. +- Don't assign to imported bindings. +- Don't use irregular whitespace characters. +- Don't use labels that share a name with a variable. +- Don't use characters made with multiple code points in character class syntax. +- Make sure to use new and constructor properly. +- Don't use shorthand assign when the variable appears on both sides. +- Don't use octal escape sequences in string literals. +- Don't use Object.prototype builtins directly. +- Don't redeclare variables, functions, classes, and types in the same scope. +- Don't have redundant "use strict". +- Don't compare things where both sides are exactly the same. +- Don't let identifiers shadow restricted names. +- Don't use sparse arrays (arrays with holes). +- Don't use template literal placeholder syntax in regular strings. +- Don't use the then property. +- Don't use unsafe negation. +- Don't use var. +- Don't use with statements in non-strict contexts. +- Make sure async functions actually use await. +- Make sure default clauses in switch statements come last. +- Make sure to pass a message value when creating a built-in error. +- Make sure get methods always return a value. +- Use a recommended display strategy with Google Fonts. +- Make sure for-in loops include an if statement. +- Use Array.isArray() instead of instanceof Array. +- Make sure to use the digits argument with Number#toFixed(). +- Make sure to use the "use strict" directive in script files. + +### Next.js Specific Rules +- Don't use `` elements in Next.js projects. +- Don't use `` elements in Next.js projects. +- Don't import next/document outside of pages/_document.jsx in Next.js projects. +- Don't use the next/head module in pages/_document.js on Next.js projects. + +### Testing Best Practices +- Don't use export or module.exports in test files. +- Don't use focused tests. +- Make sure the assertion function, like expect, is placed inside an it() function call. +- Don't use disabled tests. + +## Common Tasks +- `npx ultracite init` - Initialize Ultracite in your project +- `npx ultracite fix` - Format and fix code automatically +- `npx ultracite check` - Check for issues without fixing + +## Example: Error Handling +```typescript +// ✅ Good: Comprehensive error handling +try { + const result = await fetchData(); + return { success: true, data: result }; +} catch (error) { + console.error('API call failed:', error); + return { success: false, error: error.message }; +} + +// ❌ Bad: Swallowing errors +try { + return await fetchData(); +} catch (e) { + console.log(e); +} +``` \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..9a03f8096 --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` +AUTH_SECRET=**** + +# The following keys below are automatically created and +# added to your environment when you deploy on Vercel + +# Instructions to create an AI Gateway API key here: https://vercel.com/ai-gateway +# API key required for non-Vercel deployments +# For Vercel deployments, OIDC tokens are used automatically +# https://vercel.com/ai-gateway +AI_GATEWAY_API_KEY=**** + + +# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob +BLOB_READ_WRITE_TOKEN=**** + +# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres +POSTGRES_URL=**** + + +# Instructions to create a Redis store here: +# https://vercel.com/docs/redis +REDIS_URL=**** + +# DashScope / 通义千问配置 +# 请求地址(阿里云 DashScope OpenAI 兼容接口) +DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 + +# API Key(请在本地 .env.local 和 服务器环境中填写真实值,不要把真实 Key 提交到代码仓库) +DASHSCOPE_API_KEY=your-dashscope-api-key + +# 第三方 GPT5 提供方配置(OpenAI 兼容接口) +# API 基础地址(例如:https://api.huandutech.com) +GTP5_API_URL=https://api.huandutech.com + +# API Key(请在本地 .env.local 和 服务器环境中填写真实值,不要把真实 Key 提交到代码仓库) +GTP5_API_KEY=your-gtp5-api-key + +# 模型名称(例如:gpt-5-nano) +GTP5_MODEL=gpt-5-nano diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..1d57a670b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint +on: + push: + +jobs: + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + node-version: [20] + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.12.3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + - name: Install dependencies + run: pnpm install + - name: Run lint + run: pnpm lint diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..6e18c3ec6 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,73 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + test: + timeout-minutes: 30 + runs-on: ubuntu-latest + env: + AUTH_SECRET: ${{ secrets.AUTH_SECRET }} + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} + REDIS_URL: ${{ secrets.REDIS_URL }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Cache Playwright browsers + uses: actions/cache@v3 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm exec playwright install --with-deps chromium + + - name: Run Playwright tests + run: pnpm test + + - uses: actions/upload-artifact@v4 + if: always() && !cancelled() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..30f5b864a --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js +.pnpm-store + +# testing +coverage + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +.env +.vercel +.env*.local + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/* +deploy.tar.gz +libvips-8.14.5-linux-x64.tar.br +.temp-packages/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..699ed7331 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..3e386d5cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,35 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[graphql]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} diff --git a/3001端口检查报告.md b/3001端口检查报告.md new file mode 100644 index 000000000..b3da2f3d9 --- /dev/null +++ b/3001端口检查报告.md @@ -0,0 +1,160 @@ +# 3001 端口开放状态检查报告 + +## 检查时间 +2025-12-04 14:10 + +## 检查结果 + +### ✅ 端口监听状态 +- **状态**: 正常监听 +- **监听地址**: `*:3001` (所有网络接口) +- **进程**: `next-server (v1)` (PID: 3704508) +- **说明**: 应用正在监听 3001 端口,可以从所有网络接口访问 + +### ✅ 应用状态 +- **PM2 状态**: `online` +- **应用名称**: `ai-chatbot` +- **进程ID**: 3704508 +- **运行时间**: 2小时 +- **内存使用**: 166.3MB + +### ✅ 端口访问测试 + +#### 1. 本地访问测试 +- **localhost:3001**: ✅ 可访问 +- **127.0.0.1:3001**: ✅ 可访问 +- **响应**: 返回 `/api/auth/guest` 重定向(正常) + +#### 2. 内网IP访问测试 +- **192.168.150.22:3001**: ✅ 可访问 +- **响应**: 返回 `/api/auth/guest` 重定向(正常) + +### ✅ 防火墙状态 +- **firewalld**: 未运行或未安装 +- **ufw**: 未运行或未安装 +- **iptables**: + - 未找到针对 3001 端口的特殊规则 + - INPUT 链默认策略:允许(未限制) + +### ✅ 端口开放结论 + +**3001 端口已完全开放,可以从以下方式访问:** + +1. **本地访问** + - `http://localhost:3001` + - `http://127.0.0.1:3001` + +2. **内网访问** + - `http://192.168.150.22:3001` + +3. **通过FRPC穿透访问** + - `http://156.225.27.52:5245` (通过FRPC TCP穿透) + +## 详细检查信息 + +### 端口监听详情 +``` +LISTEN 0 511 *:3001 *:* users:(("next-server (v1",pid=3704508,fd=19)) +``` + +**说明**: +- `*:3001`: 监听所有网络接口的 3001 端口 +- 这意味着可以从内网、外网(如果防火墙允许)访问 + +### 防火墙检查 + +#### iptables 规则 +- 未找到针对 3001 端口的特殊限制规则 +- INPUT 链默认允许所有连接 + +#### 防火墙服务 +- 未发现运行中的防火墙服务(firewalld/ufw) +- 系统可能使用默认的 iptables 规则,且未限制 3001 端口 + +## 安全建议 + +### 当前状态 +✅ **端口已开放**: 3001 端口可以从内网访问 + +### 建议配置 + +如果需要限制访问,可以考虑: + +1. **仅允许内网访问**(推荐) + ```bash + # 允许内网访问 + iptables -A INPUT -p tcp --dport 3001 -s 192.168.150.0/24 -j ACCEPT + # 拒绝其他访问 + iptables -A INPUT -p tcp --dport 3001 -j DROP + ``` + +2. **使用防火墙服务管理** + ```bash + # 如果使用 firewalld + firewall-cmd --permanent --add-port=3001/tcp + firewall-cmd --reload + ``` + +3. **通过FRPC访问(当前方案)** + - 应用监听在 127.0.0.1:3001(仅本地) + - 通过 FRPC 穿透到外网端口 5245 + - 这样更安全,不需要直接开放 3001 端口到外网 + +## 当前配置分析 + +### FRPC 配置 +```ini +[ai-chatbot] +name = ai-chatbot +type = tcp +local_ip = 127.0.0.1 +local_port = 3001 +remote_port = 5245 +``` + +**说明**: +- FRPC 配置使用 `127.0.0.1:3001`(本地回环) +- 但实际上应用监听在 `*:3001`(所有接口) +- 这没有问题,FRPC 可以从本地回环访问应用 + +### 建议优化 + +如果希望更安全,可以: + +1. **修改应用监听地址** + - 让应用只监听 `127.0.0.1:3001` + - 这样只能从本地访问,更安全 + +2. **保持当前配置** + - 当前配置允许内网访问,方便调试和管理 + - 通过 FRPC 提供外网访问 + +## 总结 + +✅ **3001 端口完全开放** +- 可以从本地访问 +- 可以从内网访问(192.168.150.22:3001) +- 可以通过 FRPC 穿透访问(156.225.27.52:5245) + +✅ **应用正常运行** +- PM2 管理,状态正常 +- 响应正常,返回预期的重定向 + +✅ **防火墙未限制** +- 未发现运行中的防火墙服务 +- iptables 未限制 3001 端口 + +--- + +**检查命令**: +```bash +# 检查端口监听 +ss -tlnp | grep ':3001' + +# 测试访问 +curl http://192.168.150.22:3001 + +# 检查防火墙 +iptables -L INPUT -n +``` + diff --git a/AI_MODEL_CONFIG_GUIDE.md b/AI_MODEL_CONFIG_GUIDE.md new file mode 100644 index 000000000..9474e6639 --- /dev/null +++ b/AI_MODEL_CONFIG_GUIDE.md @@ -0,0 +1,262 @@ +# AI 模型和存储配置指南 + +## 功能概述 + +本系统实现了完整的 AI 模型配置和存储管理功能,无需修改 `.env` 文件即可在系统设置中动态配置。 + +## 核心功能 + +### 1. AI 提供商配置 + +支持配置多个 AI 服务提供商,包括: +- **阿里云通义千问** (Alibaba) +- **OpenAI 兼容接口** (OpenAI) +- **自定义提供商** (Custom) + +**访问路径**: `/admin/settings` → AI 模型 → AI 提供商 + +**配置项**: +- 提供商名称:自定义名称,如"阿里云通义千问" +- 提供商类型:选择类型 +- API 地址:提供商的 API 基础 URL(例如:https://dashscope.aliyuncs.com/compatible-mode/v1) +- API Key:访问令牌(不回显已保存的密钥) +- 启用状态:勾选启用此提供商 + +**API 端点**: `/api/admin/ai-providers` +- `GET` - 获取所有提供商列表 +- `POST` - 创建新提供商 +- `PUT` - 更新提供商配置 +- `DELETE?id={id}` - 删除提供商 + +### 2. AI 模型配置 + +为每个提供商配置具体的模型,支持设置视觉能力和图片生成能力。 + +**访问路径**: `/admin/settings` → AI 模型 → AI 模型 + +**配置项**: +- 所属提供商:选择已配置的提供商 +- 模型 ID:提供商中的模型标识(例如:qwen-turbo, qwen-vl-plus) +- 显示名称:用户界面上显示的名称 +- 描述:模型的简要说明 +- 排序顺序:确定前端显示顺序 +- 支持图片输入:模型是否支持视觉(图片识别) +- 支持图片生成:模型是否支持图片生成 +- 启用:模型是否可用 +- 设为默认:标记为默认使用的模型 + +**API 端点**: `/api/admin/ai-models` +- `GET` - 获取模型列表(可选 `providerId` 筛选) +- `POST` - 创建新模型 +- `PUT` - 更新模型配置 +- `DELETE?id={id}` - 删除模型 + +**用户端 API**: `/api/models` +- `GET` - 获取已启用的模型列表(用于前端选择) + +### 3. S3 存储配置 + +配置 S3 兼容的对象存储服务,用于图片上传和多媒体处理。 + +**访问路径**: `/admin/settings` → 存储配置 + +**配置项**: +- 配置名称:自定义名称 +- 存储类型:S3 兼容存储或本地存储 +- 端点:S3 服务端点 URL +- 区域:地区代码 +- 存储桶:目标存储桶名称 +- Access Key ID:访问凭证 +- Secret Access Key:秘密凭证(不回显) +- 公开访问 URL 前缀:文件的公开访问地址前缀 +- 启用:是否启用此配置 +- 设为默认:优先使用的存储配置 + +**API 端点**: `/api/admin/storage` +- `GET` - 获取存储配置列表 +- `POST` - 创建新配置 +- `PUT` - 更新配置 +- `DELETE?id={id}` - 删除配置 + +### 4. 前端模型选择 + +用户在聊天界面选择模型时,系统会: +1. 首先加载数据库中配置的动态模型 +2. 如果没有配置,则显示预定义的静态模型 +3. 优先显示标记为"默认"的模型 + +**调用流程**: +- 前端组件:`components/model-selector.tsx` +- 调用 API:`/api/models` 获取可用模型列表 +- 支持视觉模型标签显示 +- 支持默认模型标签显示 + +## 数据库结构 + +### AiProvider 表 +```sql +CREATE TABLE "AiProvider" ( + id uuid PRIMARY KEY, + name varchar(128) NOT NULL, -- 提供商名称 + type varchar NOT NULL, -- 类型: alibaba, openai, custom + baseUrl text NOT NULL, -- API 基础 URL + apiKey text NOT NULL, -- API Key + isEnabled boolean DEFAULT true, -- 是否启用 + createdAt timestamp DEFAULT now(), + updatedAt timestamp DEFAULT now(), + updatedBy uuid REFERENCES "User"(id) +); +``` + +### AiModel 表 +```sql +CREATE TABLE "AiModel" ( + id uuid PRIMARY KEY, + providerId uuid NOT NULL REFERENCES "AiProvider"(id) ON DELETE CASCADE, + modelId varchar(128) NOT NULL, -- 模型 ID + displayName varchar(128) NOT NULL, -- 显示名称 + description text, -- 描述 + supportsVision boolean DEFAULT false, -- 支持图片输入 + supportsImageGen boolean DEFAULT false, -- 支持图片生成 + isEnabled boolean DEFAULT true, + isDefault boolean DEFAULT false, + sortOrder json DEFAULT 0, + createdAt timestamp DEFAULT now(), + updatedAt timestamp DEFAULT now() +); +``` + +### StorageConfig 表 +```sql +CREATE TABLE "StorageConfig" ( + id uuid PRIMARY KEY, + name varchar(128) NOT NULL, -- 配置名称 + type varchar DEFAULT 's3', -- 类型: s3, local + endpoint text, -- S3 端点 + region varchar(64), -- 区域 + bucket varchar(128), -- 存储桶 + accessKeyId text, -- Access Key + secretAccessKey text, -- Secret Key + publicUrl text, -- 公开访问 URL 前缀 + isDefault boolean DEFAULT false, + isEnabled boolean DEFAULT true, + createdAt timestamp DEFAULT now(), + updatedAt timestamp DEFAULT now(), + updatedBy uuid REFERENCES "User"(id) +); +``` + +## 配置优先级 + +### AI 模型使用顺序 +1. **数据库配置优先** - 从 AiModel 表动态加载 +2. **环境变量回退** - 使用 DASHSCOPE_API_KEY 等 +3. **预定义模型** - 最后使用预定义的模型列表 + +### 存储服务优先级 +1. **数据库配置** - 优先使用标记为默认的存储配置 +2. **环境变量** - 使用 S3_* 系列环境变量 +3. **Vercel Blob** - 最后回退到 Vercel Blob + +## 实现示例 + +### 配置阿里云通义千问 + +1. **创建提供商** + ``` + 名称: 阿里云通义千问 + 类型: Alibaba + API 地址: https://dashscope.aliyuncs.com/compatible-mode/v1 + API Key: sk-xxxxxxxxxxxxxxxxxxxxxxxx + ``` + +2. **创建模型** + ``` + 提供商: 阿里云通义千问 + 模型 ID: qwen-turbo + 显示名称: 通义千问 Turbo + 支持图片输入: ☑ + 设为默认: ☑ + ``` + +### 配置 S3 存储 + +``` +配置名称: 阿里云 OSS +类型: S3 兼容存储 +端点: https://oss-cn-hangzhou.aliyuncs.com +区域: cn-hangzhou +存储桶: my-chatbot-bucket +Access Key ID: LTAI5t... +Secret Access Key: xxxxxxxxxxxxxxxx +公开访问 URL 前缀: https://my-chatbot-bucket.oss-cn-hangzhou.aliyuncs.com +设为默认: ☑ +``` + +## 关键代码位置 + +### 后端核心文件 +- `lib/db/schema.ts` - 数据库表定义 +- `lib/db/queries.ts` - 数据库查询函数 +- `lib/ai/dynamic-provider.ts` - 动态模型加载 +- `app/(admin)/api/ai-providers/route.ts` - 提供商 API +- `app/(admin)/api/ai-models/route.ts` - 模型 API +- `app/(admin)/api/storage/route.ts` - 存储 API +- `app/(chat)/api/models/route.ts` - 前端模型列表 API + +### 前端核心文件 +- `components/model-selector.tsx` - 模型选择组件 +- `app/(admin)/settings/admin-settings-client.tsx` - 管理设置页面 +- `app/(chat)/api/files/upload/route.ts` - 文件上传处理 + +## 环境变量(可选) + +虽然不再需要配置这些,但如果设置了环境变量,系统会作为回退使用: + +```env +# AI 配置回退 +DASHSCOPE_API_KEY=your_key +DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 + +# S3 存储回退 +S3_ENDPOINT=https://s3.amazonaws.com +S3_REGION=us-east-1 +S3_BUCKET=my-bucket +S3_ACCESS_KEY_ID=your_access_key +S3_SECRET_ACCESS_KEY=your_secret_key +S3_PUBLIC_URL=https://my-bucket.s3.amazonaws.com +``` + +## 故障排除 + +### 模型不显示 +- 检查 AiProvider 是否启用 (isEnabled = true) +- 检查 AiModel 是否启用 (isEnabled = true) +- 检查 API 密钥和地址是否正确 +- 查看浏览器开发者工具中 `/api/models` 的响应 + +### 文件上传失败 +- 确认 StorageConfig 已设置为默认 (isDefault = true) +- 验证 S3 凭证和端点 +- 检查存储桶权限设置 + +### 默认模型不生效 +- 确保只有一个模型设置为默认 (isDefault = true) +- 重启应用使缓存更新 +- 检查前端是否正确读取 defaultModelId + +## 安全考虑 + +- **API Key 保护** - API Key 在数据库中存储,不在响应中回显 +- **权限控制** - 所有管理 API 需要 admin 权限 +- **数据加密** - 建议在生产环境中加密存储的敏感信息 +- **访问控制** - 限制对 `/admin/settings` 的访问 + +## 迁移说明 + +如果从旧版本升级: +1. 运行数据库迁移:`npm run db:migrate` +2. 访问管理设置重新配置 AI 模型和存储 +3. 删除 `.env.local` 中的旧配置(可选) +4. 重启应用 + diff --git a/AI供应商配置指南.md b/AI供应商配置指南.md new file mode 100644 index 000000000..68e28acae --- /dev/null +++ b/AI供应商配置指南.md @@ -0,0 +1,421 @@ +# AI 供应商配置指南 + +## 概述 + +本系统支持多种 AI 供应商的动态配置,所有供应商都使用 **OpenAI 兼容的 API 格式**,但根据不同供应商的特点进行了优化。 + +## 支持的供应商类型 + +### 1. 阿里云 (alibaba) + +**适用场景:** 通义千问系列模型(Qwen) + +**配置示例:** +```json +{ + "name": "阿里云通义千问", + "type": "alibaba", + "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "apiKey": "sk-xxxxxxxxxxxxxxxx" +} +``` + +**可用模型:** +- `qwen-turbo` - 快速响应,适合日常对话 +- `qwen-plus` - 更强性能,适合复杂任务 +- `qwen-max` - 最强性能 +- `qwen-vl-plus` - 支持图像输入的多模态模型 +- `qwen3-vl-plus` - 最新多模态模型 + +**特点:** +- 完全兼容 OpenAI Chat Completions API +- 支持流式响应 (stream: true) +- 支持 Function Calling +- 支持多模态输入(图片、视频) + +**请求示例:** +```bash +POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions +Authorization: Bearer sk-xxxxxxxxxxxxxxxx +Content-Type: application/json + +{ + "model": "qwen-plus", + "messages": [ + {"role": "system", "content": "你是一个有帮助的助手"}, + {"role": "user", "content": "你好"} + ], + "stream": true +} +``` + +--- + +### 2. OpenAI 官方 (openai) + +**适用场景:** OpenAI 官方模型(GPT-4、GPT-3.5 等) + +**配置示例:** +```json +{ + "name": "OpenAI 官方", + "type": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-proj-xxxxxxxxxxxxxxxx" +} +``` + +**可用模型:** +- `gpt-4-turbo` - GPT-4 Turbo +- `gpt-4` - GPT-4 +- `gpt-3.5-turbo` - GPT-3.5 Turbo +- `o1` - 推理模型 +- `o1-mini` - 轻量推理模型 + +**特点:** +- 标准 OpenAI API +- 支持所有官方功能 +- 支持 Function Calling、Vision、Audio + +**请求示例:** +```bash +POST https://api.openai.com/v1/chat/completions +Authorization: Bearer sk-proj-xxxxxxxxxxxxxxxx +Content-Type: application/json + +{ + "model": "gpt-4-turbo", + "messages": [ + {"role": "system", "content": "You are a helpful assistant"}, + {"role": "user", "content": "Hello"} + ] +} +``` + +--- + +### 3. OpenAI 兼容 (custom) + +**适用场景:** 任何提供 OpenAI 兼容 API 的第三方服务 + +**配置示例:** +```json +{ + "name": "GPT-5 Nano", + "type": "custom", + "baseUrl": "https://api.huandutech.com/v1", + "apiKey": "sk-xxxxxxxxxxxxxxxx" +} +``` + +**典型的兼容服务:** +- Claude via OpenRouter +- GPT-5 Nano 等第三方优化模型 +- 本地部署的 OpenAI 兼容服务(如 vLLM、Text Generation WebUI) +- Azure OpenAI Service +- Cloudflare Workers AI + +**请求格式:** +```bash +POST /chat/completions +Authorization: Bearer +Content-Type: application/json + +{ + "model": "", + "messages": [ + {"role": "user", "content": "Hello"} + ], + "stream": true // 可选 +} +``` + +**兼容性要求:** +1. **必需端点:** `/chat/completions` 或 `/v1/chat/completions` +2. **必需请求头:** `Authorization: Bearer ` +3. **请求格式:** 符合 OpenAI Chat Completions API 规范 +4. **响应格式:** 返回标准 OpenAI 格式的 JSON 响应 + +**完整请求示例:** +```json +{ + "model": "gpt-5-nano", + "messages": [ + { + "role": "system", + "content": "你是一个有帮助的助手。" + }, + { + "role": "user", + "content": "你能做什么?" + } + ], + "temperature": 0.7, + "max_tokens": 2000, + "stream": true, + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0 +} +``` + +--- + +## 系统设置配置步骤 + +### 1. 添加 AI 供应商 + +1. 登录管理员账户 +2. 进入 **系统设置** → **AI 提供商** +3. 点击 **添加提供商** +4. 填写信息: + - **名称:** 供应商显示名称(如"阿里云通义千问") + - **类型:** 选择 alibaba / openai / custom + - **Base URL:** API 基础地址 + - **API Key:** 您的 API 密钥 +5. 点击 **保存** + +### 2. 添加 AI 模型 + +1. 在 **系统设置** → **AI 模型** 中 +2. 点击 **添加模型** +3. 填写信息: + - **供应商:** 选择刚创建的供应商 + - **模型 ID:** 模型的实际标识符(如 `qwen-plus`、`gpt-4`) + - **显示名称:** 前端显示的名称(如"通义千问 Plus") + - **描述:** 模型的简要说明 + - **支持视觉:** 是否支持图像输入 + - **默认模型:** 是否设为系统默认 + - **排序顺序:** 前端显示的顺序 +4. 点击 **保存** + +### 3. 测试配置 + +1. 返回聊天界面 +2. 在模型选择器中应该能看到新配置的模型 +3. 选择模型并发送测试消息 +4. 查看服务器日志确认请求正常 + +--- + +## API 请求格式详解 + +所有供应商都遵循 OpenAI Chat Completions API 规范: + +### 基本请求结构 + +```typescript +interface ChatCompletionRequest { + model: string; // 模型 ID + messages: Message[]; // 消息列表 + temperature?: number; // 0-2,控制随机性 + max_tokens?: number; // 最大生成 token 数 + stream?: boolean; // 是否流式响应 + top_p?: number; // 核采样参数 + frequency_penalty?: number; // 频率惩罚 + presence_penalty?: number; // 存在惩罚 + stop?: string | string[]; // 停止序列 +} + +interface Message { + role: "system" | "user" | "assistant" | "tool"; + content: string | ContentPart[]; + name?: string; + tool_calls?: ToolCall[]; +} +``` + +### 多模态输入(支持的模型) + +```json +{ + "model": "qwen-vl-plus", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "这张图片里有什么?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.jpg" + } + } + ] + } + ] +} +``` + +### Function Calling + +```json +{ + "model": "qwen-plus", + "messages": [ + { + "role": "user", + "content": "波士顿今天的天气怎么样?" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "获取指定位置的当前天气", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市和州,例如 San Francisco, CA" + } + }, + "required": ["location"] + } + } + } + ], + "tool_choice": "auto" +} +``` + +--- + +## 常见问题 + +### Q: 配置后前端没有显示新模型? + +**A:** 请检查: +1. 模型是否启用(`isEnabled = true`) +2. 供应商是否启用(`isEnabled = true`) +3. 清除浏览器缓存和 Cookie +4. 硬刷新页面(Ctrl+Shift+R / Cmd+Shift+R) + +### Q: 模型请求失败,返回 503 错误? + +**A:** 可能原因: +1. API Key 无效或过期 +2. Base URL 配置错误 +3. 供应商服务不可用 +4. 网络连接问题 + +**调试步骤:** +1. 检查服务器日志:`pm2 logs ai-chatbot` +2. 使用 curl 测试 API: + ```bash + curl -X POST /chat/completions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"model":"","messages":[{"role":"user","content":"test"}]}' + ``` + +### Q: 阿里云模型和 OpenAI 模型有什么区别? + +**A:** +- **API 格式:** 完全相同(OpenAI 兼容) +- **模型 ID:** 不同(qwen-plus vs gpt-4) +- **Base URL:** 不同(dashscope.aliyuncs.com vs api.openai.com) +- **功能支持:** 基本相同,部分高级功能可能有差异 + +### Q: 如何配置本地部署的模型? + +**A:** 选择 **custom** 类型,配置: +```json +{ + "type": "custom", + "baseUrl": "http://localhost:8000/v1", + "apiKey": "your-local-key" +} +``` + +确保本地服务提供 OpenAI 兼容的 API 接口。 + +--- + +## 技术实现说明 + +### 代码位置 + +- **动态 Provider:** `lib/ai/dynamic-provider.ts` +- **数据库 Schema:** `lib/db/schema.ts` +- **API 路由:** `app/api/admin/ai-providers/route.ts`、`app/api/admin/ai-models/route.ts` +- **前端组件:** `components/model-selector.tsx`、`components/multimodal-input.tsx` + +### Provider 创建逻辑 + +```typescript +function getOrCreateProvider(provider: AiProvider) { + switch (provider.type) { + case "alibaba": + return createOpenAI({ + apiKey: provider.apiKey, + baseURL: provider.baseUrl, // https://dashscope.aliyuncs.com/compatible-mode/v1 + }); + + case "openai": + return createOpenAI({ + apiKey: provider.apiKey, + baseURL: provider.baseUrl || "https://api.openai.com/v1", + }); + + case "custom": + default: + return createOpenAI({ + apiKey: provider.apiKey, + baseURL: provider.baseUrl, // 任何 OpenAI 兼容的 URL + }); + } +} +``` + +### 缓存机制 + +系统使用 `Map` 缓存已创建的 provider 实例,避免重复创建: + +```typescript +const providerCache = new Map>(); +``` + +缓存键格式:`${providerId}-${baseUrl}` + +--- + +## 最佳实践 + +1. **分类管理:** 为不同用途的模型创建不同的供应商配置(如"快速对话"、"深度思考") +2. **设置默认:** 为最常用的模型设置 `isDefault = true` +3. **排序优化:** 使用 `sortOrder` 控制前端显示顺序 +4. **描述清晰:** 为每个模型添加清晰的描述,帮助用户选择 +5. **定期更新:** 供应商可能推出新模型,及时更新配置 +6. **监控使用:** 查看日志了解各模型的使用情况和错误率 + +--- + +## 安全建议 + +1. **API Key 保护:** + - 不要在前端代码中暴露 API Key + - API Key 存储在数据库中(建议加密) + - 定期轮换 API Key + +2. **访问控制:** + - 只有管理员可以配置供应商和模型 + - 使用 `requireAdmin()` 中间件保护 API 端点 + +3. **费用控制:** + - 设置合理的 `max_tokens` 限制 + - 监控 API 使用量 + - 考虑实现用户级别的配额限制 + +--- + +## 更新日志 + +- **2025-12-05:** 实现多供应商独立配置,支持阿里云、OpenAI、OpenAI兼容 +- **未来计划:** 支持更多供应商类型(Claude、Gemini等) + diff --git a/FRPC修复完成报告.md b/FRPC修复完成报告.md new file mode 100644 index 000000000..1743a820d --- /dev/null +++ b/FRPC修复完成报告.md @@ -0,0 +1,142 @@ +# FRPC 修复完成报告 + +## 问题总结 + +### 原始问题 +1. 访问 `http://156.225.27.52:5245/` 时端口号消失,返回 Nginx 404 +2. FRPC 服务无法启动,一直处于重启循环 + +### 根本原因 +1. **FRPC 配置错误**: `ai-chatbot` 配置使用了 HTTP 类型,但缺少 `custom_domains` +2. **其他代理配置错误**: 多个 HTTP 类型的代理(`http044`, `bt-panel` 等)缺少必需的 `custom_domains` 或 `subdomain` 配置 +3. **配置冲突**: HTTP 类型配置不完整导致 FRPC 无法启动 + +## 修复方案 + +### 1. 将所有 HTTP 类型改为 TCP 类型 +- HTTP 类型需要配置 `custom_domains` 或 `subdomain`,配置复杂 +- TCP 类型更简单直接,适合端口转发场景 +- 将所有代理配置统一改为 TCP 类型 + +### 2. 修复后的配置 + +```ini +[ai-chatbot] +name = ai-chatbot +type = tcp +local_ip = 127.0.0.1 +local_port = 3001 +remote_port = 5245 +``` + +### 3. 修复的其他代理 +- `http044`: SSH (5204) - 改为 TCP +- `bt-panel`: 宝塔面板 (5205) - 改为 TCP +- `ai-classification`: AI分类系统 (5206) - 改为 TCP +- `performance-api`: 性能API (5234) - 改为 TCP +- `qwen-api`: Qwen API (5236) - 改为 TCP +- `weipinghui-parser`: 微品汇解析器 (5238) - 改为 TCP +- `shebeiguanli-frontend`: 设备管理前端 (5243) - 改为 TCP +- `shebeiguanli-backend`: 设备管理后端 (5244) - 改为 TCP + +## 修复结果 + +### ✅ FRPC 服务状态 +- **状态**: `active (running)` +- **启动时间**: 2025-12-04 14:05:50 +- **所有代理**: 全部启动成功 + +### ✅ ai-chatbot 代理状态 +- **代理名称**: `ai-chatbot` +- **状态**: `start proxy success` +- **本地地址**: `127.0.0.1:3001` +- **远程端口**: `5245` + +### ✅ 访问测试 +- **访问地址**: `http://156.225.27.52:5245` +- **连接状态**: ✅ 成功连接 +- **响应**: HTTP 307 重定向到 `/api/auth/guest` +- **说明**: 应用正常工作,返回了预期的认证重定向 + +## 当前状态 + +### 应用访问 +- **外网访问**: `http://156.225.27.52:5245` +- **内网访问**: `http://192.168.150.22:3001` +- **状态**: ✅ 正常运行 + +### FRPC 服务 +- **服务状态**: ✅ 运行中 +- **配置文件**: `/etc/frp/frpc.ini` +- **备份文件**: `/etc/frp/frpc.ini.backup.before_fix` + +### 应用服务 +- **PM2 状态**: ✅ 运行中 +- **进程名称**: `ai-chatbot` +- **监听端口**: `3001` + +## 注意事项 + +### 1. 重定向 URL 问题 +应用返回的重定向 URL 中包含 `localhost:3001`,这可能导致浏览器访问时出现问题。建议: +- 检查应用的重定向逻辑,确保使用正确的域名和端口 +- 或者在应用配置中设置正确的 `NEXT_PUBLIC_BASE_URL` + +### 2. 端口冲突 +`shebeiguanli-backend` 和 `ai-chatbot` 都使用 3001 端口,但: +- `shebeiguanli-backend` 使用内网 IP `192.168.150.22:3001` +- `ai-chatbot` 使用本地回环 `127.0.0.1:3001` +- 目前没有冲突,但需要注意 + +### 3. 配置备份 +- 原始配置已备份到: `/etc/frp/frpc.ini.backup.before_fix` +- 如需恢复,可以使用备份文件 + +## 验证命令 + +### 检查 FRPC 状态 +```bash +ssh root@192.168.150.22 +systemctl status frpc.service +``` + +### 查看 FRPC 日志 +```bash +journalctl -u frpc.service -n 50 | grep ai-chatbot +``` + +### 测试本地应用 +```bash +curl http://localhost:3001 +``` + +### 测试外网访问 +```bash +curl http://156.225.27.52:5245 +``` + +## 后续优化建议 + +1. **应用配置优化** + - 设置正确的 `NEXT_PUBLIC_BASE_URL` 环境变量 + - 修复重定向 URL 中的 localhost 问题 + +2. **监控和日志** + - 设置 FRPC 日志轮转 + - 添加应用健康检查 + +3. **安全性** + - 考虑添加访问控制 + - 配置 HTTPS(如果需要) + +## 总结 + +✅ **问题已解决**: FRPC 服务正常运行,ai-chatbot 代理成功启动 +✅ **访问正常**: 外网可以通过 `http://156.225.27.52:5245` 访问应用 +✅ **配置已优化**: 所有代理配置统一使用 TCP 类型,更稳定可靠 + +--- + +**修复时间**: 2025-12-04 14:06 +**修复方式**: 通过内网 SSH (192.168.150.22) 连接并修复配置 + diff --git a/FRPC穿透配置说明.md b/FRPC穿透配置说明.md new file mode 100644 index 000000000..0e3ed9140 --- /dev/null +++ b/FRPC穿透配置说明.md @@ -0,0 +1,153 @@ +# FRPC 穿透配置说明 + +## 配置信息 + +### 穿透配置已成功添加 + +**配置名称**: `ai-chatbot` + +**配置详情**: +- **本地地址**: `127.0.0.1:3001` (已修改,避免与3000端口冲突) +- **远程端口**: `5245` +- **穿透类型**: TCP +- **状态**: ✅ 已启动成功 + +### 访问方式 + +**通过穿透访问**: +- **穿透地址**: `156.225.27.52:5245` +- **访问URL**: `http://156.225.27.52:5245` + +**直接访问**(如果服务器开放80端口): +- **直接地址**: `http://156.225.27.52` + +### 配置位置 + +- **配置文件**: `/etc/frp/frpc.ini` +- **备份文件**: `/etc/frp/frpc.ini.backup` +- **服务状态**: `systemctl status frpc.service` + +## 当前配置内容 + +```ini +[ai-chatbot] +name = ai-chatbot +type = tcp +local_ip = 127.0.0.1 +local_port = 3001 +remote_port = 5245 +``` + +**注意**: 应用端口已从3000改为3001,因为3000端口被其他服务占用。 + +## 验证穿透 + +### 检查FRPC服务状态 + +```bash +# 查看服务状态 +systemctl status frpc.service + +# 查看日志 +journalctl -u frpc.service -n 50 | grep ai-chatbot + +# 查看所有代理状态 +journalctl -u frpc.service -n 100 | grep "start proxy success" +``` + +### 测试连接 + +```bash +# 测试本地端口 +curl http://localhost:3000 + +# 测试穿透端口(从外网) +curl http://156.225.27.52:5245 +``` + +## 注意事项 + +1. **端口冲突**: 如果3000端口被其他服务占用,需要: + - 修改应用监听端口 + - 或修改FRPC配置中的 `local_port` + +2. **防火墙**: 确保FRP服务器端(156.225.27.52)开放了5245端口 + +3. **应用状态**: 确保应用正常运行在3000端口: + ```bash + pm2 status ai-chatbot + pm2 logs ai-chatbot + ``` + +## 修改配置 + +如果需要修改穿透配置: + +```bash +# 1. 编辑配置文件 +nano /etc/frp/frpc.ini + +# 2. 修改配置后重启服务 +systemctl restart frpc.service + +# 3. 检查状态 +systemctl status frpc.service +``` + +## 常用命令 + +```bash +# 查看FRPC配置 +cat /etc/frp/frpc.ini + +# 重启FRPC服务 +systemctl restart frpc.service + +# 查看FRPC日志 +journalctl -u frpc.service -f + +# 查看所有穿透代理 +journalctl -u frpc.service | grep "start proxy success" +``` + +## 故障排查 + +### 如果穿透不工作 + +1. **检查应用是否运行**: + ```bash + pm2 status ai-chatbot + ``` + +2. **检查端口监听**: + ```bash + ss -tlnp | grep 3000 + ``` + +3. **检查FRPC日志**: + ```bash + journalctl -u frpc.service -n 50 + ``` + +4. **检查FRPC配置**: + ```bash + /usr/local/bin/frpc -c /etc/frp/frpc.ini -L /tmp/frpc.log + ``` + +5. **检查FRP服务器端端口**: + - 确保FRP服务器(156.225.27.52)的7000端口正常 + - 确保远程端口5245未被占用 + +## 其他穿透配置 + +当前服务器还有其他穿透配置: +- `http044`: SSH (5204) +- `bt-panel`: 宝塔面板 (5205) +- `ai-classification`: AI分类系统 (5206) +- `performance-api`: 性能API (5234) +- `qwen-api`: Qwen API (5236) +- `weipinghui-parser`: 微品汇解析器 (5238) +- `shebeiguanli-frontend`: 设备管理前端 (5243) +- `shebeiguanli-backend`: 设备管理后端 (5244) +- `ai-chatbot`: AI聊天机器人 (5245) ✅ 新增 + diff --git a/FRPC访问问题说明.md b/FRPC访问问题说明.md new file mode 100644 index 000000000..905d28910 --- /dev/null +++ b/FRPC访问问题说明.md @@ -0,0 +1,170 @@ +# FRPC 访问问题说明 + +## 问题描述 + +访问 `http://156.225.27.52:5245/` 时: +- 浏览器地址栏变成 `156.225.27.52`(端口号消失) +- 返回 404 Not Found(Nginx) + +## 问题分析 + +### 可能的原因 + +1. **FRPC 配置类型问题** + - 之前将配置改为 HTTP 类型,但 HTTP 类型需要通过 FRPS 的 HTTP 端口访问 + - TCP 类型更适合直接端口访问 + +2. **Nginx 拦截请求** + - Nginx 的 `default_server` 可能拦截了未匹配的请求 + - 5245 端口应该由 FRPS 监听,而不是 Nginx + +3. **FRPS 服务端配置** + - FRPS 可能没有正确配置 5245 端口的 TCP 穿透 + - 需要检查 FRPS 服务端的配置 + +4. **浏览器行为** + - 某些浏览器在遇到重定向时会去掉端口号 + - 需要确保应用正确处理端口 + +## 解决方案 + +### 方案 1: 使用 TCP 类型(推荐) + +TCP 类型穿透更简单直接,适合直接端口访问: + +```ini +[ai-chatbot] +name = ai-chatbot +type = tcp +local_ip = 127.0.0.1 +local_port = 3001 +remote_port = 5245 +``` + +**访问方式**: `http://156.225.27.52:5245/` + +### 方案 2: 使用 HTTP 类型(需要 FRPS 支持) + +如果使用 HTTP 类型,需要: +1. FRPS 配置 HTTP 端口(通常是 80 或 8080) +2. 通过 FRPS 的 HTTP 端口 + custom_domains 访问 +3. 配置示例: + +```ini +[ai-chatbot] +name = ai-chatbot +type = http +local_ip = 127.0.0.1 +local_port = 3001 +custom_domains = 156.225.27.52 +``` + +**访问方式**: `http://156.225.27.52:FRPS_HTTP_PORT/`(不是 5245) + +### 方案 3: 使用 Nginx 反向代理 + +如果 FRPS 不支持 HTTP 类型,可以: +1. 保持 FRPC 使用 TCP 类型 +2. 配置 Nginx 反向代理到本地 3001 端口 +3. 通过 Nginx 的 80 端口访问 + +## 修复步骤 + +### 自动修复 + +运行修复脚本: + +```bash +./fix-frpc-access.sh +``` + +### 手动修复 + +1. **SSH 连接到服务器** + ```bash + ssh -p 5204 root@156.225.27.52 + ``` + +2. **检查 FRPC 配置** + ```bash + cat /etc/frp/frpc.ini | grep -A 6 'ai-chatbot' + ``` + +3. **修复配置(使用 TCP 类型)** + ```bash + # 备份配置 + cp /etc/frp/frpc.ini /etc/frp/frpc.ini.backup + + # 删除旧的 ai-chatbot 配置 + sed -i '/\[ai-chatbot\]/,+6d' /etc/frp/frpc.ini + + # 添加正确的 TCP 配置 + cat >> /etc/frp/frpc.ini << 'EOF' + + [ai-chatbot] + name = ai-chatbot + type = tcp + local_ip = 127.0.0.1 + local_port = 3001 + remote_port = 5245 + EOF + ``` + +4. **重启 FRPC 服务** + ```bash + systemctl restart frpc.service + ``` + +5. **检查服务状态** + ```bash + systemctl status frpc.service + journalctl -u frpc.service -n 50 | grep ai-chatbot + ``` + +6. **测试本地应用** + ```bash + curl http://localhost:3001 + ``` + +## 检查清单 + +- [ ] FRPC 服务运行正常 +- [ ] FRPC 配置使用 TCP 类型 +- [ ] 应用运行在 3001 端口 +- [ ] FRPS 服务端配置了 5245 端口 +- [ ] 防火墙开放了 5245 端口 +- [ ] 本地应用可以访问(curl http://localhost:3001) + +## 常见问题 + +### Q: 为什么访问时端口号会消失? + +A: 这可能是浏览器的行为,或者应用返回了重定向。检查应用的重定向逻辑,确保保留端口号。 + +### Q: 为什么返回 Nginx 404? + +A: 说明请求被 Nginx 拦截了。检查: +1. FRPS 是否正确监听 5245 端口 +2. Nginx 是否配置了 default_server 拦截所有请求 +3. FRPC 配置是否正确 + +### Q: TCP 和 HTTP 类型有什么区别? + +A: +- **TCP**: 直接端口转发,访问时使用 `http://IP:PORT` +- **HTTP**: 通过 FRPS 的 HTTP 端口访问,需要配置 custom_domains + +### Q: 如何确认 FRPS 配置? + +A: FRPS 配置通常在 FRP 服务端服务器上,需要检查: +1. FRPS 配置文件中的 `allow_ports` 是否包含 5245 +2. FRPS 服务是否正常运行 +3. FRPS 日志中是否有 ai-chatbot 的连接记录 + +## 下一步 + +1. 运行修复脚本或手动修复配置 +2. 确认 FRPS 服务端配置正确 +3. 测试访问 `http://156.225.27.52:5245/` +4. 如果仍有问题,检查 FRPS 服务端配置 + diff --git a/Google Antigravity Auth Success.html b/Google Antigravity Auth Success.html new file mode 100644 index 000000000..c08bbcb28 --- /dev/null +++ b/Google Antigravity Auth Success.html @@ -0,0 +1,62 @@ + + + + + + Google Antigravity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..695ee2d6a --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 Vercel, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..4df063825 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ + + Next.js 14 and App Router-ready AI chatbot. +

Chat SDK

+
+ +

+ Chat SDK is a free, open-source template built with Next.js and the AI SDK that helps you quickly build powerful chatbot applications. +

+ +

+ Read Docs · + Features · + Model Providers · + Deploy Your Own · + Running locally +

+
+ +## Features + +- [Next.js](https://nextjs.org) App Router + - Advanced routing for seamless navigation and performance + - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance +- [AI SDK](https://ai-sdk.dev/docs/introduction) + - Unified API for generating text, structured objects, and tool calls with LLMs + - Hooks for building dynamic chat and generative user interfaces + - Supports xAI (default), OpenAI, Fireworks, and other model providers +- [shadcn/ui](https://ui.shadcn.com) + - Styling with [Tailwind CSS](https://tailwindcss.com) + - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility +- Data Persistence + - [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data + - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage +- [Auth.js](https://authjs.dev) + - Simple and secure authentication + +## Model Providers + +This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default configuration includes [xAI](https://x.ai) models (`grok-2-vision-1212`, `grok-3-mini`) routed through the gateway. + +### AI Gateway Authentication + +**For Vercel deployments**: Authentication is handled automatically via OIDC tokens. + +**For non-Vercel deployments**: You need to provide an AI Gateway API key by setting the `AI_GATEWAY_API_KEY` environment variable in your `.env.local` file. + +With the [AI SDK](https://ai-sdk.dev/docs/introduction), you can also switch to direct LLM providers like [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://ai-sdk.dev/providers/ai-sdk-providers) with just a few lines of code. + +## Deploy Your Own + +You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/templates/next.js/nextjs-ai-chatbot) + +## Running locally + +You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. + +> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts. + +1. Install Vercel CLI: `npm i -g vercel` +2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` +3. Download your environment variables: `vercel env pull` + +```bash +pnpm install +pnpm db:migrate # Setup database or apply latest database changes +pnpm dev +``` + +Your app template should now be running on [localhost:3000](http://localhost:3000). diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx new file mode 100644 index 000000000..3fa0d95ff --- /dev/null +++ b/app/(admin)/layout.tsx @@ -0,0 +1,34 @@ +import { auth } from "@/app/(auth)/auth"; +import { TopNavbar } from "@/components/top-navbar"; +import { getSiteSettings, getNavLinks } from "@/lib/db/queries"; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [session, siteSettings, navLinks] = await Promise.all([ + auth(), + getSiteSettings(), + getNavLinks(), + ]); + + return ( +
+ ({ + id: link.id, + name: link.name, + url: link.url, + order: link.order, + }))} + /> +
{children}
+
+ ); +} + diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx new file mode 100644 index 000000000..5a3f86125 --- /dev/null +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -0,0 +1,2293 @@ +"use client"; + +import { useState, useEffect } from "react"; +import useSWR from "swr"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { + Trash2, + Save, + User, + Settings, + Wrench, + Bot, + HardDrive, + Plus, + Edit, + Eye, + EyeOff, + Menu, + Key, + MessageSquare, + Bell, + Image as ImageIcon, + BarChart3, + Search, + Calendar, + TrendingUp, + Users, + Clock, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +// 类型定义 +type User = { + id: string; + email: string; + name: string | null; + role: "guest" | "regular" | "admin"; + isActive: boolean; + createdAt: string; +}; + +const ROLE_LABELS: Record = { + guest: "访客用户", + regular: "普通用户", + admin: "管理员", +}; + +type ToolConfig = { + toolName: string; + isEnabled: boolean; + description?: string; + config?: any; +}; + +type AiProvider = { + id: string; + name: string; + type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video"; + baseUrl: string; + apiKey: string; + isEnabled: boolean; + createdAt: string; +}; + +type AiModel = { + id: string; + providerId: string; + modelId: string; + displayName: string; + description?: string; + supportsVision: boolean; + supportsImageGen: boolean; + isEnabled: boolean; + isDefault: boolean; + sortOrder: number; +}; + +type StorageConfig = { + id: string; + name: string; + type: "s3" | "local"; + endpoint?: string; + region?: string; + bucket?: string; + accessKeyId?: string; + secretAccessKey?: string; + publicUrl?: string; + isDefault: boolean; + isEnabled: boolean; +}; + +type SiteSettings = { + siteName: string; + siteLogo: string | null; + siteLogoDark: string | null; + favicon: string | null; + showAutoModel: boolean; + usageTimeEnabled: boolean; + usageTimeStart: string; + usageTimeEnd: string; + usageTimeMessage: string; +}; + +type NavLink = { + id: string; + name: string; + url: string; + order: number; + isEnabled: boolean; +}; + +type AuthSettings = { + dingTalkAppKey: string | null; + dingTalkAppSecret: string | null; + isDingTalkEnabled: boolean; + isRegistrationEnabled?: boolean; +}; + +type GeneratedImageRecord = { + id: string; + userId: string; + userName: string | null; + userEmail: string; + thumbnailUrl: string; + originalUrl: string; + imageSize: string | null; + prompt: string | null; + modelId: string | null; + createdAt: string; + chatId: string | null; +}; + +type ImageAnalytics = { + overview: { + totalImages: number; + totalUsers: number; + todayImages: number; + todayUsers: number; + weekImages: number; + weekUsers: number; + monthImages: number; + monthUsers: number; + }; + userStats: Array<{ + userId: string; + userName: string | null; + userEmail: string; + imageCount: number; + }>; + dailyStats: Array<{ date: string; count: number }>; + sizeStats: Array<{ imageSize: string | null; count: number }>; + modelStats: Array<{ modelId: string | null; count: number }>; + monthlyStats: Array<{ month: string; count: number }>; + hourlyStats: Array<{ hour: number; count: number }>; +}; + +const AVAILABLE_TOOLS = [ + { name: "getWeather", description: "获取天气信息" }, + { name: "createDocument", description: "创建文档" }, + { name: "updateDocument", description: "更新文档" }, + { name: "requestSuggestions", description: "请求文档建议" }, +]; + +const PROVIDER_TYPES = [ + { value: "alibaba", label: "阿里云通义千问" }, + { value: "openai", label: "OpenAI 兼容" }, + { value: "custom", label: "自定义 OpenAI 兼容" }, + { value: "local", label: "本地模型" }, + { value: "gemini", label: "Gemini中转" }, + { value: "qianwen-video", label: "千问视频生成" }, + { value: "volcano-vision", label: "火山引擎视觉" }, + { value: "volcano-video", label: "火山引擎视频" }, +]; + +export function AdminSettingsClient() { + const [activeTab, setActiveTab] = useState("users"); + + // 用户管理状态 + const [userSearch, setUserSearch] = useState(""); + const [userPage, setUserPage] = useState(1); + const [userLimit] = useState(10); + + // 🆕 生图记录状态 + const [imageSearch, setImageSearch] = useState(""); + const [imagePage, setImagePage] = useState(1); + const [imageStartDate, setImageStartDate] = useState(""); + const [imageEndDate, setImageEndDate] = useState(""); + const [selectedImage, setSelectedImage] = useState(null); + + // 🆕 数据分析状态 + const [selectedMonth, setSelectedMonth] = useState(() => { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; + }); + const [selectedDate, setSelectedDate] = useState(null); // 选中的日期,用于联动高峰时段 + const [hoveredBar, setHoveredBar] = useState<{ type: string; index: number; value: number; label: string } | null>(null); + + // API 数据获取 + const { data: usersData, mutate: mutateUsers } = useSWR<{ + users: User[]; + total: number; + page: number; + totalPages: number; + }>( + `/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`, + fetcher + ); + const { data: providersData, mutate: mutateProviders } = useSWR<{ providers: AiProvider[] }>("/api/admin/ai-providers", fetcher); + const { data: modelsData, mutate: mutateModels } = useSWR<{ models: AiModel[] }>("/api/admin/ai-models", fetcher); + const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[] }>("/api/admin/storage", fetcher); + const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR("/api/site-settings", fetcher); + const { data: navLinksData, mutate: mutateNavLinks } = useSWR<{ links: NavLink[] }>("/api/nav-links", fetcher); + const { data: authSettingsData, mutate: mutateAuthSettings } = useSWR("/api/auth-settings", fetcher); + + // 🆕 生图记录数据 + const imageQueryParams = new URLSearchParams({ + page: imagePage.toString(), + pageSize: "200", + ...(imageSearch && { search: imageSearch }), + ...(imageStartDate && { startDate: imageStartDate }), + ...(imageEndDate && { endDate: imageEndDate }), + }); + const { data: imagesData } = useSWR<{ + images: GeneratedImageRecord[]; + pagination: { page: number; pageSize: number; total: number; totalPages: number }; + }>(`/api/admin/images?${imageQueryParams}`, fetcher); + + // 🆕 生图数据分析 + const analyticsUrl = activeTab === "analytics" + ? `/api/admin/images/analytics?month=${selectedMonth}${selectedDate ? `&date=${selectedDate}` : ""}` + : null; + const { data: analyticsData } = useSWR(analyticsUrl, fetcher); + + // 网站设置状态 + const [siteSettings, setSiteSettings] = useState({ + siteName: "AI Chatbot", + siteLogo: "", + siteLogoDark: "", + favicon: "", + showAutoModel: true, + usageTimeEnabled: false, + usageTimeStart: "08:00", + usageTimeEnd: "20:00", + usageTimeMessage: "由壹点累了也是需要休息的", + }); + + useEffect(() => { + if (siteSettingsData) { + setSiteSettings({ + siteName: siteSettingsData.siteName || "AI Chatbot", + siteLogo: siteSettingsData.siteLogo || "", + siteLogoDark: siteSettingsData.siteLogoDark || "", + favicon: siteSettingsData.favicon || "", + showAutoModel: siteSettingsData.showAutoModel ?? true, + usageTimeEnabled: siteSettingsData.usageTimeEnabled ?? false, + usageTimeStart: siteSettingsData.usageTimeStart || "08:00", + usageTimeEnd: siteSettingsData.usageTimeEnd || "20:00", + usageTimeMessage: siteSettingsData.usageTimeMessage || "由壹点累了也是需要休息的", + }); + } + }, [siteSettingsData]); + + // 导航链接状态 + const [navLinkForm, setNavLinkForm] = useState({ + id: "", + name: "", + url: "", + order: 0, + isEnabled: true, + }); + const [showNavLinkDialog, setShowNavLinkDialog] = useState(false); + const [editingNavLink, setEditingNavLink] = useState(null); + + // 快捷短语状态 + const [questions, setQuestions] = useState([]); + const { data: questionsData, mutate: mutateQuestions } = useSWR("/api/starter-questions", fetcher); + + // 系统通知状态 + const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string }>({ + enabled: false, + title: "", + content: "", + }); + + useEffect(() => { + fetch("/api/admin/settings?key=login_notification") + .then((res) => res.json()) + .then((data) => { + if (data.setting?.value) { + setNotification(data.setting.value); + } + }); + }, []); + + const handleSaveNotification = async () => { + try { + await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: "login_notification", + value: notification, + }), + }); + toast.success("系统通知已保存"); + } catch (error) { + toast.error("保存失败"); + } + }; + + useEffect(() => { + if (questionsData) { + setQuestions(questionsData); + } + }, [questionsData]); + + const handleQuestionChange = (index: number, value: string) => { + const newQuestions = [...questions]; + newQuestions[index] = value; + setQuestions(newQuestions); + }; + + const handleAddQuestion = () => { + setQuestions([...questions, ""]); + }; + + const handleRemoveQuestion = (index: number) => { + const newQuestions = questions.filter((_, i) => i !== index); + setQuestions(newQuestions); + }; + + const handleSaveQuestions = async () => { + try { + const res = await fetch("/api/starter-questions", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(questions.filter(q => q.trim() !== "")), + }); + if (!res.ok) throw new Error("Failed"); + toast.success("快捷短语已更新"); + mutateQuestions(); + } catch (error) { + toast.error("更新失败"); + } + }; + + // 认证设置状态 + const [authSettings, setAuthSettings] = useState({ + dingTalkAppKey: "", + dingTalkAppSecret: "", + isDingTalkEnabled: false, + isRegistrationEnabled: true, + }); + + useEffect(() => { + if (authSettingsData) { + setAuthSettings({ + dingTalkAppKey: authSettingsData.dingTalkAppKey || "", + dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "", + isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, + isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, + }); + } + }, [authSettingsData]); + + // AI 提供商 & 模型状态 + const [providerForm, setProviderForm] = useState({ + id: "", + name: "", + type: "openai" as "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video", + baseUrl: "", + apiKey: "", + isEnabled: true, + }); + const [showProviderDialog, setShowProviderDialog] = useState(false); + const [editingProvider, setEditingProvider] = useState(null); + const [showApiKey, setShowApiKey] = useState(false); + + const [modelForm, setModelForm] = useState({ + id: "", + providerId: "", + modelId: "", + displayName: "", + description: "", + supportsVision: false, + supportsImageGen: false, + isEnabled: true, + isDefault: false, + sortOrder: 0, + }); + const [showModelDialog, setShowModelDialog] = useState(false); + const [editingModel, setEditingModel] = useState(null); + + // 🆕 火山引擎视频多模型表单(动态从API加载) + const [volcanoVideoModels, setVolcanoVideoModels] = useState>([]); + + // 🆕 当选择火山引擎视频Provider时,从API加载预设模型配置 + useEffect(() => { + const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); + if (selectedProvider?.type === 'volcano-video' && volcanoVideoModels.length === 0) { + // 从API获取预设模型配置(✅ 显示所有模型供管理员自定义) + fetch('/api/admin/volcano-video-models') + .then(res => res.json()) + .then(data => { + if (data.presets) { + // ✅ 显示主模型和所有子模型,允许管理员自定义显示名称和描述 + const allModels = data.presets.map((preset: any) => ({ + modelId: preset.modelId, + displayName: preset.displayName, + description: preset.description, + isSubModel: preset.isSubModel || false, // 标记是否为子模型 + })); + setVolcanoVideoModels(allModels); + } + }) + .catch(err => { + console.error('[Load Volcano Models] Error:', err); + toast.error('加载预设模型配置失败'); + }); + } + }, [modelForm.providerId, providersData?.providers, volcanoVideoModels.length]); + + // 存储配置状态 + const [storageForm, setStorageForm] = useState({ + id: "", + name: "", + type: "s3" as "s3" | "local", + endpoint: "", + region: "", + bucket: "", + accessKeyId: "", + secretAccessKey: "", + publicUrl: "", + isDefault: false, + isEnabled: true, + }); + const [showStorageDialog, setShowStorageDialog] = useState(false); + const [editingStorage, setEditingStorage] = useState(null); + + // --- 用户管理逻辑 --- + const handleUserSearch = (search: string) => { + setUserSearch(search); + setUserPage(1); // 搜索时重置到第一页 + }; + + const handleUpdateUser = async (userId: string, updates: any) => { + try { + const response = await fetch("/api/admin/users", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: userId, ...updates }), + }); + if (!response.ok) throw new Error("更新失败"); + toast.success("用户更新成功"); + mutateUsers(); + } catch (error) { + toast.error("更新用户失败"); + } + }; + + const handleDeleteUser = async (userId: string) => { + if (!confirm("确定要删除此用户吗?")) return; + try { + const response = await fetch(`/api/admin/users?id=${userId}`, { method: "DELETE" }); + if (!response.ok) throw new Error("删除失败"); + toast.success("用户删除成功"); + mutateUsers(); + } catch (error) { + toast.error("删除用户失败"); + } + }; + + // --- 网站设置逻辑 --- + const handleSaveSiteSettings = async () => { + try { + const response = await fetch("/api/site-settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(siteSettings), + }); + if (!response.ok) throw new Error("保存失败"); + toast.success("网站设置保存成功"); + mutateSiteSettings(); + } catch (error) { + toast.error("保存网站设置失败"); + } + }; + + // --- 导航链接逻辑 --- + const resetNavLinkForm = () => { + setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true }); + setEditingNavLink(null); + }; + + const handleSaveNavLink = async () => { + try { + const method = editingNavLink ? "PUT" : "POST"; + const body = editingNavLink ? { ...navLinkForm, id: editingNavLink } : navLinkForm; + + const response = await fetch("/api/nav-links", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success(editingNavLink ? "链接更新成功" : "链接创建成功"); + mutateNavLinks(); + setShowNavLinkDialog(false); + resetNavLinkForm(); + } catch (error) { + toast.error("保存导航链接失败"); + } + }; + + const handleDeleteNavLink = async (id: string) => { + if (!confirm("确定要删除此链接吗?")) return; + try { + const response = await fetch(`/api/nav-links?id=${id}`, { method: "DELETE" }); + if (!response.ok) throw new Error("删除失败"); + toast.success("链接删除成功"); + mutateNavLinks(); + } catch (error) { + toast.error("删除链接失败"); + } + }; + + // --- 认证设置逻辑 --- + const handleSaveAuthSettings = async () => { + try { + const response = await fetch("/api/auth-settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(authSettings), + }); + if (!response.ok) throw new Error("保存失败"); + toast.success("认证设置保存成功"); + mutateAuthSettings(); + } catch (error) { + toast.error("保存认证设置失败"); + } + }; + + // --- AI 提供商逻辑 --- + const resetProviderForm = () => { + setProviderForm({ + id: "", + name: "", + type: "openai", + baseUrl: "", + apiKey: "", + isEnabled: true, + }); + setEditingProvider(null); + }; + + const handleSaveProvider = async () => { + try { + const method = editingProvider ? "PUT" : "POST"; + const isNewProvider = !editingProvider; + const isVolcanoVideo = providerForm.type === 'volcano-video'; + + let requestData: any = editingProvider ? { ...providerForm, id: editingProvider } : providerForm; + if (editingProvider && !providerForm.apiKey) { + const { apiKey, ...restData } = requestData; + requestData = restData; + } + + const response = await fetch("/api/admin/ai-providers", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestData), + }); + + if (!response.ok) throw new Error("保存失败"); + + const data = await response.json(); + const providerId = data.provider?.id || editingProvider; + + toast.success(editingProvider ? "提供商更新成功" : "提供商创建成功"); + mutateProviders(); + setShowProviderDialog(false); + resetProviderForm(); + + // 🆕 如果是新创建的火山引擎视频Provider,自动添加预设模型 + if (isNewProvider && isVolcanoVideo && providerId) { + toast.info("正在自动添加预设模型..."); + + try { + const modelsResponse = await fetch("/api/admin/volcano-video-models", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerId }), + }); + + const modelsData = await modelsResponse.json(); + + if (modelsResponse.ok && modelsData.addedCount > 0) { + toast.success(`✅ 已自动添加 ${modelsData.addedCount} 个预设模型!`); + mutateModels(); // 刷新模型列表 + } + } catch (modelError) { + console.error('[Auto Add Models] Error:', modelError); + toast.warning('预设模型添加失败,请手动点击"快速添加预设模型"按钮'); + } + } + } catch (error) { + toast.error("保存提供商失败"); + } + }; + + const handleDeleteProvider = async (id: string) => { + if (!confirm("确定删除?关联模型也会被删除。")) return; + try { + const response = await fetch(`/api/admin/ai-providers?id=${id}`, { method: "DELETE" }); + if (!response.ok) throw new Error("删除失败"); + toast.success("提供商删除成功"); + mutateProviders(); + mutateModels(); + } catch (error) { + toast.error("删除提供商失败"); + } + }; + + // 快速添加火山引擎视频预设模型 + const handleQuickAddVolcanoModels = async (providerId: string) => { + if (!confirm("将为此服务商添加4个预设模型:\n- Seedance Pro (全功能)\n- Seedance Pro Fast (快速)\n- Seedance Lite I2V (图生视频)\n- Seedance Lite T2V (文生视频)\n\n继续吗?")) { + return; + } + + try { + toast.info("正在添加预设模型..."); + + const response = await fetch("/api/admin/volcano-video-models", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerId }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "添加失败"); + } + + const { addedCount, errors } = data; + + if (addedCount > 0) { + toast.success(`成功添加 ${addedCount} 个预设模型!`); + mutateModels(); // 刷新模型列表 + } + + if (errors && errors.length > 0) { + toast.warning(`${errors.length} 个模型添加失败(可能已存在)`); + } + + } catch (error: any) { + console.error('[Quick Add Models] Error:', error); + toast.error(error.message || "添加预设模型失败"); + } + }; + + // --- AI 模型逻辑 --- + const resetModelForm = () => { + setModelForm({ + id: "", + providerId: "", + modelId: "", + displayName: "", + description: "", + supportsVision: false, + supportsImageGen: false, + isEnabled: true, + isDefault: false, + sortOrder: 0, + }); + setEditingModel(null); + }; + + const handleSaveModel = async () => { + try { + // 🆕 检查是否是火山引擎视频Provider + const selectedProvider = providersData?.providers?.find(p => p.id === modelForm.providerId); + const isVolcanoVideo = selectedProvider?.type === 'volcano-video'; + + // 如果是火山引擎视频且不是编辑模式,批量创建模型 + if (isVolcanoVideo && !editingModel) { + toast.info("正在添加火山引擎视频模型(1个主模型 + 4个子模型)..."); + + const response = await fetch("/api/admin/volcano-video-models", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providerId: modelForm.providerId, + // ✅ 传递所有模型配置(管理员可以自定义显示名称和描述) + models: volcanoVideoModels, + }), + }); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.error || "批量添加失败"); + + toast.success(`✅ 成功添加 ${data.addedCount} 个模型(1个主模型 + ${data.addedCount - 1} 个子模型)!`); + mutateModels(); + setShowModelDialog(false); + resetModelForm(); + return; + } + + // 普通模型的单个创建/编辑逻辑 + const method = editingModel ? "PUT" : "POST"; + const body = editingModel ? { ...modelForm, id: editingModel } : modelForm; + const response = await fetch("/api/admin/ai-models", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success(editingModel ? "模型更新成功" : "模型创建成功"); + mutateModels(); + setShowModelDialog(false); + resetModelForm(); + } catch (error: any) { + console.error('[Save Model] Error:', error); + toast.error(error.message || "保存模型失败"); + } + }; + + const handleDeleteModel = async (id: string) => { + if (!confirm("确定删除此模型?")) return; + try { + const response = await fetch(`/api/admin/ai-models?id=${id}`, { method: "DELETE" }); + if (!response.ok) throw new Error("删除失败"); + toast.success("模型删除成功"); + mutateModels(); + } catch (error) { + toast.error("删除模型失败"); + } + }; + + // --- 存储配置逻辑 --- + const resetStorageForm = () => { + setStorageForm({ + id: "", + name: "", + type: "s3", + endpoint: "", + region: "", + bucket: "", + accessKeyId: "", + secretAccessKey: "", + publicUrl: "", + isDefault: false, + isEnabled: true, + }); + setEditingStorage(null); + }; + + const handleSaveStorage = async () => { + try { + const method = editingStorage ? "PUT" : "POST"; + const body = editingStorage ? { ...storageForm, id: editingStorage } : storageForm; + const response = await fetch("/api/admin/storage", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success(editingStorage ? "存储配置更新成功" : "存储配置创建成功"); + mutateStorage(); + setShowStorageDialog(false); + resetStorageForm(); + } catch (error) { + toast.error("保存存储配置失败"); + } + }; + + const handleDeleteStorage = async (id: string) => { + if (!confirm("确定删除此存储配置?")) return; + try { + const response = await fetch(`/api/admin/storage?id=${id}`, { method: "DELETE" }); + if (!response.ok) throw new Error("删除失败"); + toast.success("存储配置删除成功"); + mutateStorage(); + } catch (error) { + toast.error("删除存储配置失败"); + } + }; + + return ( +
+ + + + + 用户管理 + + + + 生图记录 + + + + 数据分析 + + + + 网站设置 + + + + 导航设置 + + + + 快捷短语 + + + + 认证设置 + + + + 系统通知 + + + + AI 模型 + + + + 存储配置 + + + + {/* 用户管理 */} + + + + {/* 搜索框 */} +
+ handleUserSearch(e.target.value)} + className="max-w-md" + /> +
+ + {/* 用户列表 */} +
+ {usersData?.users?.map((user) => ( +
+
+
{user.name || user.email}
+
+ 邮箱: {user.email} +
+
+ 角色: {ROLE_LABELS[user.role] ?? user.role} | 状态: {user.isActive ? "已激活" : "未激活"} +
+
+
+ + + +
+
+ ))} +
+ + {/* 分页控件 */} + {usersData && usersData.totalPages > 1 && ( +
+
+ 显示 {((userPage - 1) * userLimit) + 1} - {Math.min(userPage * userLimit, usersData.total)} 条,共 {usersData.total} 条 +
+
+ {/* 首页按钮 */} + + + {/* 上一页按钮 */} + + + {/* 页码显示和跳转 */} +
+ 第 + { + const page = Number.parseInt(e.target.value, 10); + if (page >= 1 && page <= usersData.totalPages) { + setUserPage(page); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const page = Number.parseInt(e.currentTarget.value, 10); + if (page >= 1 && page <= usersData.totalPages) { + setUserPage(page); + } else { + e.currentTarget.value = userPage.toString(); + } + } + }} + className="w-16 h-8 text-center" + /> + / {usersData.totalPages} 页 +
+ + {/* 下一页按钮 */} + + + {/* 末页按钮 */} + +
+
+ )} +
+
+
+ + {/* 🆕 生图记录 */} + + + + 所有生图记录 + 查看所有用户的 AI 生图历史记录 + + + {/* 筛选条件 */} +
+
+ +
+ + { setImageSearch(e.target.value); setImagePage(1); }} + className="pl-9" + /> +
+
+
+ + { setImageStartDate(e.target.value); setImagePage(1); }} + /> +
+
+ + { setImageEndDate(e.target.value); setImagePage(1); }} + /> +
+
+ +
+
+ + {/* 统计信息 */} +
+ 共 {imagesData?.pagination.total || 0} 条记录 +
+ + {/* 图片网格 */} +
+ {imagesData?.images?.map((img) => ( +
setSelectedImage(img)} + > +
+ {img.prompt +
+
+ {img.userName || img.userEmail?.split("@")[0]} +
+
+ {new Date(img.createdAt).toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })} +
+
+ ))} +
+ + {/* 分页 */} + {imagesData && imagesData.pagination.totalPages > 1 && ( +
+
+ 第 {imagesData.pagination.page} / {imagesData.pagination.totalPages} 页 +
+
+ + + + +
+
+ )} +
+
+ + {/* 图片详情弹窗 */} + !open && setSelectedImage(null)}> + + + 图片详情 + + {selectedImage && ( +
+
+ {selectedImage.prompt +
+
+
+ +

{selectedImage.userName || selectedImage.userEmail}

+
+
+ +

{new Date(selectedImage.createdAt).toLocaleString("zh-CN")}

+
+
+ +

{selectedImage.imageSize || "未知"}

+
+
+ +

{selectedImage.modelId || "未知"}

+
+
+ +

{selectedImage.prompt || "无"}

+
+
+
+ )} +
+
+
+ + {/* 🆕 数据分析 */} + + {/* 概览卡片 */} +
+ + +
+
+ +
+
+

{analyticsData?.overview.totalImages || 0}

+

总生图数

+
+
+
+
+ + +
+
+ +
+
+

{analyticsData?.overview.totalUsers || 0}

+

总用户数

+
+
+
+
+ + +
+
+ +
+
+

{analyticsData?.overview.todayImages || 0}

+

今日生图

+
+
+
+
+ + +
+
+
+ +
+
+

{analyticsData?.overview.monthImages || 0}

+

+ {selectedMonth === `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}` + ? "本月生图" + : `${selectedMonth} 生图`} +

+
+
+ setSelectedMonth(e.target.value)} + className="w-[140px] h-8 text-xs" + /> +
+
+
+
+ +
+ {/* 用户排行 */} + + + 用户生图排行 (Top 20) + + +
+ {analyticsData?.userStats?.map((user, index) => ( +
+
+ + {index + 1} + + + {user.userName || user.userEmail?.split("@")[0]} + +
+ {user.imageCount} +
+ ))} +
+
+
+ + {/* 图片尺寸分布 */} + + + 图片尺寸分布 + + +
+ {analyticsData?.sizeStats?.map((stat) => { + const total = analyticsData.overview.totalImages || 1; + const percentage = ((stat.count / total) * 100).toFixed(1); + return ( +
+
+ {stat.imageSize || "未知"} + {stat.count} ({percentage}%) +
+
+
+
+
+ ); + })} +
+ + + + {/* 每日趋势 */} + + + 最近30天趋势 + {selectedDate && ( + + )} + + +
+ {analyticsData?.dailyStats?.map((stat, index) => { + const maxCount = Math.max(...(analyticsData.dailyStats?.map(s => s.count) || [1])); + const height = (stat.count / maxCount) * 100; + const isHovered = hoveredBar?.type === "daily" && hoveredBar?.index === index; + const isSelected = selectedDate === stat.date; + return ( +
setHoveredBar({ type: "daily", index, value: stat.count, label: stat.date })} + onMouseLeave={() => setHoveredBar(null)} + onClick={() => setSelectedDate(selectedDate === stat.date ? null : stat.date)} + > + {isHovered && ( +
+ {stat.date}: {stat.count} 张 +
点击查看当天高峰时段
+
+ )} +
+ ); + })} +
+
+ {analyticsData?.dailyStats?.[0]?.date} + {analyticsData?.dailyStats?.at(-1)?.date} +
+
+
+ + {/* 高峰时段 */} + + + + 生图高峰时段 + {selectedDate && ( + + {selectedDate} + + )} + + {!selectedDate && ( + 点击左侧日期柱形查看当天数据 + )} + + +
+ {Array.from({ length: 24 }, (_, hour) => { + const stat = analyticsData?.hourlyStats?.find(s => s.hour === hour); + const count = stat?.count || 0; + const maxCount = Math.max(...(analyticsData?.hourlyStats?.map(s => s.count) || [1]), 1); + const height = (count / maxCount) * 100; + const isHovered = hoveredBar?.type === "hourly" && hoveredBar?.index === hour; + return ( +
setHoveredBar({ type: "hourly", index: hour, value: count, label: `${hour}:00` })} + onMouseLeave={() => setHoveredBar(null)} + > + {isHovered && ( +
+ {hour}:00-{hour + 1}:00: {count} 张 +
+ )} +
+ ); + })} +
+
+ 0:00 + 12:00 + 23:00 +
+
+
+ + {/* 月度统计 */} + + + 月度统计 + + +
+ {analyticsData?.monthlyStats?.slice(-6).map((stat) => ( +
+

{stat.count}

+

{stat.month}

+
+ ))} +
+
+
+
+ + + {/* 网站设置 */} + + + +
+ + setSiteSettings({ ...siteSettings, siteName: e.target.value })} + placeholder="输入网站名称" + /> +
+
+ + setSiteSettings({ ...siteSettings, siteLogo: e.target.value })} + placeholder="https://example.com/logo.png" + /> +
+
+ + setSiteSettings({ ...siteSettings, siteLogoDark: e.target.value })} + placeholder="https://example.com/logo-dark.png" + /> +
+
+ + setSiteSettings({ ...siteSettings, favicon: e.target.value })} + placeholder="https://example.com/favicon.ico" + /> +
+
+
+ +
+ 开启后,用户可以选择"智能自动"模型,系统将根据场景智能选择最佳模型 +
+
+ setSiteSettings({ ...siteSettings, showAutoModel: checked })} + /> +
+ +
+
+
+ +
+ 开启后,仅在指定时间段内允许使用系统,超出时间段输入框将被禁用 +
+
+ setSiteSettings({ ...siteSettings, usageTimeEnabled: checked })} + /> +
+ {siteSettings.usageTimeEnabled && ( + <> +
+
+ + +
+
+ + +
+
+
+ +