diff --git a/CLAUDE.md b/CLAUDE.md index f0385a574bc9..d683dbf03d2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,104 @@ This is an AI API gateway/proxy built with Go. It aggregates 40+ upstream AI providers (OpenAI, Claude, Gemini, Azure, AWS Bedrock, etc.) behind a unified API, with user management, billing, rate limiting, and an admin dashboard. +## Common Development Commands + +### Backend (Go) + +```bash +# Run backend development server +go run main.go + +# Run with debug mode +GIN_MODE=debug DEBUG=true go run main.go + +# Run with custom port +PORT=8080 go run main.go + +# Run tests +go test ./... + +# Run specific test +go test ./relay/channel -v -run TestClaude + +# Run specific test file +go test ./relay/channel/api_request_test.go -v +``` + +### Frontend (React + Vite) + +```bash +# Install dependencies (uses Bun) +cd web && bun install + +# Run development server (runs on http://localhost:5173) +cd web && bun run dev + +# Build for production +cd web && bun run build + +# Run ESLint +cd web && bun run eslint + +# Fix ESLint issues +cd web && bun run eslint:fix + +# Format code with Prettier +cd web && bun run lint:fix + +# i18n tools +cd web && bun run i18n:extract # Extract new translation keys +cd web && bun run i18n:sync # Sync translations +cd web && bun run i18n:lint # Lint translation files +``` + +### Full Stack Development + +```bash +# Using makefile +make build-frontend # Build frontend assets +make start-backend # Start backend server +make all # Build frontend + start backend (parallel) + +# Manually (two terminals) +# Terminal 1: Frontend dev server +cd web && bun run dev + +# Terminal 2: Backend dev server +go run main.go +``` + +### Docker Development + +```bash +# Start full stack with docker-compose +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down +``` + +### Environment Configuration + +Create a `.env` file based on `.env.example`: + +```bash +cp .env.example .env +# Then edit .env with your settings +``` + +Key environment variables: +- `SQL_DSN` - Database connection string (default: SQLite in `/data`) +- `REDIS_CONN_STRING` - Redis connection for cache +- `SESSION_SECRET` - Required for multi-machine deployments +- `CRYPTO_SECRET` - Required when using Redis +- `STREAMING_TIMEOUT` - Streaming timeout in seconds (default: 300) +- `DEBUG` - Enable debug mode +- `GIN_MODE` - Gin mode (`debug` or `release`) + ## Tech Stack - **Backend**: Go 1.22+, Gin web framework, GORM v2 ORM @@ -24,6 +122,8 @@ service/ — Business logic model/ — Data models and DB access (GORM) relay/ — AI API relay/proxy with provider adapters relay/channel/ — Provider-specific adapters (openai/, claude/, gemini/, aws/, etc.) + relay/common/ — Relay utilities and RelayInfo struct + relay/helper/ — Stream scanner, billing helpers, etc. middleware/ — Auth, rate limiting, CORS, logging, distribution setting/ — Configuration management (ratio, model, operation, system, performance) common/ — Shared utilities (JSON, crypto, Redis, env, rate-limit, etc.) @@ -37,6 +137,26 @@ web/ — React frontend web/src/i18n/ — Frontend internationalization (i18next, zh/en/fr/ru/ja/vi) ``` +### Relay System + +The relay system is the core of the gateway, handling requests from clients and forwarding them to upstream AI providers: + +- **Adaptor Interface** (`relay/channel/adapter.go`): All channel adapters implement this interface with methods like `Init`, `GetRequestURL`, `SetupRequestHeader`, `ConvertOpenAIRequest`, `DoRequest`, `DoResponse`, etc. +- **RelayInfo** (`relay/common/relay_info.go`): Contains all context for a single relay request - user/token info, channel metadata, pricing, billing session, request conversion chain. +- **Request Format Conversion**: Supports multiple relay formats - OpenAI, Claude Messages, Gemini, Responses, Rerank, Embedding, Audio, Image, Realtime, Task (async). Adapters convert between formats as needed. +- **Stream Handling**: `relay/helper/stream_scanner.go` handles streaming responses with configurable buffer size. +- **TaskAdaptor Interface**: For async task-based providers (Midjourney, Suno, etc.), implements polling-based task lifecycle management with billing hooks. + +### Request Flow + +1. Router receives request → Middleware (auth, rate limit, distribution) +2. Controller parses request → Validates token/channel +3. Service layer handles business logic (quota, billing) +4. Relay layer calls appropriate channel adaptor +5. Adaptor converts request format and forwards to upstream +6. Response is converted back and sent to client +7. Billing session settles quota (pre-consume -> adjust delta -> final settle) + ## Internationalization (i18n) ### Backend (`i18n/`) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 000000000000..63477b6a7938 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,65 @@ +# new-api Docker Compose for Local Development +# +# Quick Start: +# docker-compose -f docker-compose-dev.yml up -d +# +# Purpose: +# This file contains only external services (database, cache) needed for development. +# The main new-api application runs on your local machine. +# +# Services: +# - postgres: PostgreSQL 17 database +# - redis: Redis cache server +# - redis-commander: Redis web UI (optional, for viewing cache) +# - pgadmin4: PostgreSQL web UI (optional, for database management) +# +version: '3.8' +name: new-api-dev + +services: + postgres: + image: postgres:17-alpine + container_name: new-api-postgres-dev + restart: unless-stopped + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: 123456 + POSTGRES_DB: new-api + POSTGRES_INITDB_ARGS: "-E UTF8" + TZ: Asia/Shanghai + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U newapi"] + interval: 5s + timeout: 5s + retries: 5 + + # Redis Cache Server + redis: + image: bitnamilegacy/redis:8.0 + container_name: new-api-redis-dev + restart: unless-stopped + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + postgres_data: + driver: local + redis_data: + driver: local + +networks: + default: + name: new-api-dev-network diff --git "a/docs/learn_docs/01-\351\241\271\347\233\256\346\246\202\350\277\260.md" "b/docs/learn_docs/01-\351\241\271\347\233\256\346\246\202\350\277\260.md" new file mode 100644 index 000000000000..7b848c280adf --- /dev/null +++ "b/docs/learn_docs/01-\351\241\271\347\233\256\346\246\202\350\277\260.md" @@ -0,0 +1,140 @@ +# 项目概述 + +## 什么是 new-api + +new-api 是一个基于 Go 语言的下一代大模型网关和 AI 资产管理系统。它将 40+ 个上游 AI 提供商(OpenAI、Claude、Gemini、Azure、AWS Bedrock 等)整合在一个统一的 API 接口下,提供用户管理、计费、限流和管理后台等功能。 + +## 核心功能 + +### 1. 多提供商聚合 +```mermaid +graph TD + Client[客户端请求] --> Gateway[new-api 网关] + Gateway --> OpenAI[OpenAI] + Gateway --> Claude[Claude] + Gateway --> Gemini[Gemini] + Gateway --> Azure[Azure] + Gateway --> AWS[AWS Bedrock] + Gateway --> DeepSeek[DeepSeek] + Gateway --> Moonshot[Moonshot] + Gateway --> MiniMax[MiniMax] + Gateway --> Ali[通义千问] + Gateway --> Zhipu[智谱] + Gateway --> Others[其他30+提供商] +``` + +### 2. 多 API 格式支持 +- **OpenAI 兼容格式** - `/v1/chat/completions` +- **OpenAI Responses 格式** - `/v1/responses` +- **Claude Messages 格式** - `/v1/messages` +- **Gemini 格式** - `/v1beta/models/...` +- **实时对话格式** - `/v1/realtime` (WebSocket) + +### 3. 统一计费系统 +支持多种计费模式: +- 按使用量计费(tokens) +- 按时间计费(音频/视频) +- 按图片数量计费 +- 按分辨率/时长组合计费 +- 任务预付费 + 完成结算 + +### 4. 智能路由策略 +- **通道加权随机** - 按权重分配请求 +- **自动重试** - 失败时自动切换通道 +- **用户级模型限流** - 防止单用户占用过多资源 +- **通道亲和性** - 会话级别的通道粘性 + +### 5. 异步任务系统 +支持异步任务提供商: +- Midjourney - 图像生成 +- Suno - 音乐生成 +- Sora/Kling - 视频生成 +- Jimeng - 即梦绘画 + +## 技术栈 + +| 层级 | 技术选型 | +|-------|----------| +| **后端框架** | Go 1.25.1, Gin Web Framework | +| **数据库** | SQLite, MySQL >= 5.7.8, PostgreSQL >= 9.6 | +| **缓存** | Redis (go-redis) + 内存缓存 | +| **ORM** | GORM v2 | +| **认证** | JWT, WebAuthn/Passkeys, OAuth (GitHub, Discord, OIDC) | +| **前端框架** | React 18, Vite | +| **UI 组件库** | Semi Design (@douyinfe/semi-ui) | +| **前端包管理器** | Bun | +| **国际化** | go-i18n (后端), i18next (前端) | + +## 项目定位 + +### 目标用户 +- 企业用户 - 需要 API 聚合和统一计费 +- 开发者 - 需要简单接入多个 AI 模型 +- 分发商 - 需要管理和计费终端用户 + +### 核心价值 +1. **统一接入** - 一次接入 40+ AI 提供商 +2. **成本可控** - 灵活的计费策略和限流 +3. **高可用** - 自动重试和故障转移 +4. **易管理** - 完整的用户、通道、日志管理后台 + +## 项目结构概览 + +``` +new-api/ +├── router/ # HTTP 路由层 +├── controller/ # 请求处理层 +├── service/ # 业务逻辑层 +├── model/ # 数据模型和数据库访问 +├── relay/ # AI API 转发核心 +│ └── channel/ # 各提供商适配器 +├── middleware/ # 中间件(认证、限流等) +├── setting/ # 配置管理 +├── common/ # 通用工具 +├── dto/ # 数据传输对象 +├── constant/ # 常量定义 +├── types/ # 类型定义 +├── i18n/ # 后端国际化 +├── oauth/ # OAuth 提供商实现 +├── pkg/ # 内部包 +└── web/ # React 前端 +``` + +## 快速上手 + +### 1. 环境要求 +- Go 1.25.1+ +- Node.js 18+ (前端开发) +- Bun (前端包管理) +- SQLite/MySQL/PostgreSQL + +### 2. 启动开发环境 +```bash +# 后端 +go run main.go + +# 前端(在 web/ 目录) +cd web +bun install +bun run dev +``` + +### 3. 核心配置 +```bash +# 数据库连接 +SQL_DSN="user:password@tcp(localhost:3306)/newapi" + +# Redis 连接(可选) +REDIS_CONN_STRING="redis://localhost:6379" + +# 会话密钥(多机部署必填) +SESSION_SECRET="your-secret-key" + +# 加密密钥(Redis 必填) +CRYPTO_SECRET="your-crypto-secret" +``` + +## 后续阅读 + +- [02-架构详解](./02-架构详解.md) - 深入了解系统架构 +- [03-二次开发指南](./03-二次开发指南.md) - 学习如何扩展功能 diff --git "a/docs/learn_docs/02-\346\236\266\346\236\204\350\257\246\350\247\243.md" "b/docs/learn_docs/02-\346\236\266\346\236\204\350\257\246\350\247\243.md" new file mode 100644 index 000000000000..685466d97e5e --- /dev/null +++ "b/docs/learn_docs/02-\346\236\266\346\236\204\350\257\246\350\247\243.md" @@ -0,0 +1,594 @@ +# 架构详解 + +## 整体架构 + +new-api 采用经典的分层架构:**Router → Controller → Service → Model** + +```mermaid +graph TB + subgraph "客户端层" + Client[Web 客户端] + API[API 客户端] + end + + subgraph "路由层 (router/)" + Router[Gin Router] + API_Router[/v1/api] + Relay_Router[/v1/relay] + Dashboard_Router[/dashboard] + Web_Router[前端路由] + end + + subgraph "中间件层 (middleware/)" + Auth[认证中间件] + RateLimit[限流中间件] + Distribute[分发中间件] + CORS[CORS 中间件] + Logger[日志中间件] + end + + subgraph "控制器层 (controller/)" + RelayCtrl[Relay 控制器] + UserCtrl[User 控制器] + ChannelCtrl[Channel 控制器] + TokenCtrl[Token 控制器] + end + + subgraph "业务逻辑层 (service/)" + ChannelSelect[通道选择服务] + Billing[计费服务] + Convert[格式转换服务] + BillingSession[计费会话服务] + TokenCounter[Token 计数服务] + end + + subgraph "数据层 (model/)" + User[User 模型] + Channel[Channel 模型] + Token[Token 模型] + Log[Log 模型] + Task[Task 模型] + end + + subgraph "转发层 (relay/)" + Adaptor[适配器接口] + OpenAI[OpenAI 适配器] + Claude[Claude 适配器] + Gemini[Gemini 适配器] + TaskAdaptor[任务适配器] + end + + subgraph "存储层" + DB[(数据库)] + Redis[(Redis 缓存)] + Disk[磁盘缓存] + end + + Client --> Router + API --> Router + + Router --> CORS + CORS --> Logger + Logger --> Auth + Auth --> RateLimit + RateLimit --> Distribute + + Distribute --> RelayCtrl + Distribute --> UserCtrl + Distribute --> ChannelCtrl + Distribute --> TokenCtrl + + RelayCtrl --> Convert + RelayCtrl --> ChannelSelect + + ChannelSelect --> Channel + Billing --> User + Billing --> Token + Billing --> Log + + Convert --> Adaptor + Adaptor --> OpenAI + Adaptor --> Claude + Adaptor --> Gemini + + OpenAI --> DB + Claude --> DB + Gemini --> DB + + DB --> Redis + DB --> Disk +``` + +## 请求处理流程 + +### 1. AI 请求转发流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant R as Router + participant M as Middleware + participant Ctrl as Controller + participant S as Service + participant R as Relay + participant P as 提供商 + participant D as 数据库 + participant Cache as Redis + + C->>R: POST /v1/chat/completions + R->>M: CORS 检查 + M->>M: 请求 ID 生成 + M->>Cache: 限流检查 + M->>D: Token 认证 + D-->>M: Token 信息 + M->>Ctrl: Relay(c, format) + Ctrl->>S: 通道选择 + S->>D: 获取可用通道 + D-->>S: 通道列表 + S->>S: 负载均衡 + S->>R: RelayHelper(c, info) + R->>R: 格式转换 + R->>R: 请求构建 + R->>P: HTTP 请求 + P-->>R: 响应 + R->>R: 响应转换 + R->>S: Token 计数 + S->>D: 更新配额 + R-->>C: 响应数据 +``` + +### 2. 通道选择策略 + +```mermaid +graph TD + Start[开始选择通道] --> Filter{通道过滤} + Filter --> |有指定通道| Direct[直接使用指定通道] + Filter --> |无指定通道| CheckCache{缓存检查} + + CheckCache --> |有缓存亲和性| UseCache[使用缓存通道] + CheckCache --> |无缓存| GetAvailable[获取可用通道] + + GetAvailable --> CheckWeight{是否有权重配置} + CheckWeight --> |有权重| Weighted[加权随机选择] + CheckWeight --> |无权重| Random[随机选择] + + Direct --> Verify{通道可用性检查} + UseCache --> Verify + Weighted --> Verify + Random --> Verify + + Verify --> |可用| Selected[选中通道] + Verify --> |不可用| Retry{重试次数 < 最大值?} + Retry --> |是| GetAvailable + Retry --> |否| Error[返回错误] + + Selected --> CacheUpdate[更新缓存亲和性] + CacheUpdate --> End[返回通道] +``` + +## 中间件架构 + +中间件按以下顺序执行: + +```mermaid +graph LR + Request[请求] --> CORS[CORS] + CORS --> Decompress[解压缩] + Decompress --> BodyCleanup[请求体清理] + BodyCleanup --> Stats[统计] + Stats --> RouteTag[路由标签] + RouteTag --> SystemPerformance[系统性能检查] + SystemPerformance --> Auth[认证] + Auth --> ModelRateLimit[模型请求限流] + ModelRateLimit --> Distribute[分发中间件] + + Distribute --> Controller[控制器] + + Controller --> Distribute + Distribute --> Response[响应] +``` + +### 关键中间件说明 + +| 中间件 | 文件 | 功能 | +|---------|------|------| +| **CORS** | `cors.go` | 跨域资源共享配置 | +| **认证** | `auth.go` | Token 和用户认证 | +| **分发** | `distributor.go` | 通道选择和负载均衡 | +| **限流** | `rate-limit.go` | 全局请求限流 | +| **模型限流** | `model-rate-limit.go` | 用户级模型限流 | +| **性能检查** | `performance.go` | 系统性能保护 | +| **请求 ID** | `request-id.go` | 生成唯一请求 ID | +| **i18n** | `i18n.go` | 国际化上下文设置 | +| **日志** | `logger.go` | 请求/响应日志记录 | + +## Relay 转发系统架构 + +### 适配器接口设计 + +```mermaid +classDiagram + class Adaptor { + +Init(info) + +GetRequestURL(info) string + +SetupRequestHeader(c, header, info) + +ConvertOpenAIRequest(c, info, req) + +ConvertClaudeRequest(c, info, req) + +ConvertGeminiRequest(c, info, req) + +DoRequest(c, info, body) + +DoResponse(c, resp, info) + +GetModelList() string[] + +GetChannelName() string + } + + class OpenAIAdaptor { + ChannelType + ResponseFormat + } + + class ClaudeAdaptor { + ChannelType + ResponseFormat + } + + class GeminiAdaptor { + ChannelType + ResponseFormat + } + + Adaptor <|.. OpenAIAdaptor + Adaptor <|.. ClaudeAdaptor + Adaptor <|.. GeminiAdaptor +``` + +### Relay 模式与适配 + +```mermaid +graph TB + Request[客户端请求] --> Detect{格式检测} + + Detect --> |x-api-key +
anthropic-version| Claude[Claude 格式] + Detect --> |x-goog-api-key| Gemini[Gemini 格式] + Detect --> |默认| OpenAI[OpenAI 格式] + + Claude --> ClaudeAdaptor[Claude 适配器] + Gemini --> GeminiAdaptor[Gemini 适配器] + OpenAI --> OpenAIAdaptor[OpenAI 适配器] + + ClaudeAdaptor --> Convert[Claude -> OpenAI
格式转换] + GeminiAdaptor --> Convert + + Convert --> Channel{通道类型} + + Channel --> |OpenAI| OAI_Provider[OpenAI 提供商] + Channel --> |Claude| Anthropic[Anthropic 提供商] + Channel --> |Gemini| Google[Google 提供商] + Channel --> |Azure| Azure[Azure 提供商] + Channel --> |AWS| AWS[AWS Bedrock] + Channel --> |DeepSeek| DeepSeek[DeepSeek 提供商] + Channel --> |...| Others[其他提供商] + + OAI_Provider --> ProviderAPI[提供商 API] + Anthropic --> ProviderAPI + Google --> ProviderAPI + Azure --> ProviderAPI + AWS --> ProviderAPI + DeepSeek --> ProviderAPI + Others --> ProviderAPI + + ProviderAPI --> Response[响应] + Response --> BackConvert[格式回转] + BackConvert --> Client[客户端] +``` + +## 数据库架构 + +### 主要数据表 + +```mermaid +erDiagram + USER ||--o{ TOKEN : "拥有" + USER ||--o{ CHANNEL : "管理" + CHANNEL ||--o{ TOKEN : "可用" + USER ||--o{ LOG : "产生" + TOKEN ||--o{ LOG : "产生" + USER ||--o{ TASK : "提交" + CHANNEL ||--o{ TASK : "执行" + GROUP ||--o{ USER : "包含" + GROUP ||--o{ TOKEN : "限制" + CHANNEL ||--o{ CHANNEL_AFFINITY : "亲和性" + + USER { + int id PK + string username + string password + string role + string status + int quota + datetime created_at + } + + CHANNEL { + int id PK + int type + string key + string base_url + string models + int priority + int weight + string status + } + + TOKEN { + int id PK + string key + int user_id FK + string name + int group_id + int quota + string models + datetime expire_time + } + + LOG { + int id PK + int user_id FK + int token_id FK + string model + int prompt_tokens + int completion_tokens + datetime created_at + } + + TASK { + int id PK + int user_id FK + int channel_id FK + string action + string status + string result + datetime created_at + } + + GROUP { + int id PK + string name + string models + int max_quota + } + + CHANNEL_AFFINITY { + int id PK + int user_id FK + int token_id FK + int channel_id FK + datetime created_at + } +``` + +## 缓存架构 + +### 三级缓存设计 + +```mermaid +graph LR + Request[请求] --> L1[L1: 内存缓存] + L1 --> |未命中| L2[L2: Redis 缓存] + L2 --> |未命中| L3[L3: 数据库] + + L3 --> L2 + L2 --> L1 + + L1 --> |命中| Response[返回数据] + + subgraph "缓存内容" + Channels[通道配置] + Models[模型列表] + Ratios[计费比例] + Users[用户信息] + Tokens[Token 信息] + end + + L2 -.缓存.-> Channels + L2 -.缓存.-> Models + L2 -.缓存.-> Ratios + L2 -.缓存.-> Users + L2 -.缓存.-> Tokens +``` + +## 异步任务系统架构 + +### 任务生命周期 + +```mermaid +stateDiagram-v2 + [*] --> Submitted: 提交任务 + Submitted --> Queued: 入队 + + Queued --> Processing: 开始处理 + Processing --> Running: 提供商返回任务ID + + Running --> Polling: 开始轮询 + Polling --> Running: 继续轮询 + Polling --> Success: 任务成功 + Polling --> Failed: 任务失败 + Polling --> Timeout: 超时 + + Success --> Billing: 完成结算 + Failed --> Billing: 失败结算 + Timeout --> Billing: 超时结算 + + Billing --> [*] +``` + +### 任务适配器接口 + +```mermaid +classDiagram + class TaskAdaptor { + +Init(info) + +ValidateRequestAndSetAction(c, info) + +EstimateBilling(c, info) + +BuildRequestURL(info) + +BuildRequestHeader(c, req, info) + +BuildRequestBody(c, info) + +DoRequest(c, info, body) + +DoResponse(c, resp, info) + +FetchTask(baseUrl, key, body) + +ParseTaskResult(respBody) + +AdjustBillingOnComplete(task, result) + +GetModelList() + +GetChannelName() + } + + class MidjourneyAdapter { + ParseAction(path) + BuildFormData(params) + ParseTaskResult(body) + } + + class SunoAdapter { + ParseAction(path) + BuildRequestJSON(params) + ParseTaskResult(body) + } + + class VideoAdapter { + ParseAction(path) + BuildRequestJSON(params) + ParseTaskResult(body) + } + + TaskAdaptor <|.. MidjourneyAdapter + TaskAdaptor <|.. SunoAdapter + TaskAdaptor <|.. VideoAdapter +``` + +## 计费系统架构 + +### 计费流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant C as Controller + participant S as BillingService + participant D as 数据库 + participant R as Redis + participant A as Adapter + + U->>C: 发送请求 + C->>S: PreCheckBalance() + S->>D: 查询用户配额 + D-->>S: 当前配额 + S->>R: 执行请求 + + R-->>S: 返回使用信息 + S->>S: CalculateCost() + + alt 实时计费 + S->>D: 扣除配额 + S->>R: 记录日志 + end + + alt 任务计费 + S->>D: EstimateBilling() + S->>D: 预扣配额 + Note over S,D: 任务完成后结算差额 + end + + S-->>C: 处理完成 + C-->>U: 返回响应 +``` + +### 计费会话管理 + +```mermaid +graph TB + Start[请求开始] --> CreateSession{会话存在?} + CreateSession --> |否| NewSession[创建计费会话] + CreateSession --> |是| GetSession[获取现有会话] + NewSession --> Cache[缓存会话] + GetSession --> Cache + + Cache --> Process[处理请求] + Process --> Accumulate[累积使用量] + + Accumulate --> Stream{是否流式?} + Stream --> |是| CacheUpdate[更新缓存] + Stream --> |否| Finalize[最终结算] + + CacheUpdate --> CheckDone{请求完成?} + CheckDone --> |否| Process + CheckDone --> |是| Finalize + + Finalize --> UpdateDB[更新数据库] + Finalize --> DeleteCache[删除会话缓存] + UpdateDB --> End[结束] + DeleteCache --> End +``` + +## WebSocket 实时通信 + +### OpenAI Realtime 处理流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant R as Router + participant W as WebSocket Upgrader + participant A as OpenAI Realtime Handler + participant P as OpenAI Realtime API + + C->>R: GET /v1/realtime
Sec-WebSocket-Protocol + R->>W: Upgrade to WebSocket + W-->>C: WebSocket Connection + + C->>A: session.update + A->>P: 转发配置 + + C->>A: input.audio_buffer
音频数据 + A->>P: 转发音频 + + P-->>A: conversation.item.audio
合成音频 + A-->>C: conversation.item.audio
播放音频 + + P-->>A: conversation.item.created
对话消息 + A-->>C: conversation.item.created
显示消息 + + P-->>A: input_audio_buffer.speech_started + A-->>C: input_audio_buffer.speech_started
语音开始 + + C->>A: input_audio_buffer.speech_stopped + A->>P: 转发语音停止 +``` + +## 错误处理架构 + +```mermaid +graph TB + Request[请求] --> Try{尝试执行} + Try --> Success{成功?} + Success --> |是| Response[返回响应] + Success --> |否| ErrorType{错误类型} + + ErrorType --> |系统错误| SystemError[系统内部错误] + ErrorType --> |上游错误| UpstreamError[上游 API 错误] + ErrorType --> |认证错误| AuthError[认证/授权错误] + ErrorType --> |限流错误| RateError[限流错误] + + SystemError --> Log500[记录 500 日志] + UpstreamError --> Wrap[包装为统一错误格式] + AuthError --> Wrap + RateError --> Wrap + + Wrap --> ResponseFormat{响应格式} + ResponseFormat --> |OpenAI| OAI_Format[OpenAI 错误格式] + ResponseFormat --> |Claude| Claude_Format[Claude 错误格式] + ResponseFormat --> |Gemini| Gemini_Format[Gemini 错误格式] + + Log500 --> OAI_Format + OAI_Format --> Client[返回客户端] + Claude_Format --> Client + Gemini_Format --> Client +``` diff --git "a/docs/learn_docs/03-\344\272\214\346\254\241\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/learn_docs/03-\344\272\214\346\254\241\345\274\200\345\217\221\346\214\207\345\215\227.md" new file mode 100644 index 000000000000..4d1703576790 --- /dev/null +++ "b/docs/learn_docs/03-\344\272\214\346\254\241\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -0,0 +1,744 @@ +# 二次开发指南 + +## 目录 + +1. [添加新的 AI 提供商通道](#添加新的-ai-提供商通道) +2. [添加新的异步任务提供商](#添加新的异步任务提供商) +3. [添加新的 API 路由](#添加新的-api-路由) +4. [扩展计费系统](#扩展计费系统) +5. [添加中间件](#添加中间件) +6. [自定义配置选项](#自定义配置选项) +7. [测试指南](#测试指南) + +--- + +## 添加新的 AI 提供商通道 + +### 步骤 1: 创建适配器目录 + +在 `relay/channel/` 下创建新的提供商目录,例如 `relay/channel/myprovider/` + +```bash +mkdir relay/channel/myprovider +touch relay/channel/myprovider/adaptor.go +touch relay/channel/myprovider/constants.go +touch relay/channel/myprovider/dto.go +``` + +### 步骤 2: 定义常量和 DTO + +**constants.go** +```go +package myprovider + +const ( + ChannelName = "MyProvider" + // 添加模型列表 + ModelGPT4 = "gpt-4" + ModelGPT35Turbo = "gpt-3.5-turbo" +) +``` + +**dto.go** - 定义提供商特定的请求/响应结构 +```go +package myprovider + +type MyProviderRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + Stream bool `json:"stream"` + // ... 其他提供商特定字段 +} + +type MyProviderResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} +``` + +### 步骤 3: 实现适配器接口 + +**adaptor.go** - 实现完整的适配器接口 + +```go +package myprovider + +import ( + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" + "net/http" + "io" +) + +type Adaptor struct { + ChannelType int + ResponseFormat string +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { + a.ChannelType = info.ChannelType +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/chat/completions", info.ChannelType), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error { + header.Set("Authorization", "Bearer "+info.ApiKey) + header.Set("Content-Type", "application/json") + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + // 将 OpenAI 格式转换为本提供商格式 + myRequest := MyProviderRequest{ + Model: info.UpstreamModelName, + Prompt: request.Messages, // 简化示例 + Stream: info.IsStream, + } + return myRequest, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + // 处理响应、提取使用信息 + var myResp MyProviderResponse + if err := c.ShouldBindJSON(&myResp); err != nil { + return nil, common.ErrorWrapper(err) + } + // 返回使用量供计费 + return myResp.Usage, nil +} + +// 实现其他必需的接口方法... +``` + +### 步骤 4: 注册通道类型 + +1. 在 `constant/channel_type.go` 添加通道常量: +```go +const ( + ChannelTypeOpenAI = 1 + // ... + ChannelTypeMyProvider = 50 // 使用新的 ID +) +``` + +2. 在 `constant/model.go` 的 `GetChannelTypeStrings()` 添加映射: +```go +case ChannelTypeMyProvider: + return "myprovider" +``` + +3. 在 `relay/adaptor.go` 的 `GetAdaptor()` 函数中注册: +```go +case constant.ChannelTypeMyProvider: + return &myprovider.Adaptor{} +``` + +### 步骤 5: 添加 StreamOptions 支持(如适用) + +如果提供商支持流式选项,在 `relay/common/relay_info.go` 的 `streamSupportedChannels` 中添加: +```go +var streamSupportedChannels = map[int]bool{ + constant.ChannelTypeOpenAI: true, + // ... + constant.ChannelTypeMyProvider: true, // 添加这里 +} +``` + +并在适配器中添加 StreamOptions 支持: +```go +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if info.SupportStreamOptions && info.IsStream { + request.StreamOptions = &dto.StreamOptions{ + IncludeUsage: true, + } + } + // ... +} +``` + +### 步骤 6: 添加模型列表 + +在前端配置中添加模型映射,或实现 `GetModelList()` 返回支持模型列表。 + +--- + +## 添加新的异步任务提供商 + +异步任务提供商(如 Suno、Sora、Kling)与同步提供商不同,需要: + +1. 提交任务到上游 +2. 获取任务 ID +3. 轮询任务状态 +4. 返回最终结果 + +### 步骤 1: 创建任务适配器 + +在 `relay/channel/task/` 下创建目录和文件: + +```bash +mkdir relay/channel/task/mytask +touch relay/channel/task/mytask/adaptor.go +touch relay/channel/task/mytask/constants.go +``` + +**adaptor.go** 实现 `TaskAdaptor` 接口: + +```go +package mytask + +import ( + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + + "github.com/gin-gonic/gin" + "io" + "net/http" +) + +type Adaptor struct { + ChannelType int +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { + a.ChannelType = info.ChannelType +} + +// 验证请求并设置动作类型 +func (a *Adaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError { + // 验证请求参数 + if info.Prompt == "" { + return &dto.TaskError{ + Message: "prompt is required", + Code: "invalid_request", + } + } + info.Action = "create" + return nil +} + +// 预估算计费 +func (a *Adaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 { + // 返回额外的计费比例 + return map[string]float64{ + "seconds": 1.0, + "size": 1.5, + } +} + +// 构建请求 URL +func (a *Adaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { + return info.ChannelBaseUrl + "/api/v1/create", nil +} + +// 构建请求头 +func (a *Adaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + req.Header.Set("Authorization", "Bearer "+info.ApiKey) + req.Header.Set("Content-Type", "application/json") + return nil +} + +// 构建请求体 +func (a *Adaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { + reqBody := map[string]interface{}{ + "prompt": info.Prompt, + "model": info.UpstreamModelName, + } + body, _ := json.Marshal(reqBody) + return bytes.NewReader(body), nil +} + +// 执行提交请求 +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + client := common.GetHttpClient() + req, _ := http.NewRequest("POST", info.ChannelBaseUrl, requestBody) + a.BuildRequestHeader(c, req, info) + return client.Do(req) +} + +// 处理提交响应 +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, err *dto.TaskError) { + if resp.StatusCode != http.StatusOK { + return "", nil, &dto.TaskError{ + Message: "submit failed", + Code: "submit_error", + } + } + body, _ := io.ReadAll(resp.Body) + var result map[string]interface{} + json.Unmarshal(body, &result) + return result["id"].(string), body, nil +} + +// 获取任务状态 +func (a *Adaptor) FetchTask(baseUrl, key string, body map[string]any, proxy string) (*http.Response, error) { + client := common.GetHttpClient() + taskURL := baseUrl + "/api/v1/task/" + body["task_id"].(string) + req, _ := http.NewRequest("GET", taskURL, nil) + req.Header.Set("Authorization", "Bearer "+key) + return client.Do(req) +} + +// 解析任务结果 +func (a *Adaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { + var result map[string]interface{} + json.Unmarshal(respBody, &result) + + status := result["status"].(string) + return &relaycommon.TaskInfo{ + TaskID: result["id"].(string), + Status: status, + Result: result["result"], + Finished: status == "completed" || status == "failed", + }, nil +} + +// 完成后调整计费 +func (a *Adaptor) AdjustBillingOnComplete(task *model.Task, taskResult *relaycommon.TaskInfo) int { + if taskResult.Status == "completed" { + // 返回正数表示需要补扣 + return 10 + } + return 0 +} + +func (a *Adaptor) GetModelList() []string { + return []string{"mytask-model-v1", "mytask-model-v2"} +} + +func (a *Adaptor) GetChannelName() string { + return "MyTask" +} +``` + +### 步骤 2: 注册任务适配器 + +在 `relay/adaptor.go` 中添加: +```go +func GetTaskAdaptor(platform constant.TaskPlatform) service.TaskPollingAdaptor { + switch platform { + case constant.TaskPlatformSuno: + return &suno.Adaptor{} + // ... + case constant.TaskPlatformMyTask: + return &mytask.Adaptor{} + default: + return nil + } +} +``` + +--- + +## 添加新的 API 路由 + +### 添加管理后台路由 + +在 `router/dashboard.go` 中添加: + +```go +func SetDashboardRouter(router *gin.Engine) { + dashboardRouter := router.Group("/api") + dashboardRouter.Use(middleware.RouteTag("dashboard")) + dashboardRouter.Use(middleware.TokenAuth()) + { + // 现有路由... + + // 添加新路由 + myFeatureRouter := dashboardRouter.Group("/myfeature") + myFeatureRouter.Use(middleware.UserAuth()) + { + myFeatureRouter.GET("", controller.ListMyFeature) + myFeatureRouter.POST("", controller.CreateMyFeature) + myFeatureRouter.PUT("/:id", controller.UpdateMyFeature) + myFeatureRouter.DELETE("/:id", controller.DeleteMyFeature) + } + } +} +``` + +### 添加 API 路由 + +在 `router/api-router.go` 中添加: + +```go +func SetApiRouter(router *gin.Engine) { + apiRouter := router.Group("/api") + apiRouter.Use(middleware.RouteTag("api")) + apiRouter.Use(middleware.TokenAuth()) + { + // 现有路由... + + // 添加新路由 + apiRouter.POST("/myfeature", controller.MyFeatureHandler) + } +} +``` + +--- + +## 扩展计费系统 + +### 添加新的计费比例类型 + +在 `model/channel_satisfy.go` 中扩展: + +```go +type ChannelRatioConfig struct { + // 现有字段... + MyTaskSecondsRatio float64 `json:"mytask_seconds_ratio"` + MyTaskSizeRatio float64 `json:"mytask_size_ratio"` +} +``` + +### 实现自定义计费逻辑 + +在 `service/billing.go` 中添加: + +```go +func CalculateMyTaskCost(info *relaycommon.RelayInfo, otherRatios map[string]float64) (float64, error) { + modelRatio, err := model.GetModelRatio(info.OriginModelName) + if err != nil { + return 0, err + } + + // 获取额外比例 + secondsRatio := otherRatios["seconds"] + sizeRatio := otherRatios["size"] + + // 计算基础费用 + baseCost := modelRatio.Ratio * 1.0 // per token/second base + + // 应用额外比例 + totalCost := baseCost * secondsRatio * sizeRatio + + return totalCost, nil +} +``` + +--- + +## 添加中间件 + +### 创建中间件文件 + +在 `middleware/` 下创建新文件: + +```go +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +func MyCustomMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 前置处理 + requestID := c.GetHeader("X-Request-ID") + + // 调用下一个处理 + c.Next() + + // 后置处理 + statusCode := c.Writer.Status() + // 记录日志等 + } +} +``` + +### 注册中间件 + +在路由文件中使用: + +```go +func SetRelayRouter(router *gin.Engine) { + relayRouter := router.Group("/v1") + relayRouter.Use(middleware.MyCustomMiddleware()) + // ... +} +``` + +--- + +## 自定义配置选项 + +### 添加新的配置类型 + +在 `dto/channel_setting.go` 中扩展: + +```go +type ChannelSettings struct { + // 现有字段... + MyCustomSetting string `json:"my_custom_setting"` + MyAdvancedFlag bool `json:"my_advanced_flag"` +} +``` + +### 在数据库中存储 + +在 `model/channel.go` 的 `Channel` 结构中添加: + +```go +type Channel struct { + // 现有字段... + OtherSettings ChannelOtherSettings `json:"other_settings"` +} + +type ChannelOtherSettings struct { + // 现有字段... + MyCustomSetting string `json:"my_custom_setting"` + MyAdvancedFlag bool `json:"my_advanced_flag"` +} +``` + +--- + +## 测试指南 + +### 单元测试 + +创建 `*_test.go` 文件: + +```go +package myprovider + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestAdaptorConvertRequest(t *testing.T) { + adaptor := &Adaptor{} + info := &relaycommon.RelayInfo{ + UpstreamModelName: "gpt-4", + IsStream: true, + } + request := &dto.GeneralOpenAIRequest{ + Model: "gpt-4", + } + + result, err := adaptor.ConvertOpenAIRequest(nil, info, request) + + assert.NoError(t, err) + assert.NotNil(t, result) +} + +func TestAdaptorGetRequestURL(t *testing.T) { + adaptor := &Adaptor{} + info := &relaycommon.RelayInfo{ + ChannelBaseUrl: "https://api.myprovider.com", + } + + url, err := adaptor.GetRequestURL(info) + + assert.NoError(t, err) + assert.Equal(t, "https://api.myprovider.com/v1/chat/completions", url) +} +``` + +### 运行测试 + +```bash +# 运行所有测试 +go test ./... + +# 运行特定包的测试 +go test ./relay/channel/myprovider/ + +# 运行特定测试 +go test ./relay/channel/myprovider/ -run TestAdaptorConvertRequest + +# 详细输出 +go test -v ./relay/channel/myprovider/ +``` + +### 集成测试 + +测试完整的请求流程: + +```go +func TestRelayIntegration(t *testing.T) { + // 启动测试服务器 + // 发送请求 + // 验证响应 +} +``` + +--- + +## 前端扩展 + +### 添加新页面 + +在 `web/src/pages/` 下创建新页面组件: + +```tsx +import React, { useEffect, useState } from 'react'; +import { Button, Form, Input } from '@douyinfe/semi-ui'; + +const MyFeaturePage: React.FC = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + // 加载数据 + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + const response = await API.get('/api/myfeature'); + setData(response.data); + setLoading(false); + }; + + return ( +
+

My Feature

+
+ + +
+
+ ); +}; + +export default MyFeaturePage; +``` + +### 添加路由 + +在 `web/src/App.tsx` 中添加路由: + +```tsx +import MyFeaturePage from './pages/MyFeaturePage'; + +// 在路由配置中添加 +} /> +``` + +### 添加国际化 + +在 `web/src/i18n/locales/zh.json` 添加: + +```json +{ + "我的功能": "My Feature", + "提交": "Submit" +} +``` + +在组件中使用: + +```tsx +const { t } = useTranslation(); + +``` + +--- + +## 常见问题 + +### Q: 如何调试适配器? + +A: 使用 `common.DebugEnabled` 开启调试模式,或添加日志: + +```go +if common.DebugEnabled { + common.SysLog(fmt.Sprintf("Request: %+v", request)) +} +``` + +### Q: 如何处理流式响应? + +A: 参考 `relay/channel/openai/relay-openai.go` 的 `OaiStreamHandler` 实现: + +```go +func StreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (usage any, err *types.NewAPIError) { + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "data: ") { + // 处理 SSE 数据 + } + } + return usage, nil +} +``` + +### Q: 如何添加新的错误类型? + +A: 在 `types/error.go` 中扩展: + +```go +type NewAPIError struct { + Code string `json:"code"` + Message string `json:"message"` + Param string `json:"param,omitempty"` + Type string `json:"type,omitempty"` + StatusCode int `json:"-"` +} +``` + +### Q: 数据库迁移如何处理? + +A: 在 `model/main.go` 中添加自动迁移逻辑: + +```go +func InitDB() error { + // ... 连接数据库 + + // 自动迁移 + if err := DB.AutoMigrate(&MyModel{}); err != nil { + return err + } + + // ... 其他初始化 +} +``` + +--- + +## 最佳实践 + +1. **遵循接口设计** - 实现适配器时严格遵循接口定义 +2. **错误处理** - 使用统一的错误类型和错误码 +3. **日志记录** - 使用 `common.SysLog` 和 `common.SysError` +4. **测试覆盖** - 为新功能编写单元测试 +5. **文档更新** - 更新相关 API 文档和配置说明 +6. **向后兼容** - 保持现有 API 兼容性 +7. **性能考虑** - 避免阻塞操作,合理使用缓存 +8. **安全考虑** - 验证所有输入,防止注入攻击 + +--- + +## 相关资源 + +- [项目架构详解](./02-架构详解.md) +- [CLAUDE.md](../../CLAUDE.md) - 项目开发约定 +- [API 文档](https://docs.newapi.pro/en/docs/api) diff --git "a/docs/learn_docs/04-\346\234\254\345\234\260\345\274\200\345\217\221Debug\346\214\207\345\215\227.md" "b/docs/learn_docs/04-\346\234\254\345\234\260\345\274\200\345\217\221Debug\346\214\207\345\215\227.md" new file mode 100644 index 000000000000..d2a4d77f31b0 --- /dev/null +++ "b/docs/learn_docs/04-\346\234\254\345\234\260\345\274\200\345\217\221Debug\346\214\207\345\215\227.md" @@ -0,0 +1,634 @@ +# 本地开发 Debug 指南 + +本文档介绍 new-api 本地开发环境搭建、调试技巧和常用命令。 + +--- + +## 目录 + +1. [环境准备](#环境准备) +2. [Docker 开发环境](#docker-开发环境) +3. [本地开发模式](#本地开发模式) +4. [调试技巧](#调试技巧) +5. [常见问题排查](#常见问题排查) + +--- + +## 环境准备 + +### 1.1 系统要求 + +| 组件 | 要求 | +|-------|------| +| **操作系统** | macOS, Linux, Windows (WSL2) | +| **Go** | 1.25.1 或更高版本 | +| **Node.js** | 18.x 或更高版本 | +| **Bun** | 1.x(推荐,或使用 npm/pnpm) | +| **Docker** | 20.10+(用于 Docker Compose 环境) | +| **Docker Compose** | 2.x | + +### 1.2 安装必要工具 + +```bash +# 检查 Go 版本 +go version + +# 检查 Node.js 版本 +node --version + +# 安装 Bun(推荐) +curl -fsSL https://bun.sh/install | bash + +# 或使用 npm +npm install -g npm + +# 或使用 pnpm +npm install -g pnpm + +# 检查 Docker 版本 +docker --version +docker-compose --version +``` + +--- + +## Docker 开发环境 + +### 2.1 启动外部服务 + +使用 `docker-compose-dev.yml` 启动数据库和 Redis: + +```bash +cd /Users/zhai/my_project/go_lang_workspaces/new-api +docker-compose -f docker-compose-dev.yml up -d +``` + +### 2.2 服务说明 + +| 服务 | 说明 | 端口 | 用途 | +|-------|------|-------|-------| +| **postgres** | 5432 | PostgreSQL 数据库 | +| **redis** | 6379 | Redis 缓存 | +| **redis-commander** | 8081 | Redis 可视化管理(可选)| +| **pgadmin4** | 5050 | PostgreSQL 可视化管理(可选)| + +### 2.3 连接信息 + +#### PostgreSQL +``` +Host: localhost +Port: 5432 +User: newapi +Password: newapi_password +Database: newapi_dev +Connection String: postgres://newapi:newapi_password@localhost:5432/newapi_dev?sslmode=disable +``` + +#### Redis +``` +Host: localhost +Port: 6379 +Connection String: redis://localhost:6379 +``` + +### 2.4 常用 Docker 命令 + +```bash +# 启动所有服务 +docker-compose -f docker-compose-dev.yml up -d + +# 停止所有服务 +docker-compose -f docker-compose-dev.yml down + +# 重启某个服务 +docker-compose -f docker-compose-dev.yml restart redis + +# 查看日志 +docker-compose -f docker-compose-dev.yml logs -f postgres + +# 查看服务状态 +docker-compose -f docker-compose-dev.yml ps + +# 进入容器(调试用) +docker-compose -f docker-compose-dev.yml exec postgres psql -U newapi -d newapi_dev + +# 清理所有数据(危险操作!) +docker-compose -f docker-compose-dev.yml down -v +``` + +--- + +## 本地开发模式 + +### 3.1 模式对比 + +| 模式 | 说明 | 前端 | 后端 | 适用场景 | +|-------|------|-------|-------|-------| +| **开发模式** | 前后端分离运行 | 独立运行 | 日常开发调试 | +| **集成模式** | 前端打包后一起 | Go 编译 | 本地测试完整功能 | + +### 3.2 开发模式启动(推荐) + +**启动顺序:** + +1. **启动外部服务** +```bash +cd /Users/zhai/my_project/go_lang_workspaces/new-api +docker-compose -f docker-compose-dev.yml up -d +``` + +2. **启动后端**(新终端) +```bash +cd /Users/zhai/my_project/go_lang_workspaces/new-api +# 创建环境变量文件 +cat > .env << 'EOF' +SQL_DSN=postgres://newapi:newapi_password@localhost:5432/newapi_dev?sslmode=disable +REDIS_CONN_STRING=redis://localhost:6379 +SESSION_SECRET=dev-secret-key-change-in-production +GIN_MODE=debug +EOF + +# 启动后端 +go run main.go +``` + +3. **启动前端**(新终端) +```bash +cd /Users/zhai/my_project/go_lang_workspaces/new-api/web + +# 使用 Bun(推荐) +bun install +bun run dev + +# 或使用 npm/pnpm +pnpm install +pnpm run dev +``` + +### 3.3 访问地址 + +| 服务 | 地址 | 说明 | +|-------|------|------| +| **后端 API** | http://localhost:3000 | API 服务 | +| **前端 Dev** | http://localhost:5173 | Vite 开发服务器 | +| **Redis Commander** | http://localhost:8081 | Redis 管理(可选)| +| **pgAdmin** | http://localhost:5050 | PostgreSQL 管理(可选)| + +### 3.4 集成模式启动 + +如果要测试完整的打包前端: + +```bash +# 1. 构建前端 +cd /Users/zhai/my_project/go_lang_workspaces/new-api/web +bun run build + +# 2. 创建临时 dist 目录(如果有构建问题) +mkdir -p ../web/dist +cp index.html ../web/dist/ + +# 3. 启动后端 +cd .. +go run main.go + +# 访问 http://localhost:3000 +``` + +--- + +## 调试技巧 + +### 4.1 启用调试日志 + +#### 方式一:环境变量 + +```bash +# 启用调试模式 +GIN_MODE=debug go run main.go + +# 启用错误日志 +ERROR_LOG_ENABLED=true go run main.go +``` + +#### 方式二:代码中设置 + +在 `common/env.go` 中: +```go +var DebugEnabled = os.Getenv("GIN_MODE") == "debug" +``` + +### 4.2 使用 Delve 调试器 + +#### 安装 Delve +```bash +go install github.com/go-delve/delve/cmd/dlv@latest +``` + +#### 启动调试模式 +```bash +# 调试模式启动(默认端口 :2345) +dlv debug main.go --headless --listen=:2345 --api-version=2 + +# 或使用断点 +dlv debug main.go +# 在代码中设置断点后,在 Delve 提示符中使用: +(Delve) break main.go:line_number +(Delve) continue +``` + +#### VS Code 配置 + +创建 `.vscode/launch.json`: +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "env": { + "GIN_MODE": "debug" + }, + "args": [], + "showLog": true + } + ] +} +``` + +### 4.3 使用 pprof 性能分析 + +#### 启用 pprof + +```bash +# 设置环境变量 +ENABLE_PPROF=true go run main.go + +# pprof 将在 http://localhost:8005 启动 +``` + +#### 分析 CPU 性能 +```bash +# 1. 生成 CPU profile +go tool pprof -http=:9999 cpu.out + +# 2. 运行负载测试 +ab -n 1000 -c 10 http://localhost:3000/v1/models + +# 3. 在浏览器访问 http://localhost:9999 查看 +``` + +#### 分析内存 +```bash +# 1. 生成 heap profile +go tool pprof -http=:9999 heap.out + +# 2. 在浏览器访问 http://localhost:9999 查看 +``` + +### 4.4 数据库调试 + +#### 连接 PostgreSQL +```bash +# 使用 docker exec 连接 +docker-compose -f docker-compose-dev.yml exec postgres psql -U newapi -d newapi_dev + +# 常用查询 +\dt # 列出所有表 +\d+ channel # 查看 channel 表结构 +SELECT * FROM users; # 查询用户 +``` + +#### 连接 Redis +```bash +# 使用 redis-cli(需要安装) +redis-cli -h localhost -p 6379 + +# 常用命令 +KEYS * # 列出所有键 +GET channel:1 # 获取通道缓存 +FLUSHALL # 清空所有数据(危险!) +``` + +### 4.5 日志调试 + +#### 查看 Gin 请求日志 + +在 `middleware/logger.go` 中启用详细日志: +```go +// 输出请求详情 +c.Set("middleware.logger", middleware.LogDetail) +``` + +#### 查看系统日志 + +```bash +# 后端日志输出到控制台,直接查看 + +# 或查看文件日志(如果配置) +tail -f data/logs/new-api.log +``` + +### 4.6 数据库日志 + +启用数据库查询日志: + +```go +import "gorm.io/gorm/logger" + +// 在 InitDB 时配置 +DB.Use(logger.Default.LogMode(logger.Info)) +``` + +--- + +## 常见问题排查 + +### 5.1 数据库连接失败 + +**错误信息:** +``` +failed to initialize database: dial tcp: connection refused +``` + +**排查步骤:** +```bash +# 1. 检查 Docker 服务状态 +docker-compose -f docker-compose-dev.yml ps + +# 2. 检查端口占用 +lsof -i :5432 # PostgreSQL +lsof -i :6379 # Redis + +# 3. 查看容器日志 +docker-compose -f docker-compose-dev.yml logs postgres +docker-compose -f docker-compose-dev.yml logs redis + +# 4. 重启服务 +docker-compose -f docker-compose-dev.yml restart postgres redis +``` + +### 5.2 端口被占用 + +**错误信息:** +``` +bind: address already in use +``` + +**排查步骤:** +```bash +# 查找占用端口的进程 +lsof -i :3000 # 后端端口 +lsof -i :5173 # 前端端口 + +# macOS 使用 lsof,Linux 使用 ss 或 netstat + +# 杀死进程 +kill -9 +``` + +### 5.3 前端构建失败 + +**错误信息:** +``` +Missing "./dist/css/semi.css" specifier +``` + +**解决方案:** +```bash +# 方案 1:清理重装 +cd web +rm -rf node_modules package-lock.json pnpm-lock.yaml +pnpm install + +# 方案 2:使用开发模式,跳过构建 +# 前后端独立运行,不需要构建前端 + +# 方案 3:降级 semi-ui 版本 +pnpm add @douyinfe/semi-ui@2.68.0 +``` + +### 5.4 Go 依赖下载慢 + +**问题描述:** +国内访问 `proxy.golang.org` 慢或失败。 + +**解决方案:** +```bash +# 设置 Go 代理 +go env -w GOPROXY=https://goproxy.cn,direct + +# 使用 Go 官方代理 +export GOPROXY=https://proxy.golang.org,direct + +# 取消代理 +go env -w GOPROXY=direct +``` + +### 5.5 请求超时 + +**问题描述:** +API 请求返回超时错误。 + +**排查步骤:** +```bash +# 1. 检查网络连接 +curl -v http://localhost:3000/api/status + +# 2. 检查上游连接 +curl -v https://api.openai.com + +# 3. 检查数据库连接 +docker-compose -f docker-compose-dev.yml exec postgres ping -c 1 + +# 4. 增加 STREAMING_TIMEOUT +# 在 .env 中设置 +STREAMING_TIMEOUT=600 +``` + +### 5.6 Redis 缓存问题 + +**检查 Redis 连接:** +```bash +# 检查后端日志 +[SYS] REDIS_CONN_STRING not set, Redis is not enabled + +# 正确配置环境变量 +REDIS_CONN_STRING=redis://localhost:6379 +``` + +**清空 Redis 缓存:** +```bash +# 使用 redis-cli +redis-cli -h localhost -p 6379 FLUSHALL + +# 或进入容器 +docker-compose -f docker-compose-dev.yml exec redis redis-cli FLUSHALL +``` + +--- + +## 开发工作流 + +### 6.1 典型开发流程 + +```mermaid +graph LR + A[开始] --> B[启动 Docker 服务] + B --> C[启动后端] + C --> D[启动前端] + D --> E[开发功能] + E --> F[本地测试] + F --> G[写单元测试] + G --> H[提交代码] + H --> I[停止服务] +``` + +### 6.2 快速重启脚本 + +创建 `dev.sh` 脚本: + +```bash +#!/bin/bash + +# 停止现有进程 +pkill -f "go run main.go" +pkill -f "vite" +pkill -f "bun" + +# 启动 Docker 服务 +docker-compose -f docker-compose-dev.yml up -d + +# 启动后端(后台) +go run main.go & +BACKEND_PID=$! + +# 启动前端(后台) +cd web && bun run dev & +FRONTEND_PID=$! + +echo "Backend PID: $BACKEND_PID" +echo "Frontend PID: $FRONTEND_PID" +echo "Press Ctrl+C to stop all" + +# 等待 Ctrl+C +trap "kill $BACKEND_PID $FRONTEND_PID; exit" INT + +wait +``` + +使用方法: +```bash +chmod +x dev.sh +./dev.sh +``` + +--- + +## IDE 配置 + +### 7.1 VS Code 推荐扩展 + +| 扩展名 | 用途 | +|---------|------| +| **Go** | Go 语言支持 | +| **Chinese (Simplified) Language Pack** | 中文语言包 | +| **ESLint** | JavaScript 代码检查 | +| **Prettier** | 代码格式化 | +| **GitLens** | Git 增强 | +| **Thunder Client** | REST API 测试 | + +### 7.2 GoLand 配置 + +1. **导入项目**:File → Open → 选择项目目录 +2. **配置 GOPATH**:Settings → Go → GOPATH 设置 +3. **启用 Go Modules**:Settings → Go → Go Modules → 启用 +4. **配置运行配置**:Settings → Go → Build Tags & Vendoring + +--- + +## API 测试 + +### 8.1 使用 curl 测试 + +```bash +# 1. 获取系统状态 +curl http://localhost:3000/api/status | jq . + +# 2. 测试模型列表(需要 Token) +curl -H "Authorization: Bearer YOUR_TOKEN" \ + http://localhost:3000/v1/models | jq . + +# 3. 测试聊天接口 +curl -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello!"}] + }' \ + http://localhost:3000/v1/chat/completions | jq . + +# 4. 测试流式响应 +curl -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello!"}], + "stream": true + }' \ + -N http://localhost:3000/v1/chat/completions +``` + +### 8.2 使用 Postman/Thunder Client + +1. **导入环境变量**:创建 `.env` 文件,导入 `http://localhost:3000` +2. **设置认证**:Bearer Token → 在初始化后获取 +3. **保存请求集合**:保存常用的 API 请求 +4. **测试不同场景**:正常、错误、限流等 + +--- + +## 性能优化 + +### 9.1 减少重新编译 + +```go +// 使用 -race 检测竞态条件 +go run -race main.go + +// 使用 -p 标记编译 +go run -gcflags "-m=2" main.go +``` + +### 9.2 使用 Go build cache + +```bash +# 第一次编译会缓存依赖 +go build -o /dev/null ./... + +# 后续编译会更快 +go build -o new-api main.go +``` + +### 9.3 并发测试 + +```bash +# 使用 wrk 进行并发测试 +wrk -t4s -c10 http://localhost:3000/v1/models + +# 使用 ab (Apache Benchark) +ab -n 1000 -c 10 http://localhost:3000/api/status +``` + +--- + +## 相关资源 + +- [项目概述](./01-项目概述.md) +- [架构详解](./02-架构详解.md) +- [二次开发指南](./03-二次开发指南.md) +- [CLAUDE.md](../../CLAUDE.md) - 项目开发约定 +- [官方文档](https://docs.newapi.pro) diff --git a/docs/learn_docs/README.md b/docs/learn_docs/README.md new file mode 100644 index 000000000000..45c1a96bf52b --- /dev/null +++ b/docs/learn_docs/README.md @@ -0,0 +1,209 @@ +# new-api 项目学习文档 + +欢迎使用 new-api 项目学习文档!本目录包含帮助您理解、学习和二次开发 new-api 的完整资料。 + +## 文档目录 + +### 📖 [01-项目概述.md](./01-项目概述.md) +- 项目介绍和核心功能 +- 技术栈概览 +- 项目结构说明 +- 快速上手指南 + +**适合人群**:所有希望了解 new-api 的开发者 + +### 🏗️ [02-架构详解.md](./02-架构详解.md) +- 整体架构设计 +- 请求处理流程 +- 中间件架构 +- Relay 转发系统 +- 数据库设计 +- 缓存架构 +- 异步任务系统 +- 计费系统 +- WebSocket 通信 +- 错误处理 + +**适合人群**:需要深入理解系统架构的开发者 + +### 🔧 [03-二次开发指南.md](./03-二次开发指南.md) +- 添加新的 AI 提供商通道 +- 添加新的异步任务提供商 +- 添加新的 API 路由 +- 扩展计费系统 +- 添加中间件 +- 自定义配置选项 +- 测试指南 +- 前端扩展 +- 常见问题 +- 最佳实践 + +**适合人群**:需要进行二次开发的开发者 + +### 🐛 [04-本地开发Debug指南.md](./04-本地开发Debug指南.md) +- 环境准备 +- Docker 开发环境 +- 本地开发模式 +- 调试技巧 +- 常见问题排查 +- 开发工作流 +- IDE 配置 +- API 测试 +- 性能优化 + +**适合人群**:本地开发环境搭建和调试的开发者 + +--- + +## 阅读路线 + +### 新手入门路线 +1. 📖 阅读 [01-项目概述.md](./01-项目概述.md) +2. 🏃 运行开发环境 +3. 🎯 浏览代码库结构 +4. 🔍 阅读感兴趣的功能模块 + +### 架构理解路线 +1. 📖 阅读 [01-项目概述.md](./01-项目概述.md) +2. 🏗️ 阅读 [02-架构详解.md](./02-架构详解.md) +3. 🔍 对照源码理解各个组件 +4. 📝 绘制自己的架构图加深理解 + +### 二次开发路线 +1. 📖 阅读 [01-项目概述.md](./01-项目概述.md) +2. 🏗️ 阅读 [02-架构详解.md](./02-架构详解.md) +3. 🔧 阅读 [03-二次开发指南.md](./03-二次开发指南.md) +4. 🐛 阅读 [04-本地开发Debug指南.md](./04-本地开发Debug指南.md) +5. 💻 开始编码开发 +6. 🧪 编写单元测试 +7. 📝 更新相关文档 + +--- + +## 核心概念快速参考 + +| 概念 | 说明 | 相关章节 | +|------|------|----------| +| **Adaptor** | 提供商适配器接口,实现统一的转发逻辑 | 02-架构详解 | +| **Relay Mode** | 转发模式:Chat、Embedding、Images、Audio 等 | 02-架构详解 | +| **Channel** | 上游通道配置,包含 API 密钥、权重等 | 01-项目概述 | +| **Token** | 访问令牌,关联用户和配额 | 01-项目概述 | +| **Distribute** | 分发中间件,实现通道选择和负载均衡 | 02-架构详解 | +| **Task** | 异步任务系统,支持 Midjourney、Suno 等 | 02-架构详解 | + +--- + +## 架构图索引 + +| 图表 | 描述 | 位置 | +|------|------|-------| +| 多提供商聚合 | 展示 40+ 提供商的聚合架构 | 01-项目概述 | +| 整体架构 | 完整的系统分层架构图 | 02-架构详解 | +| 请求处理流程 | AI 请求的完整处理时序图 | 02-架构详解 | +| 通道选择策略 | 通道选择的决策流程 | 02-架构详解 | +| 中间件架构 | 中间件执行顺序和功能 | 02-架构详解 | +| 适配器接口 | Adaptor 接口的类图 | 02-架构详解 | +| Relay 模式适配 | 不同 API 格式的转换流程 | 02-架构详解 | +| 数据库 ER 图 | 主要数据表关系图 | 02-架构详解 | +| 缓存架构 | 三级缓存设计 | 02-架构详解 | +| 任务生命周期 | 异步任务的状态转换 | 02-架构详解 | +| 任务适配器接口 | TaskAdaptor 接口的类图 | 02-架构详解 | +| 计费流程 | 完整的计费处理时序 | 02-架构详解 | +| 计费会话管理 | 计费会话的生命周期 | 02-架构详解 | +| WebSocket 通信 | Realtime API 的消息流程 | 02-架构详解 | +| 错误处理架构 | 错误的分类和处理流程 | 02-架构详解 | + +--- + +## 常见开发任务 + +### 添加新的 AI 提供商 +- 文档:[03-二次开发指南.md](./03-二次开发指南.md#添加新的-ai-提供商通道) +- 关键文件:`relay/channel/{provider}/adaptor.go` +- 关键接口:`Adaptor` + +### 添加异步任务提供商 +- 文档:[03-二次开发指南.md](./03-二次开发指南.md#添加新的异步任务提供商) +- 关键文件:`relay/channel/task/{provider}/adaptor.go` +- 关键接口:`TaskAdaptor` + +### 添加新 API 端点 +- 文档:[03-二次开发指南.md](./03-二次开发指南.md#添加新的-api-路由) +- 关键文件:`router/`、`controller/` + +### 扩展计费功能 +- 文档:[03-二次开发指南.md](./03-二次开发指南.md#扩展计费系统) +- 关键文件:`service/billing.go` + +### 添加中间件 +- 文档:[03-二次开发指南.md](./03-二次开发指南.md#添加中间件) +- 关键文件:`middleware/` + +--- + +## 本地开发快速启动 + +### Docker 服务配置文件 + +项目包含 `docker-compose-dev.yml` 用于本地开发环境,包含以下服务: + +| 服务 | 用途 | +|------|------| +| **postgres** | PostgreSQL 数据库 | +| **redis** | Redis 缓存 | + +### 快速启动步骤 + +```bash +# 1. 启动 Docker 服务 +docker-compose -f docker-compose-dev.yml up -d + +# 2. 创建后端环境变量 +cat > .env << 'EOF' +SQL_DSN=postgres://newapi:newapi_password@localhost:5432/newapi_dev?sslmode=disable +REDIS_CONN_STRING=redis://localhost:6379 +SESSION_SECRET=dev-secret-key-change-in-production +GIN_MODE=debug +EOF + +# 3. 启动后端 +go run main.go + +# 4. 启动前端(新终端) +cd web && bun install && bun run dev +``` + +### 访问地址 + +| 服务 | 地址 | +|------|------| +| **后端 API** | http://localhost:3000 | +| **前端 Dev** | http://localhost:5173 | + +--- + +## 相关资源 + +- [项目概述](./01-项目概述.md) +- [架构详解](./02-架构详解.md) +- [二次开发指南](./03-二次开发指南.md) +- [本地开发Debug指南](./04-本地开发Debug指南.md) +- [CLAUDE.md](../../CLAUDE.md) - 项目开发约定 +- [docker-compose-dev.yml](../../docker-compose-dev.yml) - 本地开发环境配置 +- [官方文档](https://docs.newapi.pro/en/docs) +- [GitHub 仓库](https://github.com/QuantumNous/new-api) +- [Issue 反馈](https://github.com/QuantumNous/new-api/issues) + +--- + +## 版本历史 + +| 版本 | 日期 | 更新内容 | +|------|------|----------| +| 1.0.0 | 2025-03-18 | 初始版本,包含项目概述、架构详解、二次开发指南 | +| 1.1.0 | 2025-03-18 | 新增本地开发Debug指南 | +| 1.1.1 | 2025-03-18 | 优化 docker-compose-dev.yml 配置 | + +--- + +**祝您学习愉快!如有问题,欢迎查阅官方文档或提交 Issue。** diff --git a/web/package.json b/web/package.json index 97c7c821fef3..9f059630ac2f 100644 --- a/web/package.json +++ b/web/package.json @@ -5,14 +5,18 @@ "type": "module", "dependencies": { "@douyinfe/semi-icons": "^2.63.1", + "@douyinfe/semi-illustrations": "^2.93.0", + "@douyinfe/semi-theme-default": "^2.93.0", "@douyinfe/semi-ui": "^2.69.1", "@lobehub/icons": "^2.0.0", "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", + "antd": "^6.3.3", "axios": "1.13.5", "clsx": "^2.1.1", "dayjs": "^1.11.11", + "highlight.js": "^11.11.1", "history": "^5.3.0", "i18next": "^23.16.8", "i18next-browser-languagedetector": "^7.2.0", @@ -20,6 +24,7 @@ "lucide-react": "^0.511.0", "marked": "^4.1.1", "mermaid": "^11.6.0", + "prop-types": "^15.8.1", "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/web/src/index.jsx b/web/src/index.jsx index 5162b0cbdd2a..8fc821d72a73 100644 --- a/web/src/index.jsx +++ b/web/src/index.jsx @@ -20,7 +20,6 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; -import '@douyinfe/semi-ui/dist/css/semi.css'; import { UserProvider } from './context/User'; import 'react-toastify/dist/ReactToastify.css'; import { StatusProvider } from './context/Status'; @@ -37,17 +36,17 @@ import en_GB from '@douyinfe/semi-ui/lib/es/locale/source/en_GB'; // Welcome message (Do not remove this without permission from the original developer) if (typeof window !== 'undefined') { console.log( - '%cWE ❤ NEWAPI%c Github: https://github.com/QuantumNous/new-api', - 'color: #10b981; font-weight: bold; font-size: 24px;', - 'color: inherit; font-size: 14px;', + '%cWE ❤ NEWAPI%c Github: https://github.com/QuantumNous/new-api', + 'color: #10b981; font-weight: bold; font-size: 24px;', + 'color: inherit; font-size: 14px;', ); } function SemiLocaleWrapper({ children }) { const { i18n } = useTranslation(); const semiLocale = React.useMemo( - () => ({ zh: zh_CN, en: en_GB })[i18n.language] || zh_CN, - [i18n.language], + () => ({ zh: zh_CN, en: en_GB })[i18n.language] || zh_CN, + [i18n.language], ); return {children}; } @@ -56,22 +55,22 @@ function SemiLocaleWrapper({ children }) { const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - - - - - - - - - - - , + + + + + + + + + + + + + , ); diff --git a/web/vite.config.js b/web/vite.config.js index 73e46212a587..028d3c436b77 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -19,16 +19,23 @@ For commercial licensing, please contact support@quantumnous.com import react from '@vitejs/plugin-react'; import { defineConfig, transformWithEsbuild } from 'vite'; -import pkg from '@douyinfe/vite-plugin-semi'; import path from 'path'; import { codeInspectorPlugin } from 'code-inspector-plugin'; -const { vitePluginSemi } = pkg; // https://vitejs.dev/config/ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, './src'), + '~@douyinfe/semi-theme-default': path.resolve(__dirname, './node_modules/@douyinfe/semi-theme-default'), + }, + }, + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + additionalData: `@import "@douyinfe/semi-theme-default/scss/variables.scss" as *;`, + }, }, }, plugins: [ @@ -51,9 +58,6 @@ export default defineConfig({ }, }, react(), - vitePluginSemi({ - cssLayer: true, - }), ], optimizeDeps: { force: true,