commit 53ddb7e585902b7b5ceae59f2b2e34abd87eadf4 Author: Your Name Date: Wed Dec 17 20:15:18 2025 +0800 Initial commit - clean repository 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 && ( + <> +
+
+ + +
+
+ + +
+
+
+ +