diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..72667cd93949b13864248bc1302fc24f142fdfbc
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,90 @@
+
+# 如果需要修改Docker暴露端口,请修改ports中的参数
+# 示例(8080:3000) 则访问 http://localhost:8080
+SERVICE_PORT=3000
+
+
+# 监听地址(非必填)
+LISTEN_ADDRESS=
+
+# PM2 多进程配置
+# PM2进程数量配置
+# max: 使用所有CPU核心
+# 数字: 指定进程数量,如 4
+# 1: 单进程模式
+PM2_INSTANCES=1
+
+# PM2内存限制,超过此限制将自动重启进程
+# 支持格式: 100M, 1G, 2G 等
+PM2_MAX_MEMORY=1G
+
+# API 密钥配置
+# 支持单个或多个API_KEY,用逗号分隔
+# 第一个API_KEY为管理员密钥,拥有全部权限(可访问前端管理页面、修改设置)
+# 其他API_KEY为普通密钥,仅有调用API的权限,不能访问前端管理页面
+#
+# 单个密钥示例:
+# API_KEY=sk-admin123
+#
+# 多个密钥示例:
+# API_KEY=sk-admin123,sk-user456,sk-user789
+# 其中:
+# - sk-admin123: 管理员密钥(可访问前端管理页面,可修改所有设置)
+# - sk-user456,sk-user789: 普通密钥(仅可调用API,不能访问前端页面)
+API_KEY=sk-123456
+
+# 是否输出思考过程
+OUTPUT_THINK=true
+
+# 搜索信息显示模式
+SEARCH_INFO_MODE=table
+
+# 简化模型映射
+# true: 只返回基础模型,不包含thinking、search、image等变体
+# false: 返回完整模型列表,包含所有变体
+SIMPLE_MODEL_MAP=false
+
+# Redis链接(如果使用redis模式,则必填,当redis使用tls时将redis://替换为rediss://)
+REDIS_URL=
+
+# 数据保存模式
+# none 不保存数据,仅使用环境变量中的设置
+# file 保存在本地文件中
+# redis 保存到远程/本地redis中
+DATA_SAVE_MODE=none
+
+# 账号与密码用:分隔,账号与账号间用,分隔(如果使用redis和file模式,则不需要填写)
+ACCOUNTS=
+
+# 日志配置
+# 日志级别 (DEBUG, INFO, WARN, ERROR)
+LOG_LEVEL=INFO
+
+# 是否启用文件日志
+ENABLE_FILE_LOG=false
+
+# 日志文件目录
+LOG_DIR=./logs
+
+# 最大日志文件大小 (MB)
+MAX_LOG_FILE_SIZE=10
+
+# 保留的日志文件数量
+MAX_LOG_FILES=5
+
+# ========== 代理与反代配置 ==========
+
+# 自定义反代URL配置
+# QWEN_CHAT_PROXY_URL: 替代 https://chat.qwen.ai 的反代地址
+# 示例: QWEN_CHAT_PROXY_URL=https://your-proxy.com
+QWEN_CHAT_PROXY_URL=
+
+# QWEN_CLI_PROXY_URL: 替代 https://portal.qwen.ai 的反代地址
+# 示例: QWEN_CLI_PROXY_URL=https://your-cli-proxy.com
+QWEN_CLI_PROXY_URL=
+
+# HTTP/HTTPS 代理配置
+# 支持 HTTP, HTTPS, SOCKS5 代理
+# 示例: PROXY_URL=http://127.0.0.1:7890
+# 示例: PROXY_URL=socks5://127.0.0.1:1080
+PROXY_URL=
\ No newline at end of file
diff --git a/.env.hf.example b/.env.hf.example
new file mode 100644
index 0000000000000000000000000000000000000000..12f70ab6f4382e169e15f59bfef3852897bdca6b
--- /dev/null
+++ b/.env.hf.example
@@ -0,0 +1,117 @@
+# Hugging Face Spaces 推荐运行配置
+# 说明:这个文件是示例模板,不要把真实密钥直接写进仓库
+#
+# 使用方式:
+# 1. GitHub Actions 中配置自动同步所需变量
+# - Secret: HF_TOKEN
+# - Variable: HF_SPACE_ID=DanielleNguyen/Qwen2API-A
+# 2. Hugging Face Space 中按下面两类分别配置:
+# - HF Secrets:敏感信息,例如 API_KEY、HF_TOKEN
+# - HF Variables:普通运行配置,例如端口、目录、开关
+# 3. 不建议在仓库中提交真实 .env 文件
+
+# ==================================================
+# HF Secrets 建议新建这些
+# ==================================================
+# API_KEY=sk-admin-yourkey,sk-user-yourkey
+# HF_TOKEN=hf_xxx
+# HF_BUCKET_TOKEN=
+# ACCOUNTS=
+# REDIS_URL=
+# PROXY_URL=
+# QWEN_CHAT_PROXY_URL=
+# QWEN_CLI_PROXY_URL=
+
+# ==================================================
+# HF Variables 建议新建这些
+# ==================================================
+
+# ==============================
+# 服务监听配置
+# Hugging Face Docker Space 建议固定如下:
+# - SERVICE_PORT=7860
+# - LISTEN_ADDRESS=0.0.0.0
+# ==============================
+SERVICE_PORT=7860
+LISTEN_ADDRESS=0.0.0.0
+
+# ==============================
+# 运行模式配置
+# Hugging Face 免费空间当前建议单进程运行,更稳定
+# PM2_MAX_MEMORY 是自动重启阈值,不是实际只给 4G 内存
+# ==============================
+PM2_INSTANCES=1
+PM2_MAX_MEMORY=4G
+NODE_ENV=production
+
+# ==============================
+# API 访问密钥
+# 第一个密钥是管理员密钥,后面的密钥是普通调用密钥
+# 这是敏感信息,建议放到 HF Secrets
+# ==============================
+API_KEY=sk-admin-demo,sk-user-demo
+
+# ==============================
+# 功能配置
+# 这些属于普通配置,建议放到 HF Variables
+# ==============================
+OUTPUT_THINK=true
+SEARCH_INFO_MODE=table
+SIMPLE_MODEL_MAP=false
+
+# ==============================
+# 数据存储配置
+# 当前推荐方案:使用 Hugging Face Bucket 做文件持久化
+# - 项目内部使用 DATA_SAVE_MODE=file
+# - 启动时从 Bucket 拉取数据到本地目录
+# - 运行中定时同步本地目录回 Bucket
+# ==============================
+DATA_SAVE_MODE=file
+
+# 下面两项通常属于敏感信息,建议放到 HF Secrets
+REDIS_URL=
+ACCOUNTS=
+
+# 下面这些路径配置建议放到 HF Variables
+DATA_DIR=/data/qwen2api/data
+CACHE_DIR=/data/qwen2api/caches
+LOG_DIR=/data/qwen2api/logs
+
+# ==============================
+# Hugging Face Bucket 持久化配置
+# HF_BUCKET_REPO:你的 Bucket 名称,建议放到 HF Variables
+# HF_TOKEN:Hugging Face 账号 Token,Space 通过它访问 Bucket,建议放到 HF Secrets
+# HF_BUCKET_TOKEN:可选;如果单独给 Bucket 配 token,则优先使用它,也建议放到 HF Secrets
+# ==============================
+HF_BUCKET_REPO=DanielleNguyen/Qwen2API-A-Storage
+HF_TOKEN=
+HF_BUCKET_TOKEN=
+HF_BUCKET_LOCAL_DIR=/data/qwen2api
+HF_BUCKET_REMOTE_DIR=runtime
+HF_BUCKET_SYNC_INTERVAL=300
+HF_BUCKET_SYNC_DEBOUNCE_SECONDS=5
+HF_BUCKET_STARTUP_GRACE_SECONDS=30
+
+# ==============================
+# 日志配置
+# Hugging Face 一般不建议开启文件日志,直接看容器日志即可
+# ==============================
+LOG_LEVEL=INFO
+ENABLE_FILE_LOG=false
+MAX_LOG_FILE_SIZE=10
+MAX_LOG_FILES=5
+
+# ==============================
+# 代理 / 上游反代配置
+# 按需填写;不需要就留空
+# 如果地址中带认证信息,建议放到 HF Secrets
+# ==============================
+QWEN_CHAT_PROXY_URL=
+QWEN_CLI_PROXY_URL=
+PROXY_URL=
+
+# ==============================
+# 图片缓存配置
+# 配合 Bucket 持久化时建议使用 file
+# ==============================
+CACHE_MODE=file
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index a6344aac8c09253b3b630fb776ae94478aa0275b..0000000000000000000000000000000000000000
--- a/.gitattributes
+++ /dev/null
@@ -1,35 +0,0 @@
-*.7z filter=lfs diff=lfs merge=lfs -text
-*.arrow filter=lfs diff=lfs merge=lfs -text
-*.bin filter=lfs diff=lfs merge=lfs -text
-*.bz2 filter=lfs diff=lfs merge=lfs -text
-*.ckpt filter=lfs diff=lfs merge=lfs -text
-*.ftz filter=lfs diff=lfs merge=lfs -text
-*.gz filter=lfs diff=lfs merge=lfs -text
-*.h5 filter=lfs diff=lfs merge=lfs -text
-*.joblib filter=lfs diff=lfs merge=lfs -text
-*.lfs.* filter=lfs diff=lfs merge=lfs -text
-*.mlmodel filter=lfs diff=lfs merge=lfs -text
-*.model filter=lfs diff=lfs merge=lfs -text
-*.msgpack filter=lfs diff=lfs merge=lfs -text
-*.npy filter=lfs diff=lfs merge=lfs -text
-*.npz filter=lfs diff=lfs merge=lfs -text
-*.onnx filter=lfs diff=lfs merge=lfs -text
-*.ot filter=lfs diff=lfs merge=lfs -text
-*.parquet filter=lfs diff=lfs merge=lfs -text
-*.pb filter=lfs diff=lfs merge=lfs -text
-*.pickle filter=lfs diff=lfs merge=lfs -text
-*.pkl filter=lfs diff=lfs merge=lfs -text
-*.pt filter=lfs diff=lfs merge=lfs -text
-*.pth filter=lfs diff=lfs merge=lfs -text
-*.rar filter=lfs diff=lfs merge=lfs -text
-*.safetensors filter=lfs diff=lfs merge=lfs -text
-saved_model/**/* filter=lfs diff=lfs merge=lfs -text
-*.tar.* filter=lfs diff=lfs merge=lfs -text
-*.tar filter=lfs diff=lfs merge=lfs -text
-*.tflite filter=lfs diff=lfs merge=lfs -text
-*.tgz filter=lfs diff=lfs merge=lfs -text
-*.wasm filter=lfs diff=lfs merge=lfs -text
-*.xz filter=lfs diff=lfs merge=lfs -text
-*.zip filter=lfs diff=lfs merge=lfs -text
-*.zst filter=lfs diff=lfs merge=lfs -text
-*tfevents* filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..099a04c8103801766628b2109e523e76c498e05b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+package-lock.json
+.env
+data/data.json
+data
+caches
+logs/
+*.log
+pkg_dist/*
+pkg_dist/
+.idea
+/public/dist
diff --git a/README.md b/README.md
index b3e3351b3e2f551ef4d65c411fc8965cdb7f1a1f..e2a360ecf6b33df529c405c961b93c6477d9d893 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,854 @@
+
+
+# 🚀 Qwen-Proxy
+
+[](https://github.com/Rfym21/Qwen2API)
+[](https://nodejs.org/)
+[](https://hub.docker.com/r/rfym21/qwen2api)
+
+[🔗 加入交流群](https://t.me/nodejs_project) | [📖 文档](#api-文档) | [🐳 Docker 部署](#docker-部署)
+
+
+
+## 🛠️ 快速开始
+
+### 项目说明
+
+Qwen-Proxy 是一个将 `https://chat.qwen.ai` 和 `Qwen Code / Qwen Cli` 转换为 OpenAI 兼容 API 的代理服务。通过本项目,您只需要一个账户,即可以使用任何支持 OpenAI API 的客户端(如 ChatGPT-Next-Web、LobeChat 等)来调用 `https://chat.qwen.ai` 和 `Qwen Code / Qwen Cli`的各种模型。其中 `/cli` 端点下的模型由 `Qwen Code / Qwen Cli` 提供,支持256k上下文,原生 tools 参数支持
+
+**主要特性:**
+- 兼容 OpenAI API 格式,无缝对接各类客户端
+- 支持多账户轮询,提高可用性
+- 支持流式/非流式响应
+- 支持多模态(图片识别、图片生成)
+- 支持智能搜索、深度思考等高级功能
+- 支持 CLI 端点,提供 256K 上下文和工具调用能力
+- 提供 Web 管理界面,方便配置和监控
+
+### ⚠️ 高并发说明
+
+> **重要提示**: `chat.qwen.ai` 对单 IP 有限速策略,目前已知该限制与 Cookie 无关,仅与 IP 相关。
+
+**解决方案:**
+
+如需高并发使用,建议配合代理池实现 IP 轮换:
+
+| 方案 | 配置方式 | 说明 |
+|------|----------|------|
+| **方案一** | `PROXY_URL` + [ProxyFlow](https://github.com/Rfym21/ProxyFlow) | 直接配置代理地址,所有请求通过代理池轮换 IP |
+| **方案二** | `QWEN_CHAT_PROXY_URL` + [UrlProxy](https://github.com/Rfym21/UrlProxy) + [ProxyFlow](https://github.com/Rfym21/ProxyFlow) | 通过反代 + 代理池组合,实现更灵活的 IP 轮换 |
+
+**配置示例:**
+
+```bash
+# 方案一:直接使用代理池
+PROXY_URL=http://127.0.0.1:8282 # ProxyFlow 代理地址
+
+# 方案二:反代 + 代理池组合
+QWEN_CHAT_PROXY_URL=http://127.0.0.1:8000/qwen # UrlProxy 反代地址(UrlProxy 配置 HTTP_PROXY 指向 ProxyFlow)
+```
+
+### 环境要求
+
+- Node.js 18+ (源码部署时需要)
+- Docker (可选)
+- Redis (可选,用于数据持久化)
+
+### ⚙️ 环境配置
+
+创建 `.env` 文件并配置以下参数:
+
+```bash
+# 🌐 服务配置
+LISTEN_ADDRESS=localhost # 监听地址
+SERVICE_PORT=3000 # 服务端口
+
+# 🔐 安全配置
+API_KEY=sk-123456,sk-456789 # API 密钥 (必填,支持多密钥)
+ACCOUNTS= # 账户配置 (格式: user1:pass1,user2:pass2)
+
+# 🚀 PM2 多进程配置
+PM2_INSTANCES=1 # PM2进程数量 (1/数字/max)
+PM2_MAX_MEMORY=1G # PM2内存限制 (100M/1G/2G等)
+ # 注意: PM2集群模式下所有进程共用同一个端口
+
+# 🔍 功能配置
+SEARCH_INFO_MODE=table # 搜索信息展示模式 (table/text)
+OUTPUT_THINK=true # 是否输出思考过程 (true/false)
+SIMPLE_MODEL_MAP=false # 简化模型映射 (true/false)
+
+# 🌐 代理与反代配置
+QWEN_CHAT_PROXY_URL= # 自定义 Chat API 反代URL (默认: https://chat.qwen.ai)
+QWEN_CLI_PROXY_URL= # 自定义 CLI API 反代URL (默认: https://portal.qwen.ai)
+PROXY_URL= # HTTP/HTTPS/SOCKS5 代理地址 (例如: http://127.0.0.1:7890)
+
+# 🗄️ 数据存储
+DATA_SAVE_MODE=none # 数据保存模式 (none/file/redis)
+REDIS_URL= # Redis 连接地址 (可选,使用TLS时为rediss://)
+
+# 📸 缓存配置
+CACHE_MODE=default # 图片缓存模式 (default/file)
+```
+
+#### 📋 配置说明
+
+| 参数 | 说明 | 示例 |
+|------|------|------|
+| `LISTEN_ADDRESS` | 服务监听地址 | `localhost` 或 `0.0.0.0` |
+| `SERVICE_PORT` | 服务运行端口 | `3000` |
+| `API_KEY` | API 访问密钥,支持多密钥配置。第一个为管理员密钥(可访问前端管理页面),其他为普通密钥(仅可调用API)。多个密钥用逗号分隔 | `sk-admin123,sk-user456,sk-user789` |
+| `PM2_INSTANCES` | PM2进程数量 | `1`/`4`/`max` |
+| `PM2_MAX_MEMORY` | PM2内存限制 | `100M`/`1G`/`2G` |
+| `SEARCH_INFO_MODE` | 搜索结果展示格式 | `table` 或 `text` |
+| `OUTPUT_THINK` | 是否显示 AI 思考过程 | `true` 或 `false` |
+| `SIMPLE_MODEL_MAP` | 简化模型映射,只返回基础模型不包含变体 | `true` 或 `false` |
+| `QWEN_CHAT_PROXY_URL` | 自定义 Chat API 反代地址 | `https://your-proxy.com` |
+| `QWEN_CLI_PROXY_URL` | 自定义 CLI API 反代地址 | `https://your-cli-proxy.com` |
+| `PROXY_URL` | 出站请求代理地址,支持 HTTP/HTTPS/SOCKS5 | `http://127.0.0.1:7890` |
+| `DATA_SAVE_MODE` | 数据持久化方式 | `none`/`file`/`redis` |
+| `REDIS_URL` | Redis 数据库连接地址,使用TLS加密时需使用 `rediss://` 协议 | `redis://localhost:6379` 或 `rediss://xxx.upstash.io` |
+| `CACHE_MODE` | 图片缓存存储方式 | `default`/`file` |
+| `LOG_LEVEL` | 日志级别 | `DEBUG`/`INFO`/`WARN`/`ERROR` |
+| `ENABLE_FILE_LOG` | 是否启用文件日志 | `true` 或 `false` |
+| `LOG_DIR` | 日志文件目录 | `./logs` |
+| `MAX_LOG_FILE_SIZE` | 最大日志文件大小(MB) | `10` |
+| `MAX_LOG_FILES` | 保留的日志文件数量 | `5` |
+
+> 💡 **提示**: 可以在 [Upstash](https://upstash.com/) 免费创建 Redis 实例,使用 TLS 协议时地址格式为 `rediss://...`
+
+

+
+
+#### 🔑 多API_KEY配置说明
+
+`API_KEY` 环境变量支持配置多个API密钥,用于实现不同权限级别的访问控制:
+
+**配置格式:**
+```bash
+# 单个密钥(管理员权限)
+API_KEY=sk-admin123
+
+# 多个密钥(第一个为管理员,其他为普通用户)
+API_KEY=sk-admin123,sk-user456,sk-user789
+```
+
+**权限说明:**
+
+| 密钥类型 | 权限范围 | 功能描述 |
+|----------|----------|----------|
+| **管理员密钥** | 完整权限 | • 访问前端管理页面
• 修改系统设置
• 调用所有API接口
• 添加/删除普通密钥 |
+| **普通密钥** | API调用权限 | • 仅可调用API接口
• 无法访问前端管理页面
• 无法修改系统设置 |
+
+**使用场景:**
+- **团队协作**: 为不同团队成员分配不同权限的API密钥
+- **应用集成**: 为第三方应用提供受限的API访问权限
+- **安全隔离**: 将管理权限与普通使用权限分离
+
+**注意事项:**
+- 第一个API_KEY自动成为管理员密钥,拥有最高权限
+- 管理员可以通过前端页面动态添加或删除普通密钥
+- 所有密钥都可以正常调用API接口,权限差异仅体现在管理功能上
+
+#### 📸 CACHE_MODE 缓存模式说明
+
+`CACHE_MODE` 环境变量控制图片缓存的存储方式,用于优化图片上传和处理性能:
+
+| 模式 | 说明 | 适用场景 |
+|------|------|----------|
+| `default` | 内存缓存模式 (默认) | 单进程部署,重启后缓存丢失 |
+| `file` | 文件缓存模式 | 多进程部署,缓存持久化到 `./caches/` 目录 |
+
+**推荐配置:**
+- **单进程部署**: 使用 `CACHE_MODE=default`,性能最佳
+- **多进程/集群部署**: 使用 `CACHE_MODE=file`,确保进程间缓存共享
+- **Docker 部署**: 建议使用 `CACHE_MODE=file` 并挂载 `./caches` 目录
+
+**文件缓存目录结构:**
+```
+caches/
+├── [signature1].txt # 缓存文件,包含图片URL
+├── [signature2].txt
+└── ...
+```
+
---
-title: Qwen2API A
-emoji: 😻
-colorFrom: pink
-colorTo: red
-sdk: docker
-pinned: false
+
+## 🚀 部署方式
+
+### 🐳 Docker 部署
+
+#### 方式一:直接运行
+
+```bash
+docker run -d \
+ -p 3000:3000 \
+ -e API_KEY=sk-admin123,sk-user456,sk-user789 \
+ -e DATA_SAVE_MODE=none \
+ -e CACHE_MODE=file \
+ -e ACCOUNTS= \
+ -v ./caches:/app/caches \
+ --name qwen2api \
+ rfym21/qwen2api:latest
+```
+
+#### 方式二:Docker Compose
+
+```bash
+# 下载配置文件
+curl -o docker-compose.yml https://raw.githubusercontent.com/Rfym21/Qwen2API/refs/heads/main/docker/docker-compose.yml
+
+# 启动服务
+docker compose pull && docker compose up -d
+```
+
+### 📦 本地部署
+
+```bash
+# 克隆项目
+git clone https://github.com/Rfym21/Qwen2API.git
+cd Qwen2API
+
+# 安装依赖
+npm install
+
+# 配置环境变量
+cp .env.example .env
+# 编辑 .env 文件
+
+# 智能启动 (推荐 - 自动判断单进程/多进程)
+npm start
+
+# 开发模式
+npm run dev
+```
+
+### 🚀 PM2 多进程部署
+
+使用 PM2 进行生产环境多进程部署,提供更好的性能和稳定性。
+
+**重要说明**: PM2 集群模式下,所有进程共用同一个端口,PM2 会自动进行负载均衡。
+
+### 🤖 智能启动模式
+
+使用 `npm start` 可以自动判断启动方式:
+
+- 当 `PM2_INSTANCES=1` 时,使用单进程模式
+- 当 `PM2_INSTANCES>1` 时,使用 Node.js 集群模式
+- 自动限制进程数不超过 CPU 核心数
+
+### ☁️ Hugging Face 部署
+
+推荐使用 **Docker Space + GitHub 自动同步** 的方式部署。这个项目本身已经提供 `docker/Dockerfile`,因此在 Hugging Face 上使用 Docker Space 是最稳妥的方案。
+
+[](https://huggingface.co/spaces/devme/q2waepnilm)
+
+
+

+
+
+#### 推荐部署方式
+
+部署链路如下:
+
+```text
+本地修改代码 -> Push 到 GitHub main -> GitHub Actions 自动同步到 Hugging Face Space -> Hugging Face 自动重建并启动
+```
+
+#### 第一步:创建 Hugging Face Space
+
+- 在 Hugging Face 新建 Space
+- **SDK 请选择 `Docker`**
+- Space 名称示例:`DanielleNguyen/Qwen2API-A`
+
+#### 第二步:在 GitHub 配置自动同步变量
+
+仓库中已提供自动同步工作流:`.github/workflows/huggingface-sync.yml`
+
+请在 GitHub 仓库中配置以下内容:
+
+**GitHub Actions Secrets:**
+
+```bash
+HF_TOKEN=你的_huggingface_token
+```
+
+**GitHub Actions Variables:**
+
+```bash
+HF_SPACE_ID=DanielleNguyen/Qwen2API-A
+```
+
+配置位置:`GitHub 仓库 -> Settings -> Secrets and variables -> Actions`
+
+> ⚠️ 注意:`HF_TOKEN` 必须放在 `Secrets` 中,不要写入仓库文件,也不要提交到 `.env` 中。
+
+#### 第三步:在 Hugging Face Space 配置运行时变量
+
+进入:`Hugging Face Space -> Settings -> Variables and secrets`
+
+如果你采用 **HF Bucket 持久化**,建议至少配置以下变量:
+
+```bash
+SERVICE_PORT=7860
+LISTEN_ADDRESS=0.0.0.0
+PM2_INSTANCES=1
+PM2_MAX_MEMORY=4G
+NODE_ENV=production
+OUTPUT_THINK=true
+SEARCH_INFO_MODE=table
+SIMPLE_MODEL_MAP=false
+DATA_SAVE_MODE=file
+DATA_DIR=/data/qwen2api/data
+CACHE_DIR=/data/qwen2api/caches
+LOG_DIR=/data/qwen2api/logs
+HF_BUCKET_REPO=DanielleNguyen/Qwen2API-A-Storage
+HF_BUCKET_LOCAL_DIR=/data/qwen2api
+HF_BUCKET_REMOTE_DIR=runtime
+HF_BUCKET_SYNC_INTERVAL=300
+LOG_LEVEL=INFO
+ENABLE_FILE_LOG=false
+CACHE_MODE=file
+```
+
+建议作为 Secret 配置的敏感项:
+
+```bash
+API_KEY=sk-admin-yourkey,sk-user-yourkey
+HF_TOKEN=hf_xxx
+HF_BUCKET_TOKEN=
+REDIS_URL=
+ACCOUNTS=
+PROXY_URL=
+QWEN_CHAT_PROXY_URL=
+QWEN_CLI_PROXY_URL=
+```
+
+> 💡 项目中已提供 Hugging Face 示例模板:`.env.hf.example`
+
+#### 第四步:HF Bucket 持久化工作方式
+
+当前项目已适配如下持久化链路:
+
+```text
+容器启动 -> 从 HF Bucket 拉取 /data/qwen2api -> 应用用 file 模式读写本地文件 -> 后台定时同步回 HF Bucket
+```
+
+默认会持久化这些目录:
+
+```bash
+DATA_DIR=/data/qwen2api/data
+CACHE_DIR=/data/qwen2api/caches
+LOG_DIR=/data/qwen2api/logs
+```
+
+推荐的 Bucket 名称:
+
+```bash
+HF_BUCKET_REPO=DanielleNguyen/Qwen2API-A-Storage
+```
+
+#### 第五步:端口与监听地址说明
+
+为兼容 Hugging Face Docker Space,推荐固定使用:
+
+```bash
+SERVICE_PORT=7860
+LISTEN_ADDRESS=0.0.0.0
+```
+
+原因:
+
+- `7860` 是 Hugging Face Space 常见服务端口
+- `0.0.0.0` 可确保容器外部可以访问到服务
+- 项目实际启动端口由环境变量 `SERVICE_PORT` 控制
+
+#### 第六步:推送代码触发自动部署
+
+当你推送代码到 `main` 分支后:
+
+- GitHub Actions 会自动执行 `.github/workflows/huggingface-sync.yml`
+- 自动将仓库代码同步到 Hugging Face Space
+- Hugging Face 收到新代码后会自动重建容器并启动服务
+
+#### 推荐的 Hugging Face 配置
+
+如果你只是先跑通服务并启用 Bucket 持久化,建议使用:
+
+```bash
+SERVICE_PORT=7860
+LISTEN_ADDRESS=0.0.0.0
+PM2_INSTANCES=1
+DATA_SAVE_MODE=file
+CACHE_MODE=file
+DATA_DIR=/data/qwen2api/data
+CACHE_DIR=/data/qwen2api/caches
+LOG_DIR=/data/qwen2api/logs
+HF_BUCKET_REPO=DanielleNguyen/Qwen2API-A-Storage
+ENABLE_FILE_LOG=false
+```
+
+如果你要让运行数据真正可恢复,请再补上 Secret:
+
+```bash
+HF_TOKEN=你的_huggingface_token
+```
+
+> Space 访问 Bucket 时可以直接使用 Hugging Face 账号 Token。若你希望和 GitHub Actions 用途隔离,也可以额外配置 `HF_BUCKET_TOKEN` 专供 Bucket 读写使用。
+
+#### 常见问题
+
+**1. 为什么不建议在仓库里提交真实 `.env`?**
+
+因为 `API_KEY`、`HF_TOKEN`、`HF_BUCKET_TOKEN`、`REDIS_URL` 等都属于敏感信息,应该放在 GitHub Secrets 或 Hugging Face Secrets 中。
+
+**2. 为什么推荐 Docker Space?**
+
+因为本项目是完整的 Node.js 服务,并且已经自带 `docker/Dockerfile`,使用 Docker Space 可以直接复用现有构建和启动流程。
+
+**3. 如果 GitHub 已经配置了 `HF_TOKEN` 和 `HF_SPACE_ID`,还需要在 HF 里配置它们吗?**
+
+不需要。
+
+- `HF_TOKEN` 和 `HF_SPACE_ID` 只用于 **GitHub -> Hugging Face 同步代码**
+- Hugging Face Space 内只需要配置项目运行时环境变量,例如 `API_KEY`、`SERVICE_PORT`、`LISTEN_ADDRESS`
+
+**4. Space 怎么和 Bucket 通信?一定要单独的 `HF_BUCKET_TOKEN` 吗?**
+
+不一定。
+
+- 在 Hugging Face Space 里,运行时可以直接使用你的 Hugging Face 账号 Token,即 `HF_TOKEN`
+- 当前项目已兼容:优先读取 `HF_BUCKET_TOKEN`,如果没设置则自动回退到 `HF_TOKEN`
+- 如果你想把“GitHub 同步代码”和“Space 访问 Bucket”分开控制,可以单独再配 `HF_BUCKET_TOKEN`
+
---
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+## 📁 项目结构
+
+```
+Qwen2API/
+├── README.md
+├── ecosystem.config.js # PM2配置文件
+├── package.json
+│
+├── docker/ # Docker配置目录
+│ ├── Dockerfile
+│ ├── docker-compose.yml
+│ └── docker-compose-redis.yml
+│
+├── caches/ # 缓存文件目录
+├── data/ # 数据文件目录
+│ ├── data.json
+│ └── data_template.json
+├── scripts/ # 脚本目录
+│ └── fingerprint-injector.js # 浏览器指纹注入脚本
+│
+├── src/ # 后端源代码目录
+│ ├── server.js # 主服务器文件
+│ ├── start.js # 智能启动脚本 (自动判断单进程/多进程)
+│ ├── config/
+│ │ └── index.js # 配置文件
+│ ├── controllers/ # 控制器目录
+│ │ ├── chat.js # 聊天控制器
+│ │ ├── chat.image.video.js # 图片/视频生成控制器
+│ │ ├── cli.chat.js # CLI聊天控制器
+│ │ └── models.js # 模型控制器
+│ ├── middlewares/ # 中间件目录
+│ │ ├── authorization.js # 授权中间件
+│ │ └── chat-middleware.js # 聊天中间件
+│ ├── models/ # 模型目录
+│ │ └── models-map.js # 模型映射配置
+│ ├── routes/ # 路由目录
+│ │ ├── accounts.js # 账户路由
+│ │ ├── chat.js # 聊天路由
+│ │ ├── cli.chat.js # CLI聊天路由
+│ │ ├── models.js # 模型路由
+│ │ ├── settings.js # 设置路由
+│ │ └── verify.js # 验证路由
+│ └── utils/ # 工具函数目录
+│ ├── account-rotator.js # 账户轮询器
+│ ├── account.js # 账户管理
+│ ├── chat-helpers.js # 聊天辅助函数
+│ ├── cli.manager.js # CLI管理器
+│ ├── cookie-generator.js # Cookie生成器
+│ ├── data-persistence.js # 数据持久化
+│ ├── fingerprint.js # 浏览器指纹生成
+│ ├── img-caches.js # 图片缓存
+│ ├── logger.js # 日志工具
+│ ├── precise-tokenizer.js # 精确分词器
+│ ├── proxy-helper.js # 代理辅助函数
+│ ├── redis.js # Redis连接
+│ ├── request.js # HTTP请求封装
+│ ├── setting.js # 设置管理
+│ ├── ssxmod-manager.js # ssxmod参数管理
+│ ├── token-manager.js # Token管理器
+│ ├── tools.js # 工具调用处理
+│ └── upload.js # 文件上传
+│
+└── public/ # 前端项目目录
+ ├── dist/ # 编译后的前端文件
+ │ ├── assets/ # 静态资源
+ │ ├── favicon.png
+ │ └── index.html
+ ├── src/ # 前端源代码
+ │ ├── App.vue # 主应用组件
+ │ ├── main.js # 入口文件
+ │ ├── style.css # 全局样式
+ │ ├── assets/ # 静态资源
+ │ │ └── background.mp4
+ │ ├── routes/ # 路由配置
+ │ │ └── index.js
+ │ └── views/ # 页面组件
+ │ ├── auth.vue # 认证页面
+ │ ├── dashboard.vue # 仪表板页面
+ │ └── settings.vue # 设置页面
+ ├── package.json # 前端依赖配置
+ ├── package-lock.json
+ ├── index.html # 前端入口HTML
+ ├── postcss.config.js # PostCSS配置
+ ├── tailwind.config.js # TailwindCSS配置
+ ├── vite.config.js # Vite构建配置
+ └── public/ # 公共静态资源
+ └── favicon.png
+```
+
+## 📖 API 文档
+
+### 🔐 API 认证说明
+
+本API支持多密钥认证机制,所有API请求都需要在请求头中包含有效的API密钥:
+
+```http
+Authorization: Bearer sk-your-api-key
+```
+
+**支持的密钥类型:**
+- **管理员密钥**: 第一个配置的API_KEY,拥有完整权限
+- **普通密钥**: 其他配置的API_KEY,仅可调用API接口
+
+**认证示例:**
+```bash
+# 使用管理员密钥
+curl -H "Authorization: Bearer sk-admin123" http://localhost:3000/v1/models
+
+# 使用普通密钥
+curl -H "Authorization: Bearer sk-user456" http://localhost:3000/v1/chat/completions
+```
+
+### 🔍 获取模型列表
+
+获取所有可用的 AI 模型列表。
+
+```http
+GET /v1/models
+Authorization: Bearer sk-your-api-key
+```
+
+```http
+GET /models (免认证)
+```
+
+**响应示例:**
+```json
+{
+ "object": "list",
+ "data": [
+ {
+ "id": "qwen-max-latest",
+ "object": "model",
+ "created": 1677610602,
+ "owned_by": "qwen"
+ }
+ ]
+}
+```
+
+### 💬 聊天对话
+
+发送聊天消息并获取 AI 回复。
+
+```http
+POST /v1/chat/completions
+Content-Type: application/json
+Authorization: Bearer sk-your-api-key
+```
+
+**请求体:**
+```json
+{
+ "model": "qwen-max-latest",
+ "messages": [
+ {
+ "role": "system",
+ "content": "你是一个有用的助手。"
+ },
+ {
+ "role": "user",
+ "content": "你好,请介绍一下自己。"
+ }
+ ],
+ "stream": false,
+ "temperature": 0.7,
+ "max_tokens": 2000
+}
+```
+
+**响应示例:**
+```json
+{
+ "id": "chatcmpl-123",
+ "object": "chat.completion",
+ "created": 1677652288,
+ "model": "qwen-max-latest",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "你好!我是一个AI助手..."
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 20,
+ "completion_tokens": 50,
+ "total_tokens": 70
+ }
+}
+```
+
+### 🎨 图像生成/编辑
+
+使用 `-image` 模型启用文本到图像生成功能。
+使用 `-image-edit` 模型启用图像修改功能。
+当使用 `-image` 模型时你可以通过在请求体中添加 `size` 参数或在消息内容中包含特定关键词 `1:1`, `4:3`, `3:4`, `16:9`, `9:16` 来控制图片尺寸。
+
+```http
+POST /v1/chat/completions
+Content-Type: application/json
+Authorization: Bearer sk-your-api-key
+```
+
+**请求体:**
+```json
+{
+ "model": "qwen-max-latest-image",
+ "messages": [
+ {
+ "role": "user",
+ "content": "画一只在花园里玩耍的小猫咪,卡通风格"
+ }
+ ],
+ "size": "1:1",
+ "stream": false
+}
+```
+
+**支持的参数:**
+- `size`: 图片尺寸,支持 `"1:1"`、`"4:3"`、`"3:4"`、`"16:9"`、`"9:16"`
+- `stream`: 支持流式和非流式响应
+
+**响应示例:**
+```json
+{
+ "created": 1677652288,
+ "model": "qwen-max-latest",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": ""
+ },
+ "finish_reason": "stop"
+ }
+ ]
+}
+```
+
+### 🎯 高级功能
+
+#### 🔍 智能搜索模式
+
+在模型名称后添加 `-search` 后缀启用搜索功能:
+
+```json
+{
+ "model": "qwen-max-latest-search",
+ "messages": [...]
+}
+```
+
+#### 🧠 推理模式
+
+在模型名称后添加 `-thinking` 后缀启用思考过程输出:
+
+```json
+{
+ "model": "qwen-max-latest-thinking",
+ "messages": [...]
+}
+```
+
+#### 🔍🧠 组合模式
+
+同时启用搜索和推理功能:
+
+```json
+{
+ "model": "qwen-max-latest-thinking-search",
+ "messages": [...]
+}
+```
+
+#### 🎨 T2I 生图模式
+
+通过设置 `chat_type` 参数为 `t2i` 启用文本到图像生成功能:
+
+```json
+{
+ "model": "qwen-max-latest",
+ "chat_type": "t2i",
+ "messages": [
+ {
+ "role": "user",
+ "content": "画一只可爱的小猫咪"
+ }
+ ],
+ "size": "1:1"
+}
+```
+
+**支持的图片尺寸:** `1:1`、`4:3`、`3:4`、`16:9`、`9:16`
+
+**智能尺寸识别:** 系统会自动从提示词中识别尺寸关键词并设置对应尺寸
+
+#### 🖼️ 多模态支持
+
+API 自动处理图像上传,支持在对话中发送图片:
+
+```json
+{
+ "model": "qwen-max-latest",
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "这张图片里有什么?"
+ },
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": "data:image/jpeg;base64,..."
+ }
+ }
+ ]
+ }
+ ]
+}
+```
+
+### 🖥️ CLI 端点
+
+CLI 端点使用 Qwen Code / Qwen Cli 的 OAuth 令牌访问,支持 256K 上下文和工具调用(Function Calling)。
+
+**支持的模型:**
+
+| 模型 ID | 说明 |
+|---------|------|
+| `qwen3-coder-plus` | Qwen3 Coder Plus |
+| `qwen3-coder-flash` | Qwen3 Coder Flash(速度更快) |
+| `coder-model` | Qwen 3.5 Plus(带思维链,256K 上下文) |
+| `qwen3.5-plus` | `coder-model` 的别名,自动重定向 |
+
+#### 💬 CLI 聊天对话
+
+通过 CLI 端点发送聊天请求,支持流式和非流式响应。
+
+```http
+POST /cli/v1/chat/completions
+Content-Type: application/json
+Authorization: Bearer API_KEY
+```
+
+**请求体:**
+```json
+{
+ "model": "qwen3-coder-plus",
+ "messages": [
+ {
+ "role": "user",
+ "content": "你好,请介绍一下自己。"
+ }
+ ],
+ "stream": false,
+ "temperature": 0.7,
+ "max_tokens": 2000
+}
+```
+
+使用 `coder-model`(即 Qwen 3.5 Plus)或其别名 `qwen3.5-plus`:
+```json
+{
+ "model": "coder-model",
+ "messages": [
+ {
+ "role": "user",
+ "content": "写一个快速排序算法。"
+ }
+ ],
+ "stream": false
+}
+```
+
+**流式请求:**
+```json
+{
+ "model": "qwen3-coder-flash",
+ "messages": [
+ {
+ "role": "user",
+ "content": "写一首关于春天的诗。"
+ }
+ ],
+ "stream": true
+}
+```
+
+**响应格式:**
+
+非流式响应与标准 OpenAI API 格式相同:
+```json
+{
+ "id": "chatcmpl-123",
+ "object": "chat.completion",
+ "created": 1677652288,
+ "model": "qwen3-coder-plus",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "你好!我是一个AI助手..."
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 20,
+ "completion_tokens": 50,
+ "total_tokens": 70
+ }
+}
+```
+
+流式响应使用 Server-Sent Events (SSE) 格式:
+```
+data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"qwen3-coder-flash","choices":[{"index":0,"delta":{"content":"你好"},"finish_reason":null}]}
+
+data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"qwen3-coder-flash","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
+
+data: [DONE]
+```
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..646430448905f4b7ccac5c7906540f949bf9fd08
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,45 @@
+FROM node:lts-alpine
+
+ENV NODE_ENV=production
+ENV SERVICE_PORT=7860
+ENV LISTEN_ADDRESS=0.0.0.0
+
+RUN apk add --no-cache python3 py3-pip
+
+# 全局安装PM2
+RUN npm install -g pm2
+
+RUN pip install --no-cache-dir huggingface_hub watchdog
+
+WORKDIR /app
+
+# 复制package文件
+COPY package*.json ./
+
+# 安装依赖
+RUN npm install
+
+# 复制应用代码
+COPY . .
+
+# 构建前端应用
+RUN cd public && npm install && npm run build
+
+# 删除前端不必要文件
+RUN rm -rf public/src public/node_modules public/package*.json
+
+# 设置权限
+RUN chmod 777 /app
+
+# 创建日志目录
+RUN mkdir -p logs
+
+# 允许执行入口脚本
+RUN chmod +x /app/docker/entrypoint.sh
+
+# 暴露 Hugging Face Docker Space 默认端口
+# 应用实际监听端口仍由 SERVICE_PORT 控制
+EXPOSE 7860
+
+# 启动前自动从 HF Bucket 恢复数据,并在运行中后台同步
+ENTRYPOINT ["/app/docker/entrypoint.sh"]
diff --git a/docker/docker-compose-redis.yml b/docker/docker-compose-redis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b3082d628c7a9290c2de20a1174ddacc10b89de2
--- /dev/null
+++ b/docker/docker-compose-redis.yml
@@ -0,0 +1,51 @@
+services:
+ qwen2api:
+ container_name: qwen2api
+ image: rfym21/qwen2api:latest
+ restart: always
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./data:/app/data
+ - ./logs:/app/logs
+ environment:
+ # 如果需要修改Docker暴露端口,请修改ports中的参数
+ # 示例(8080:3000) 则访问 http://localhost:8080
+ - SERVICE_PORT=3000
+ # API 密钥 (非必填)
+ # 如果需要使用多账户或使用内置账户,请填写
+ - API_KEY=sk-123456
+ # 监听地址(非必填)
+ - LISTEN_ADDRESS=
+ # PM2 多进程配置
+ # PM2进程数量: max(使用所有CPU核心), 数字(指定进程数量), 1(单进程)
+ # 注意: PM2集群模式下所有进程共用同一个端口
+ - PM2_INSTANCES=1
+ # PM2内存限制,超过此限制将自动重启进程
+ - PM2_MAX_MEMORY=1G
+ # 搜索信息展示模式
+ # table: 使用折叠块和表格展示
+ # text: 使用纯文本
+ - SEARCH_INFO_MODE=table
+ # 是否输出思考过程
+ - OUTPUT_THINK=true
+ # 简化模型映射 (true: 只返回基础模型, false: 返回完整模型列表)
+ - SIMPLE_MODEL_MAP=false
+ # redis 连接地址(必填)
+ - REDIS_URL=redis://redis:6379
+ # 数据保存模式
+ - DATA_SAVE_MODE=redis
+ # 图片缓存
+ - CACHE_MODE=default
+ redis:
+ image: redis:7.2-alpine
+ container_name: redis_qwen2api
+ restart: always
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis-data:/data
+
+volumes:
+ redis-data:
+
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..09c6d2bcccb4d9da30f3db9c450d3ff9c2121b0c
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,41 @@
+services:
+ qwen2api:
+ container_name: qwen2api
+ image: rfym21/qwen2api:latest
+ restart: always
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./data:/app/data
+ - ./logs:/app/logs
+ environment:
+ # 如果需要修改Docker暴露端口,请修改ports中的参数
+ # 示例(8080:3000) 则访问 http://localhost:8080
+ - SERVICE_PORT=3000
+ # API 密钥 (非必填)
+ # 如果需要使用多账户或使用内置账户,请填写
+ - API_KEY=sk-123456
+ # 监听地址(非必填)
+ - LISTEN_ADDRESS=
+ # PM2 多进程配置
+ # PM2进程数量: max(使用所有CPU核心), 数字(指定进程数量), 1(单进程)
+ # 注意: PM2集群模式下所有进程共用同一个端口
+ - PM2_INSTANCES=1
+ # PM2内存限制,超过此限制将自动重启进程
+ - PM2_MAX_MEMORY=1G
+ # 搜索信息展示模式
+ # table: 使用折叠块和表格展示
+ # text: 使用纯文本
+ - SEARCH_INFO_MODE=table
+ # 是否输出思考过程
+ - OUTPUT_THINK=true
+ # 简化模型映射 (true: 只返回基础模型, false: 返回完整模型列表)
+ - SIMPLE_MODEL_MAP=false
+ # redis 连接地址(如果使用redis模式,则必填,当redis使用tls时将redis://替换为rediss://)
+ - REDIS_URL=
+ # 数据保存模式
+ - DATA_SAVE_MODE=none
+ # 账号(如果使用redis和file模式,则不需要填写)
+ - ACCOUNTS=
+ # 图片缓存
+ - CACHE_MODE=default
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100644
index 0000000000000000000000000000000000000000..cef94ee6645f5b540231f39e6f9ca517af1f6cb0
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -eu
+
+APP_DATA_ROOT="${HF_BUCKET_LOCAL_DIR:-/data/qwen2api}"
+export HF_BUCKET_LOCAL_DIR="$APP_DATA_ROOT"
+
+mkdir -p "$APP_DATA_ROOT/data" "$APP_DATA_ROOT/caches" "$APP_DATA_ROOT/logs"
+
+export DATA_DIR="${DATA_DIR:-$APP_DATA_ROOT/data}"
+export CACHE_DIR="${CACHE_DIR:-$APP_DATA_ROOT/caches}"
+export LOG_DIR="${LOG_DIR:-$APP_DATA_ROOT/logs}"
+export SERVICE_PORT="${SERVICE_PORT:-7860}"
+export LISTEN_ADDRESS="${LISTEN_ADDRESS:-0.0.0.0}"
+export HF_BUCKET_SYNC_INTERVAL="${HF_BUCKET_SYNC_INTERVAL:-300}"
+
+python3 /app/scripts/hf-bucket-sync.py restore || true
+python3 /app/scripts/hf-bucket-sync.py daemon &
+
+exec npm start
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..51f4f8bac03830f14fecf830d6cfab98ccc333c6
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,53 @@
+const os = require('os')
+
+// 获取CPU核心数
+const cpuCores = os.cpus().length
+
+// 解析进程数配置
+let instances = process.env.PM2_INSTANCES || 1
+if (instances === 'max') {
+ instances = cpuCores
+} else if (!isNaN(instances)) {
+ instances = parseInt(instances)
+} else {
+ instances = 1
+}
+
+// 限制进程数不能超过CPU核心数
+if (instances > cpuCores) {
+ console.log(`⚠️ 警告: 配置的进程数(${instances})超过CPU核心数(${cpuCores}),自动调整为${cpuCores}`)
+ instances = cpuCores
+}
+
+module.exports = {
+ apps: [{
+ name: 'qwen2api',
+ script: './src/server.js',
+ instances: instances,
+ exec_mode: 'cluster',
+
+ // 环境变量
+ env: {
+ PM2_USAGE: 'true'
+ },
+
+ // 日志配置
+ log_file: './logs/pm2-combined.log',
+ out_file: './logs/pm2-out.log',
+ error_file: './logs/pm2-error.log',
+ log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
+
+ // 进程管理配置
+ max_memory_restart: process.env.PM2_MAX_MEMORY || '1G',
+ min_uptime: '10s',
+ max_restarts: 10,
+
+ // 监听文件变化
+ watch: false,
+ ignore_watch: ['node_modules', 'logs', 'caches', 'data'],
+
+ // 其他配置
+ merge_logs: true,
+ time: true
+ }]
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..5ef0a26cce6f3367258f921641f1181d3e7174dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "qwen2api",
+ "version": "2026.03.04.10.58",
+ "main": "src/server.js",
+ "scripts": {
+ "start": "node src/start.js",
+ "dev": "nodemon src/server.js",
+ "pm2": "pm2 start ecosystem.config.js",
+ "pm2:stop": "pm2 stop qwen2api",
+ "pm2:restart": "pm2 restart qwen2api",
+ "pm2:reload": "pm2 reload qwen2api",
+ "pm2:delete": "pm2 delete qwen2api",
+ "pm2:logs": "pm2 logs qwen2api",
+ "pm2:status": "pm2 status",
+ "pm2:monit": "pm2 monit"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "ali-oss": "^6.22.0",
+ "axios": "^1.11.0",
+ "body-parser": "^1.20.3",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "form-data": "^4.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "ioredis": "^5.6.1",
+ "jwt-decode": "^4.0.0",
+ "mime-types": "^3.0.1",
+ "multer": "^1.4.5-lts.1",
+ "pm2": "^6.0.8",
+ "tiktoken": "^1.0.21"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.7"
+ }
+}
diff --git a/public/.gitignore b/public/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..251ce6d2bd9308e5975611a1ad18308ba6da8117
--- /dev/null
+++ b/public/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..11e703ecfa4a85d0171761b53c608b21aef195b1
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Qwen2 API Dashboard
+
+
+
+
+
+
diff --git a/public/package.json b/public/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..65221fa84ad62ca45007cfd2c32db6d327157c1e
--- /dev/null
+++ b/public/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "dashboard",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 6868",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.8.4",
+ "vue": "^3.4.29",
+ "vue-router": "^4.5.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.5",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.3",
+ "tailwindcss": "^3.4.3",
+ "vite": "^5.2.8"
+ }
+}
diff --git a/public/postcss.config.js b/public/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d
--- /dev/null
+++ b/public/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/public/src/App.vue b/public/src/App.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a8198c722f7c90c321a85abf5b9f629748bc5600
--- /dev/null
+++ b/public/src/App.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/src/main.js b/public/src/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..f637b031b63bb6a3f7b75236a4fad7ffb3d6262b
--- /dev/null
+++ b/public/src/main.js
@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import router from './routes/index.js'
+import App from './App.vue'
+import "./style.css"
+
+createApp(App)
+ .use(router)
+ .mount('#app')
diff --git a/public/src/routes/index.js b/public/src/routes/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..92d110952c3be169ec57027bf82da3139d821692
--- /dev/null
+++ b/public/src/routes/index.js
@@ -0,0 +1,74 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import axios from 'axios'
+
+const routes = [
+ {
+ name: 'dashboard',
+ path: '/',
+ component: () => import('../views/dashboard.vue')
+ },
+ {
+ name: 'auth',
+ path: '/auth',
+ component: () => import('../views/auth.vue')
+ },
+ {
+ name: 'settings',
+ path: '/settings',
+ component: () => import('../views/settings.vue')
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes
+})
+
+
+// 路由守卫
+router.beforeEach(async (to, from, next) => {
+
+ if (to.path === '/auth') {
+ next()
+ } else {
+ const apiKey = localStorage.getItem('apiKey')
+ if (!apiKey) {
+ alert('请先设置身份验证apiKey')
+ next({ path: '/auth' })
+ } else {
+ try {
+ const verifyResponse = await axios.post('/verify', {
+ apiKey: apiKey
+ })
+
+ if (verifyResponse.data.status === 200) {
+ const isAdmin = verifyResponse.data.isAdmin
+
+ // 存储用户权限信息
+ localStorage.setItem('isAdmin', isAdmin.toString())
+
+ // 检查是否需要管理员权限
+ if ((to.path === '/' || to.path === '/settings') && !isAdmin) {
+ alert('您没有访问管理页面的权限')
+ next({ path: '/auth' })
+ return
+ }
+
+ next()
+ } else {
+ localStorage.removeItem('apiKey')
+ localStorage.removeItem('isAdmin')
+ next({ path: '/auth' })
+ }
+ } catch (error) {
+ localStorage.removeItem('apiKey')
+ localStorage.removeItem('isAdmin')
+ next({ path: '/auth' })
+ }
+ }
+ }
+
+})
+
+
+export default router
\ No newline at end of file
diff --git a/public/src/style.css b/public/src/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..bd6213e1dfe6b0a79ce7d8b37d0d2dc70f0250bb
--- /dev/null
+++ b/public/src/style.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/public/src/views/auth.vue b/public/src/views/auth.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e745b3bebaa19ee6575c77eed2cd72c15e8279d2
--- /dev/null
+++ b/public/src/views/auth.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
管理员身份验证
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/src/views/dashboard.vue b/public/src/views/dashboard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bc0ff59448a510f60dc12f3bfe52b817df238721
--- /dev/null
+++ b/public/src/views/dashboard.vue
@@ -0,0 +1,1025 @@
+
+
+
+
+
Token Manager by 兜豆子
+
+
+
+
+
+
+ 系统设置
+
+
+
+
+
+
+
+ 每页显示:
+
+
+
+ 共 {{ totalItems }} 项
+
+ {{ currentPage }}/{{ totalPages }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ toast.message }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/src/views/settings.vue b/public/src/views/settings.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bf5b128ab354d37b8a2ecd697669f3b23e17bfa5
--- /dev/null
+++ b/public/src/views/settings.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
系统设置
+
+ 返回Token管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔐 普通密钥
+
+
+
+
+ 暂无普通密钥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用自动刷新
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用思考输出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 只返回基础模型,不包含thinking、search、image等变体
+
+
+
+
+
+
+
+
+
+
+
添加普通API Key
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/tailwind.config.js b/public/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff48818cbe8639b10e036feefb2a11415a2d50ed
--- /dev/null
+++ b/public/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
\ No newline at end of file
diff --git a/public/vite.config.js b/public/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2715d90f2afc8dc037e81918e68e73c36d812aea
--- /dev/null
+++ b/public/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ proxy: {
+ '/': {
+ target: 'http://localhost:4000', // 实际后端地址
+ changeOrigin: true,
+ }
+ }
+ }
+})
diff --git a/scripts/fingerprint-injector.js b/scripts/fingerprint-injector.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8a09639938a4278f3878663dc48b161839a6fc6
--- /dev/null
+++ b/scripts/fingerprint-injector.js
@@ -0,0 +1,20 @@
+(function() {
+ // 拦截 String.prototype.charAt
+ const originalCharAt = String.prototype.charAt;
+ let capturedData = null;
+
+ String.prototype.charAt = function(index) {
+ if (this.length > 200 && this.includes('^') && !capturedData) {
+ const fields = this.split('^');
+ if (fields.length === 37) {
+ capturedData = this.toString();
+ console.log('\n=== 检测到浏览器指纹 ===');
+ console.log(capturedData);
+
+ // 恢复原方法
+ String.prototype.charAt = originalCharAt;
+ }
+ }
+ return originalCharAt.call(this, index);
+ };
+})();
diff --git a/scripts/hf-bucket-sync.py b/scripts/hf-bucket-sync.py
new file mode 100644
index 0000000000000000000000000000000000000000..d880761b22801a3cdfabc132bdf858954608d11d
--- /dev/null
+++ b/scripts/hf-bucket-sync.py
@@ -0,0 +1,151 @@
+import os
+import sys
+import time
+import threading
+from pathlib import Path
+
+from huggingface_hub import sync_bucket
+from watchdog.events import FileSystemEventHandler
+from watchdog.observers import Observer
+
+
+BUCKET_REPO = os.environ.get("HF_BUCKET_REPO", "").strip()
+BUCKET_TOKEN = (
+ os.environ.get("HF_BUCKET_TOKEN", "").strip()
+ or os.environ.get("HF_TOKEN", "").strip()
+)
+LOCAL_ROOT = Path(os.environ.get("HF_BUCKET_LOCAL_DIR", "/data/qwen2api")).resolve()
+REMOTE_ROOT = os.environ.get("HF_BUCKET_REMOTE_DIR", "runtime").strip("/")
+SYNC_INTERVAL = int(os.environ.get("HF_BUCKET_SYNC_INTERVAL", "300"))
+STARTUP_GRACE_SECONDS = int(os.environ.get("HF_BUCKET_STARTUP_GRACE_SECONDS", "30"))
+SYNC_DEBOUNCE_SECONDS = int(os.environ.get("HF_BUCKET_SYNC_DEBOUNCE_SECONDS", "5"))
+
+
+def log(message: str) -> None:
+ print(f"[hf-bucket] {message}", flush=True)
+
+
+def bucket_path() -> str:
+ if REMOTE_ROOT:
+ return f"hf://buckets/{BUCKET_REPO}/{REMOTE_ROOT}"
+ return f"hf://buckets/{BUCKET_REPO}"
+
+
+def ensure_local_root() -> None:
+ LOCAL_ROOT.mkdir(parents=True, exist_ok=True)
+
+
+def can_use_bucket() -> bool:
+ if not BUCKET_REPO:
+ log("skip: HF_BUCKET_REPO 未设置")
+ return False
+ if not BUCKET_TOKEN:
+ log("skip: HF_TOKEN 或 HF_BUCKET_TOKEN 未设置")
+ return False
+ return True
+
+
+def restore() -> None:
+ if not can_use_bucket():
+ return
+
+ ensure_local_root()
+ try:
+ sync_bucket(bucket_path(), str(LOCAL_ROOT), token=BUCKET_TOKEN)
+ log(f"restore 完成: {bucket_path()} -> {LOCAL_ROOT}")
+ except Exception as exc:
+ log(f"restore 失败: {exc}")
+
+
+def push() -> None:
+ if not can_use_bucket():
+ return
+
+ ensure_local_root()
+ try:
+ sync_bucket(str(LOCAL_ROOT), bucket_path(), token=BUCKET_TOKEN, delete=False)
+ log(f"sync 完成: {LOCAL_ROOT} -> {bucket_path()}")
+ except Exception as exc:
+ log(f"sync 失败: {exc}")
+
+
+def daemon() -> None:
+ if not can_use_bucket():
+ return
+
+ class ChangeHandler(FileSystemEventHandler):
+ def __init__(self) -> None:
+ self._timer = None
+ self._lock = threading.Lock()
+ self._startup_time = time.time()
+
+ def _schedule(self) -> None:
+ if time.time() - self._startup_time < STARTUP_GRACE_SECONDS:
+ return
+
+ with self._lock:
+ if self._timer is not None:
+ self._timer.cancel()
+ self._timer = threading.Timer(SYNC_DEBOUNCE_SECONDS, push)
+ self._timer.daemon = True
+ self._timer.start()
+
+ def on_created(self, event):
+ if not event.is_directory:
+ self._schedule()
+
+ def on_modified(self, event):
+ if not event.is_directory:
+ self._schedule()
+
+ def on_deleted(self, event):
+ if not event.is_directory:
+ self._schedule()
+
+ def on_moved(self, event):
+ if not event.is_directory:
+ self._schedule()
+
+ observer = Observer()
+ observer.schedule(ChangeHandler(), str(LOCAL_ROOT), recursive=True)
+ observer.start()
+
+ log(
+ f"后台同步已启动,监听目录 {LOCAL_ROOT},即时同步防抖 {SYNC_DEBOUNCE_SECONDS} 秒,定时兜底 {SYNC_INTERVAL} 秒"
+ )
+
+ def periodic_sync() -> None:
+ while True:
+ time.sleep(SYNC_INTERVAL)
+ push()
+
+ periodic_thread = threading.Thread(target=periodic_sync, daemon=True)
+ periodic_thread.start()
+
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ observer.stop()
+ observer.join()
+
+
+def main() -> int:
+ command = sys.argv[1] if len(sys.argv) > 1 else "daemon"
+
+ if command == "restore":
+ restore()
+ return 0
+ if command == "push":
+ push()
+ return 0
+ if command == "daemon":
+ daemon()
+ return 0
+
+ log(f"未知命令: {command}")
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8497cb1efdd6bfdc1a0f9a65d6a9ddde4c423fff
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,52 @@
+const dotenv = require('dotenv')
+dotenv.config()
+const paths = require('../utils/paths')
+
+/**
+ * 解析API_KEY环境变量,支持逗号分隔的多个key
+ * @returns {Object} 包含apiKeys数组和adminKey的对象
+ */
+const parseApiKeys = () => {
+ const apiKeyEnv = process.env.API_KEY
+ if (!apiKeyEnv) {
+ return { apiKeys: [], adminKey: null }
+ }
+
+ const keys = apiKeyEnv.split(',').map(key => key.trim()).filter(key => key.length > 0)
+ return {
+ apiKeys: keys,
+ adminKey: keys.length > 0 ? keys[0] : null
+ }
+}
+
+const { apiKeys, adminKey } = parseApiKeys()
+
+const config = {
+ dataSaveMode: process.env.DATA_SAVE_MODE || "none",
+ apiKeys: apiKeys,
+ adminKey: adminKey,
+ simpleModelMap: process.env.SIMPLE_MODEL_MAP === 'true' ? true : false,
+ listenAddress: process.env.LISTEN_ADDRESS || null,
+ listenPort: process.env.SERVICE_PORT || 3000,
+ searchInfoMode: process.env.SEARCH_INFO_MODE === 'table' ? "table" : "text",
+ outThink: process.env.OUTPUT_THINK === 'true' ? true : false,
+ redisURL: process.env.REDIS_URL || null,
+ autoRefresh: true,
+ autoRefreshInterval: 6 * 60 * 60,
+ cacheMode: process.env.CACHE_MODE || "default",
+ logLevel: process.env.LOG_LEVEL || "INFO",
+ enableFileLog: process.env.ENABLE_FILE_LOG === 'true',
+ logDir: paths.logDir,
+ maxLogFileSize: parseInt(process.env.MAX_LOG_FILE_SIZE) || 10,
+ maxLogFiles: parseInt(process.env.MAX_LOG_FILES) || 5,
+ dataDir: paths.dataDir,
+ cacheDir: paths.cacheDir,
+ dataFilePath: paths.dataFilePath,
+ // 自定义反代URL配置
+ qwenChatProxyUrl: process.env.QWEN_CHAT_PROXY_URL || "https://chat.qwen.ai",
+ qwenCliProxyUrl: process.env.QWEN_CLI_PROXY_URL || "https://portal.qwen.ai",
+ // 代理配置
+ proxyUrl: process.env.PROXY_URL || null
+}
+
+module.exports = config
diff --git a/src/controllers/chat.image.video.js b/src/controllers/chat.image.video.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b636b9782b3ac9ad55ad3043c6e13823d6c5818
--- /dev/null
+++ b/src/controllers/chat.image.video.js
@@ -0,0 +1,357 @@
+const axios = require('axios')
+const { logger } = require('../utils/logger.js')
+const { setResponseHeaders } = require('./chat.js')
+const accountManager = require('../utils/account.js')
+const { sleep } = require('../utils/tools.js')
+const { generateChatID } = require('../utils/request.js')
+const { getSsxmodItna, getSsxmodItna2 } = require('../utils/ssxmod-manager')
+const { getProxyAgent, getChatBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
+
+/**
+ * 主要的聊天完成处理函数
+ * @param {object} req - Express 请求对象
+ * @param {object} res - Express 响应对象
+ */
+const handleImageVideoCompletion = async (req, res) => {
+ const { model, messages, size, chat_type } = req.body
+ // console.log(JSON.stringify(req.body.messages.filter(item => item.role == "user" || item.role == "assistant")))
+ const token = accountManager.getAccountToken()
+
+ try {
+
+ // 请求体模板
+ const reqBody = {
+ "stream": false,
+ "chat_id": null,
+ "model": model,
+ "messages": [
+ {
+ "role": "user",
+ "content": "",
+ "files": [],
+ "chat_type": chat_type,
+ "feature_config": {
+ "output_schema": "phase"
+ }
+ }
+ ]
+ }
+
+ const chat_id = await generateChatID(token, model)
+
+ if (!chat_id) {
+ // 如果生成chat_id失败,则返回错误
+ throw new Error()
+ } else {
+ reqBody.chat_id = chat_id
+ }
+
+ // 拿到用户最后一句消息
+ const _userPrompt = messages[messages.length - 1].content
+ if (!_userPrompt) {
+ throw new Error()
+ }
+
+ // 提取历史消息
+ const messagesHistory = messages.filter(item => item.role == "user" || item.role == "assistant")
+ // 聊天消息中所有图片url
+ const select_image_list = []
+
+ // 遍历模型回复消息,拿到所有图片
+ if (chat_type == "image_edit") {
+ for (const item of messagesHistory) {
+ if (item.role == "assistant") {
+ // 使用matchAll提取所有图片链接
+ const matches = [...item.content.matchAll(/!\[image\]\((.*?)\)/g)]
+ // 将所有匹配到的图片url添加到图片列表
+ for (const match of matches) {
+ select_image_list.push(match[1])
+ }
+ } else {
+ if (Array.isArray(item.content) && item.content.length > 0) {
+ for (const content of item.content) {
+ if (content.type == "image") {
+ select_image_list.push(content.image)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ //分情况处理
+ if (chat_type == 't2i' || chat_type == 't2v') {
+ if (Array.isArray(_userPrompt)) {
+ reqBody.messages[0].content = _userPrompt.map(item => item.type == "text" ? item.text : "").join("\n\n")
+ } else {
+ reqBody.messages[0].content = _userPrompt
+ }
+ } else if (chat_type == 'image_edit') {
+ if (!Array.isArray(_userPrompt)) {
+
+ if (messagesHistory.length === 1) {
+ reqBody.messages[0].chat_type = "t2i"
+ } else if (select_image_list.length >= 1) {
+ reqBody.messages[0].files.push({
+ "type": "image",
+ "url": select_image_list[select_image_list.length - 1]
+ })
+ }
+ reqBody.messages[0].content += _userPrompt
+ } else {
+ const texts = _userPrompt.filter(item => item.type == "text")
+ if (texts.length === 0) {
+ throw new Error()
+ }
+ // 拼接提示词
+ for (const item of texts) {
+ reqBody.messages[0].content += item.text
+ }
+
+ const files = _userPrompt.filter(item => item.type == "image")
+ // 如果图片为空,则设置为t2i
+ if (files.length === 0) {
+ reqBody.messages[0].chat_type = "t2i"
+ }
+ // 遍历图片
+ for (const item of files) {
+ reqBody.messages[0].files.push({
+ "type": "image",
+ "url": item.image
+ })
+ }
+
+ }
+ }
+
+
+ // 处理图片视频尺寸
+ if (chat_type == 't2i' || chat_type == 't2v') {
+ // 获取图片尺寸,优先级 参数 > 提示词 > 默认
+ if (size != undefined && size != null) {
+ reqBody.size = "1:1"
+ } else if (_userPrompt.indexOf("@4:3") != -1) {
+ reqBody.size = "4:3"//"1024*768"
+ } else if (_userPrompt.indexOf("@3:4") != -1) {
+ reqBody.size = "3:4"//"768*1024"
+ } else if (_userPrompt.indexOf("@16:9") != -1) {
+ reqBody.size = "16:9"//"1280*720"
+ } else if (_userPrompt.indexOf("@9:16") != -1) {
+ reqBody.size = "9:16"//"720*1280"
+ }
+ }
+
+ const chatBaseUrl = getChatBaseUrl()
+ const proxyAgent = getProxyAgent()
+
+ logger.info('发送图片视频请求', 'CHAT')
+ logger.info(`选择图片: ${select_image_list[select_image_list.length - 1] || "未选择图片,切换生成图/视频模式"}`, 'CHAT')
+ logger.info(`使用提示: ${reqBody.messages[0].content}`, 'CHAT')
+ // console.log(JSON.stringify(reqBody))
+ const newChatType = reqBody.messages[0].chat_type
+
+ const requestConfig = {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
+ "Connection": "keep-alive",
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br, zstd",
+ "Content-Type": "application/json",
+ "Timezone": "Mon Dec 08 2025 17:28:55 GMT+0800",
+ "sec-ch-ua": "\"Microsoft Edge\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
+ "source": "web",
+ "Version": "0.1.13",
+ "bx-v": "2.5.31",
+ "Origin": chatBaseUrl,
+ "Sec-Fetch-Site": "same-origin",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Dest": "empty",
+ "Referer": `${chatBaseUrl}/c/guest`,
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
+ "Cookie": `ssxmod_itna=${getSsxmodItna()};ssxmod_itna2=${getSsxmodItna2()}`,
+ },
+ responseType: newChatType == 't2i' ? 'stream' : 'json',
+ timeout: 1000 * 60 * 5
+ }
+
+ // 添加代理配置
+ if (proxyAgent) {
+ requestConfig.httpsAgent = proxyAgent
+ requestConfig.proxy = false
+ }
+
+ const response_data = await axios.post(`${chatBaseUrl}/api/v2/chat/completions?chat_id=${chat_id}`, reqBody, requestConfig)
+
+ try {
+ let contentUrl = null
+ if (newChatType == 't2i') {
+ const decoder = new TextDecoder('utf-8')
+ response_data.data.on('data', async (chunk) => {
+ const data = decoder.decode(chunk, { stream: true }).split('\n').filter(item => item.trim() != "")
+ console.log(data)
+ for (const item of data) {
+ const jsonObj = JSON.parse(item.replace("data:", '').trim())
+ if (jsonObj && jsonObj.choices && jsonObj.choices[0] && jsonObj.choices[0].delta && jsonObj.choices[0].delta.content.trim() != "" && contentUrl == null) {
+ contentUrl = jsonObj.choices[0].delta.content
+ }
+ }
+ })
+
+ response_data.data.on('end', () => {
+ return returnResponse(res, model, contentUrl, req.body.stream)
+ })
+ } else if (newChatType == 'image_edit') {
+ console.log(response_data.data)
+ contentUrl = response_data.data?.data?.choices[0]?.message?.content[0]?.image
+ return returnResponse(res, model, contentUrl, req.body.stream)
+ } else if (newChatType == 't2v') {
+ return handleVideoCompletion(req, res, response_data.data, token)
+ }
+
+ } catch (error) {
+ logger.error('图片处理错误', 'CHAT', error)
+ res.status(500).json({ error: "服务错误!!!" })
+ }
+
+ } catch (error) {
+ res.status(500).json({
+ error: "服务错误,请稍后再试"
+ })
+ }
+}
+
+/**
+ * 返回响应
+ * @param {*} res
+ * @param {*} model
+ * @param {*} contentUrl
+ */
+const returnResponse = (res, model, contentUrl, stream) => {
+ setResponseHeaders(res, stream)
+ logger.info(`返回响应: ${contentUrl}`, 'CHAT')
+
+ const returnBody = {
+ "created": new Date().getTime(),
+ "model": model,
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": ``
+ }
+ }
+ ]
+ }
+
+ if (stream) {
+ res.write(`data: ${JSON.stringify(returnBody)}\n\n`)
+ res.write(`data: [DONE]\n\n`)
+ res.end()
+ } else {
+ res.json(returnBody)
+ }
+}
+
+const handleVideoCompletion = async (req, res, response_data, token) => {
+ try {
+ const videoTaskID = response_data?.data?.messages[0]?.extra?.wanx?.task_id
+ if (!response_data || !response_data.success || !videoTaskID) {
+ throw new Error()
+ }
+
+ logger.info(`视频任务ID: ${videoTaskID}`, 'CHAT')
+ const returnBody = {
+ "id": `chatcmpl-${new Date().getTime()}`,
+ "object": "chat.completion.chunk",
+ "created": new Date().getTime(),
+ "model": response_data.data.model,
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": ""
+ },
+ "finish_reason": null
+ }
+ ]
+ }
+
+ // 设置尝试次数
+ const maxAttempts = 60
+ // 设置每次请求的间隔时间
+ const delay = 20 * 1000
+ // 循环尝试获取任务状态
+ for (let i = 0; i < maxAttempts; i++) {
+ const content = await getVideoTaskStatus(videoTaskID, token)
+ if (content) {
+ returnBody.choices[0].message.content = `
+
+
+[Download Video](${content})
+`
+ // 设置响应头
+ setResponseHeaders(res, req.body.stream)
+
+ if (req.body.stream) {
+ res.write(`data: ${JSON.stringify(returnBody)}\n\n`)
+ res.write(`data: [DONE]\n\n`)
+ res.end()
+ } else {
+ res.json(returnBody)
+ }
+ return
+ } else if (content == null && req.body.stream) {
+ // 发送空数据保活
+ res.write(`data: ${JSON.stringify(returnBody)}\n\n`)
+ }
+
+ await sleep(delay)
+ }
+ } catch (error) {
+ logger.error('获取视频任务状态失败', 'CHAT', error)
+ res.status(500).json({ error: error.response_data?.data?.code || "可能该帐号今日生成次数已用完" })
+ }
+}
+
+const getVideoTaskStatus = async (videoTaskID, token) => {
+ try {
+ const chatBaseUrl = getChatBaseUrl()
+ const proxyAgent = getProxyAgent()
+
+ const requestConfig = {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ ...(getSsxmodItna() && { 'Cookie': `ssxmod_itna=${getSsxmodItna()};ssxmod_itna2=${getSsxmodItna2()}` })
+ }
+ }
+
+ // 添加代理配置
+ if (proxyAgent) {
+ requestConfig.httpsAgent = proxyAgent
+ requestConfig.proxy = false
+ }
+
+ const response_data = await axios.get(`${chatBaseUrl}/api/v1/tasks/status/${videoTaskID}`, requestConfig)
+
+ if (response_data.data?.task_status == "success") {
+ logger.info('获取视频任务状态成功', 'CHAT', response_data.data?.content)
+ return response_data.data?.content
+ }
+ logger.info(`获取视频任务 ${videoTaskID} 状态: ${response_data.data?.task_status}`, 'CHAT')
+ return null
+ } catch (error) {
+ console.log(error.response.data)
+ return null
+ }
+}
+
+module.exports = {
+ handleImageVideoCompletion
+}
\ No newline at end of file
diff --git a/src/controllers/chat.js b/src/controllers/chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ba803c5c6c645014fbfbd07950d3547cbe17d24
--- /dev/null
+++ b/src/controllers/chat.js
@@ -0,0 +1,449 @@
+const { isJson, generateUUID } = require('../utils/tools.js')
+const { createUsageObject } = require('../utils/precise-tokenizer.js')
+const { sendChatRequest } = require('../utils/request.js')
+const accountManager = require('../utils/account.js')
+const config = require('../config/index.js')
+const axios = require('axios')
+const { logger } = require('../utils/logger')
+
+/**
+ * 设置响应头
+ * @param {object} res - Express 响应对象
+ * @param {boolean} stream - 是否流式响应
+ */
+const setResponseHeaders = (res, stream) => {
+ try {
+ if (stream) {
+ res.set({
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ })
+ } else {
+ res.set({
+ 'Content-Type': 'application/json',
+ })
+ }
+ } catch (e) {
+ logger.error('处理聊天请求时发生错误', 'CHAT', '', e)
+ }
+}
+
+/**
+ * 处理流式响应
+ * @param {object} res - Express 响应对象
+ * @param {object} response - 上游响应流
+ * @param {boolean} enable_thinking - 是否启用思考模式
+ * @param {boolean} enable_web_search - 是否启用网络搜索
+ * @param {object} requestBody - 原始请求体,用于提取prompt信息
+ */
+const handleStreamResponse = async (res, response, enable_thinking, enable_web_search, requestBody = null) => {
+ try {
+ const message_id = generateUUID()
+ const decoder = new TextDecoder('utf-8')
+ let web_search_info = null
+ let thinking_start = false
+ let thinking_end = false
+ let buffer = ''
+
+ // Token消耗量统计
+ let totalTokens = {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0
+ }
+ let completionContent = '' // 收集完整的回复内容用于token估算
+
+ // 提取prompt文本用于token估算
+ let promptText = ''
+ if (requestBody && requestBody.messages) {
+ promptText = requestBody.messages.map(msg => {
+ if (typeof msg.content === 'string') {
+ return msg.content
+ } else if (Array.isArray(msg.content)) {
+ return msg.content.map(item => item.text || '').join('')
+ }
+ return ''
+ }).join('\n')
+ }
+
+ response.on('data', async (chunk) => {
+ const decodeText = decoder.decode(chunk, { stream: true })
+ // console.log(decodeText)
+ buffer += decodeText
+
+ const chunks = []
+ let startIndex = 0
+
+ while (true) {
+ const dataStart = buffer.indexOf('data: ', startIndex)
+ if (dataStart === -1) break
+
+ const dataEnd = buffer.indexOf('\n\n', dataStart)
+ if (dataEnd === -1) break
+
+ const dataChunk = buffer.substring(dataStart, dataEnd).trim()
+ chunks.push(dataChunk)
+
+ startIndex = dataEnd + 2
+ }
+
+ if (startIndex > 0) {
+ buffer = buffer.substring(startIndex)
+ }
+
+ for (const item of chunks) {
+ try {
+ let dataContent = item.replace("data: ", '')
+ let decodeJson = isJson(dataContent) ? JSON.parse(dataContent) : null
+ if (decodeJson === null || !decodeJson.choices || decodeJson.choices.length === 0) {
+ continue
+ }
+
+ // 提取真实的usage信息(如果上游API提供)
+ if (decodeJson.usage) {
+ totalTokens = {
+ prompt_tokens: decodeJson.usage.prompt_tokens || totalTokens.prompt_tokens,
+ completion_tokens: decodeJson.usage.completion_tokens || totalTokens.completion_tokens,
+ total_tokens: decodeJson.usage.total_tokens || totalTokens.total_tokens
+ }
+ }
+
+ // 处理 web_search 信息
+ if (decodeJson.choices[0].delta && decodeJson.choices[0].delta.name === 'web_search') {
+ web_search_info = decodeJson.choices[0].delta.extra.web_search_info
+ }
+
+ if (!decodeJson.choices[0].delta || !decodeJson.choices[0].delta.content ||
+ (decodeJson.choices[0].delta.phase !== 'think' && decodeJson.choices[0].delta.phase !== 'answer')) {
+ continue
+ }
+
+ let content = decodeJson.choices[0].delta.content
+ completionContent += content // 累计完整内容用于token估算
+
+ if (decodeJson.choices[0].delta.phase === 'think' && !thinking_start) {
+ thinking_start = true
+ if (web_search_info) {
+ content = `\n\n${await accountManager.generateMarkdownTable(web_search_info, config.searchInfoMode)}\n\n${content}`
+ } else {
+ content = `\n\n${content}`
+ }
+ }
+ if (decodeJson.choices[0].delta.phase === 'answer' && !thinking_end && thinking_start) {
+ thinking_end = true
+ content = `\n\n\n${content}`
+ }
+
+ const StreamTemplate = {
+ "id": `chatcmpl-${message_id}`,
+ "object": "chat.completion.chunk",
+ "created": new Date().getTime(),
+ "choices": [
+ {
+ "index": 0,
+ "delta": {
+ "content": content
+ },
+ "finish_reason": null
+ }
+ ]
+ }
+
+ res.write(`data: ${JSON.stringify(StreamTemplate)}\n\n`)
+ } catch (error) {
+ logger.error('流式数据处理错误', 'CHAT', '', error)
+ res.status(500).json({ error: "服务错误!!!" })
+ }
+ }
+ })
+
+ response.on('end', async () => {
+ try {
+ // 处理最终的搜索信息
+ if ((config.outThink === false || !enable_thinking) && web_search_info && config.searchInfoMode === "text") {
+ const webSearchTable = await accountManager.generateMarkdownTable(web_search_info, "text")
+ res.write(`data: ${JSON.stringify({
+ "id": `chatcmpl-${message_id}`,
+ "object": "chat.completion.chunk",
+ "created": new Date().getTime(),
+ "choices": [
+ {
+ "index": 0,
+ "delta": {
+ "content": `\n\n---\n${webSearchTable}`
+ },
+ "finish_reason": null
+ }
+ ]
+ })}\n\n`)
+ }
+
+ // 计算最终的token使用量
+ if (totalTokens.prompt_tokens === 0 && totalTokens.completion_tokens === 0) {
+ totalTokens = createUsageObject(requestBody?.messages || promptText, completionContent, null)
+ logger.info(`流式使用tiktoken计算 - Prompt: ${totalTokens.prompt_tokens}, Completion: ${totalTokens.completion_tokens}, Total: ${totalTokens.total_tokens}`, 'CHAT')
+ } else {
+ logger.info(`流式使用上游真实Token - Prompt: ${totalTokens.prompt_tokens}, Completion: ${totalTokens.completion_tokens}, Total: ${totalTokens.total_tokens}`, 'CHAT')
+ }
+
+ // 确保token数量的有效性
+ totalTokens.prompt_tokens = Math.max(0, totalTokens.prompt_tokens || 0)
+ totalTokens.completion_tokens = Math.max(0, totalTokens.completion_tokens || 0)
+ totalTokens.total_tokens = totalTokens.prompt_tokens + totalTokens.completion_tokens
+
+ // 发送最终的finish chunk,包含finish_reason
+ res.write(`data: ${JSON.stringify({
+ "id": `chatcmpl-${message_id}`,
+ "object": "chat.completion.chunk",
+ "created": new Date().getTime(),
+ "choices": [
+ {
+ "index": 0,
+ "delta": {},
+ "finish_reason": "stop"
+ }
+ ]
+ })}\n\n`)
+
+ // 发送usage信息chunk(符合OpenAI API标准)
+ res.write(`data: ${JSON.stringify({
+ "id": `chatcmpl-${message_id}`,
+ "object": "chat.completion.chunk",
+ "created": new Date().getTime(),
+ "choices": [],
+ "usage": totalTokens
+ })}\n\n`)
+
+ // 发送结束标记
+ res.write(`data: [DONE]\n\n`)
+ res.end()
+ } catch (e) {
+ logger.error('流式响应处理错误', 'CHAT', '', e)
+ res.status(500).json({ error: "服务错误!!!" })
+ }
+ })
+ } catch (error) {
+ logger.error('聊天处理错误', 'CHAT', '', error)
+ res.status(500).json({ error: "服务错误!!!" })
+ }
+}
+
+/**
+ * 处理非流式响应(从流式数据累积完整响应)
+ * @param {object} res - Express 响应对象
+ * @param {object} response - 上游响应流
+ * @param {boolean} enable_thinking - 是否启用思考模式
+ * @param {boolean} enable_web_search - 是否启用网络搜索
+ * @param {string} model - 模型名称
+ * @param {object} requestBody - 原始请求体,用于提取prompt信息
+ */
+const handleNonStreamResponse = async (res, response, enable_thinking, enable_web_search, model, requestBody = null) => {
+ try {
+ const decoder = new TextDecoder('utf-8')
+ let buffer = ''
+ let fullContent = ''
+ let web_search_info = null
+ let thinking_start = false
+ let thinking_end = false
+
+ // Token消耗量统计
+ let totalTokens = {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ total_tokens: 0
+ }
+
+ // 提取prompt文本用于token估算
+ let promptText = ''
+ if (requestBody && requestBody.messages) {
+ promptText = requestBody.messages.map(msg => {
+ if (typeof msg.content === 'string') {
+ return msg.content
+ } else if (Array.isArray(msg.content)) {
+ return msg.content.map(item => item.text || '').join('')
+ }
+ return ''
+ }).join('\n')
+ }
+
+ // 处理流式响应并累积内容
+ await new Promise((resolve, reject) => {
+ response.on('data', async (chunk) => {
+ const decodeText = decoder.decode(chunk, { stream: true })
+ buffer += decodeText
+
+ const chunks = []
+ let startIndex = 0
+
+ while (true) {
+ const dataStart = buffer.indexOf('data: ', startIndex)
+ if (dataStart === -1) break
+
+ const dataEnd = buffer.indexOf('\n\n', dataStart)
+ if (dataEnd === -1) break
+
+ const dataChunk = buffer.substring(dataStart, dataEnd).trim()
+ chunks.push(dataChunk)
+
+ startIndex = dataEnd + 2
+ }
+
+ if (startIndex > 0) {
+ buffer = buffer.substring(startIndex)
+ }
+
+ for (const item of chunks) {
+ try {
+ let dataContent = item.replace("data: ", '')
+ let decodeJson = isJson(dataContent) ? JSON.parse(dataContent) : null
+ if (decodeJson === null || !decodeJson.choices || decodeJson.choices.length === 0) {
+ continue
+ }
+
+ // 提取真实的usage信息(如果上游API提供)
+ if (decodeJson.usage) {
+ totalTokens = {
+ prompt_tokens: decodeJson.usage.prompt_tokens || totalTokens.prompt_tokens,
+ completion_tokens: decodeJson.usage.completion_tokens || totalTokens.completion_tokens,
+ total_tokens: decodeJson.usage.total_tokens || totalTokens.total_tokens
+ }
+ }
+
+ // 处理 web_search 信息
+ if (decodeJson.choices[0].delta && decodeJson.choices[0].delta.name === 'web_search') {
+ web_search_info = decodeJson.choices[0].delta.extra.web_search_info
+ }
+
+ if (!decodeJson.choices[0].delta || !decodeJson.choices[0].delta.content ||
+ (decodeJson.choices[0].delta.phase !== 'think' && decodeJson.choices[0].delta.phase !== 'answer')) {
+ continue
+ }
+
+ let content = decodeJson.choices[0].delta.content
+
+ // 处理thinking模式
+ if (decodeJson.choices[0].delta.phase === 'think' && !thinking_start) {
+ thinking_start = true
+ if (web_search_info) {
+ const webSearchTable = await accountManager.generateMarkdownTable(web_search_info, config.searchInfoMode)
+ content = `\n\n${webSearchTable}\n\n${content}`
+ } else {
+ content = `\n\n${content}`
+ }
+ }
+ if (decodeJson.choices[0].delta.phase === 'answer' && !thinking_end && thinking_start) {
+ thinking_end = true
+ content = `\n\n\n${content}`
+ }
+
+ fullContent += content
+ } catch (error) {
+ logger.error('非流式数据处理错误', 'CHAT', '', error)
+ }
+ }
+ })
+
+ response.on('end', () => {
+ resolve()
+ })
+
+ response.on('error', (error) => {
+ logger.error('非流式响应流读取错误', 'CHAT', '', error)
+ reject(error)
+ })
+ })
+
+ // 处理最终的搜索信息
+ if ((config.outThink === false || !enable_thinking) && web_search_info && config.searchInfoMode === "text") {
+ const webSearchTable = await accountManager.generateMarkdownTable(web_search_info, "text")
+ fullContent += `\n\n---\n${webSearchTable}`
+ }
+
+ // 计算最终的token使用量
+ if (totalTokens.prompt_tokens === 0 && totalTokens.completion_tokens === 0) {
+ totalTokens = createUsageObject(requestBody?.messages || promptText, fullContent, null)
+ logger.info(`非流式使用tiktoken计算 - Prompt: ${totalTokens.prompt_tokens}, Completion: ${totalTokens.completion_tokens}, Total: ${totalTokens.total_tokens}`, 'CHAT')
+ } else {
+ logger.info(`非流式使用上游真实Token - Prompt: ${totalTokens.prompt_tokens}, Completion: ${totalTokens.completion_tokens}, Total: ${totalTokens.total_tokens}`, 'CHAT')
+ }
+
+ // 确保token数量的有效性
+ totalTokens.prompt_tokens = Math.max(0, totalTokens.prompt_tokens || 0)
+ totalTokens.completion_tokens = Math.max(0, totalTokens.completion_tokens || 0)
+ totalTokens.total_tokens = totalTokens.prompt_tokens + totalTokens.completion_tokens
+
+ // 返回完整的JSON响应
+ const bodyTemplate = {
+ "id": `chatcmpl-${generateUUID()}`,
+ "object": "chat.completion",
+ "created": new Date().getTime(),
+ "model": model,
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": fullContent
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": totalTokens
+ }
+ res.json(bodyTemplate)
+ } catch (error) {
+ logger.error('非流式聊天处理错误', 'CHAT', '', error)
+ res.status(500)
+ .json({
+ error: "服务错误!!!"
+ })
+ }
+}
+
+
+/**
+ * 主要的聊天完成处理函数
+ * @param {object} req - Express 请求对象
+ * @param {object} res - Express 响应对象
+ */
+const handleChatCompletion = async (req, res) => {
+ const { stream, model } = req.body
+
+ const enable_thinking = req.enable_thinking
+ const enable_web_search = req.enable_web_search
+
+ try {
+ const response_data = await sendChatRequest(req.body)
+
+ if (!response_data.status || !response_data.response) {
+ res.status(500)
+ .json({
+ error: "请求发送失败!!!"
+ })
+ return
+ }
+
+ if (stream) {
+ setResponseHeaders(res, true)
+ await handleStreamResponse(res, response_data.response, enable_thinking, enable_web_search, req.body)
+ } else {
+ setResponseHeaders(res, false)
+ await handleNonStreamResponse(res, response_data.response, enable_thinking, enable_web_search, model, req.body)
+ }
+
+ } catch (error) {
+ logger.error('聊天处理错误', 'CHAT', '', error)
+ res.status(500)
+ .json({
+ error: "token无效,请求发送失败!!!"
+ })
+ }
+}
+
+module.exports = {
+ handleChatCompletion,
+ handleStreamResponse,
+ handleNonStreamResponse,
+ setResponseHeaders
+}
diff --git a/src/controllers/cli.chat.js b/src/controllers/cli.chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..02b5260466c7ab4c1ea8038a308a50b2b9fd4207
--- /dev/null
+++ b/src/controllers/cli.chat.js
@@ -0,0 +1,213 @@
+const axios = require('axios')
+const { logger } = require('../utils/logger')
+const { getProxyAgent, getCliBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
+
+const MODEL_REDIRECT = {
+ 'qwen3.5-plus': 'coder-model',
+}
+
+function preprocessCliRequestBody(rawBody) {
+ const body = rawBody && typeof rawBody === 'object' ? JSON.parse(JSON.stringify(rawBody)) : {}
+
+ if (body.model && MODEL_REDIRECT[body.model]) {
+ body.model = MODEL_REDIRECT[body.model]
+ }
+ const isStream = body.stream === true
+
+ if (isStream) {
+ const hasToolsArray = Array.isArray(body.tools)
+ if (!hasToolsArray || body.tools.length === 0) {
+ body.tools = [{
+ type: 'function',
+ function: {
+ name: 'do_not_call_me',
+ description: 'Do not call this tool.',
+ parameters: {
+ type: 'object',
+ properties: {
+ operation: { type: 'number', description: 'placeholder' }
+ },
+ required: ['operation']
+ }
+ }
+ }]
+ }
+
+ if (!body.stream_options || typeof body.stream_options !== 'object') {
+ body.stream_options = {}
+ }
+ body.stream_options.include_usage = true
+ }
+
+ return body
+}
+
+function formatCliJsonResponse(data, fallbackModel) {
+ if (!data || typeof data !== 'object') {
+ return data
+ }
+ if (!data.object) {
+ data.object = 'chat.completion'
+ }
+ if (!data.model && fallbackModel) {
+ data.model = fallbackModel
+ }
+ if (!Array.isArray(data.choices)) {
+ data.choices = []
+ }
+ return data
+}
+
+/**
+ * 处理CLI聊天完成请求(支持OpenAI格式的流式和JSON响应)
+ * @param {Object} req - Express请求对象
+ * @param {Object} res - Express响应对象
+ */
+const handleCliChatCompletion = async (req, res) => {
+ try {
+ const access_token = req.account.cli_info.access_token
+ const body = preprocessCliRequestBody(req.body)
+ const isStream = body.stream === true
+
+ // 打印当前使用的账号邮箱
+ logger.info(`CLI请求使用账号[${req.account.email}]开始处理`, 'CLI', '🚀')
+
+ // 无论成功失败都增加请求计数
+ req.account.cli_info.request_number++
+
+ const cliBaseUrl = getCliBaseUrl()
+ const proxyAgent = getProxyAgent()
+
+ // 设置请求配置
+ const axiosConfig = {
+ method: 'POST',
+ url: `${cliBaseUrl}/v1/chat/completions`,
+ headers: {
+ 'Authorization': `Bearer ${access_token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': isStream ? 'text/event-stream' : 'application/json',
+ 'User-Agent': 'QwenCode/0.10.3 (darwin; arm64)',
+ 'X-Dashscope-Useragent': 'QwenCode/0.10.3 (darwin; arm64)',
+ 'X-Stainless-Runtime-Version': 'v22.17.0',
+ 'Sec-Fetch-Mode': 'cors',
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Arch': 'arm64',
+ 'X-Stainless-Package-Version': '5.11.0',
+ 'X-Dashscope-Cachecontrol': 'enable',
+ 'X-Stainless-Retry-Count': '0',
+ 'X-Stainless-Os': 'MacOS',
+ 'X-Dashscope-Authtype': 'qwen-oauth',
+ 'X-Stainless-Runtime': 'node'
+ },
+ data: body,
+ timeout: 5 * 60 * 1000,
+ validateStatus: function () {
+ return true
+ }
+ }
+
+ // 添加代理配置
+ if (proxyAgent) {
+ axiosConfig.httpsAgent = proxyAgent
+ axiosConfig.proxy = false
+ }
+
+ // 如果是流式请求,设置响应类型为流
+ if (isStream) {
+ axiosConfig.responseType = 'stream'
+
+ // 设置响应头为流式
+ res.setHeader('Content-Type', 'text/event-stream')
+ res.setHeader('Cache-Control', 'no-cache')
+ res.setHeader('Connection', 'keep-alive')
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Access-Control-Allow-Headers', '*')
+ }
+
+ const response = await axios(axiosConfig)
+
+ // 检查响应状态
+ if (response.status !== 200) {
+ logger.error(`CLI请求使用账号[${req.account.email}]转发失败 - 状态码: ${response.status} - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '❌')
+ return res.status(response.status).json({
+ error: {
+ message: `api_error`,
+ type: 'api_error',
+ code: response.status,
+ details: response.data
+ }
+ })
+ }
+
+ // 处理流式响应
+ if (isStream) {
+ // 逐行转发,确保始终输出标准 SSE 片段
+ response.data.on('data', (chunk) => {
+ const text = chunk.toString('utf8')
+ const lines = text.split('\n')
+ for (const line of lines) {
+ if (!line || !line.startsWith('data:')) continue
+ res.write(`${line}\n\n`)
+ }
+ })
+
+ // 处理流错误
+ response.data.on('error', (streamError) => {
+ logger.error(`CLI请求使用账号[${req.account.email}]流式传输失败 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '❌')
+ if (!res.headersSent) {
+ res.status(500).json({
+ error: {
+ message: 'stream_error',
+ type: 'stream_error',
+ code: 500
+ }
+ })
+ }
+ })
+
+ // 处理流结束
+ response.data.on('end', () => {
+ logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (流式) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
+ res.end()
+ })
+ } else {
+ // 处理JSON响应
+ res.json(formatCliJsonResponse(response.data, body.model))
+ logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (JSON) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
+ }
+ } catch (error) {
+ logger.error(`CLI请求使用账号[${req.account.email}]处理异常 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '💥', error.message)
+
+ // 如果是axios错误,提供更详细的错误信息
+ if (error.response) {
+ return res.status(error.response.status).json({
+ error: {
+ message: "api_error",
+ type: 'api_error',
+ code: error.response.status,
+ details: error.response.data
+ }
+ })
+ } else if (error.request) {
+ return res.status(503).json({
+ error: {
+ message: 'connection_error',
+ type: 'connection_error',
+ code: 503
+ }
+ })
+ } else {
+ return res.status(500).json({
+ error: {
+ message: 'internal_error',
+ type: 'internal_error',
+ code: 500
+ }
+ })
+ }
+ }
+}
+
+module.exports = {
+ handleCliChatCompletion
+}
diff --git a/src/controllers/models.js b/src/controllers/models.js
new file mode 100644
index 0000000000000000000000000000000000000000..038a8016a6795935f0ae0ea2a837385633ac166d
--- /dev/null
+++ b/src/controllers/models.js
@@ -0,0 +1,75 @@
+const { getLatestModels } = require('../models/models-map.js')
+const config = require('../config/index.js')
+
+const handleGetModels = async (req, res) => {
+ const models = []
+
+ const ModelsMap = await getLatestModels()
+
+ for (const model of ModelsMap) {
+ delete model.name
+ models.push(model)
+
+ if (config.simpleModelMap) {
+ continue
+ }
+
+ const isThinking = model?.info?.meta?.abilities?.thinking
+ const isSearch = model?.info?.meta?.chat_type?.includes('search')
+ const isImage = model?.info?.meta?.chat_type?.includes('t2i')
+ const isVideo = model?.info?.meta?.chat_type?.includes('t2v')
+ const isImageEdit = model?.info?.meta?.chat_type?.includes('image_edit')
+ const isDeepResearch = model?.info?.meta?.chat_type?.includes('deep_research')
+
+ if (isThinking) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-thinking`
+
+ models.push(newModelData)
+ }
+
+ if (isSearch) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-search`
+ models.push(newModelData)
+ }
+
+ if (isThinking && isSearch) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-thinking-search`
+ models.push(newModelData)
+ }
+
+ if (isImage) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-image`
+ models.push(newModelData)
+ }
+
+ if (isVideo) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-video`
+ models.push(newModelData)
+ }
+
+ if (isImageEdit) {
+ const newModelData = JSON.parse(JSON.stringify(model))
+ newModelData.id = `${model.id}-image-edit`
+ models.push(newModelData)
+ }
+
+ // if (isDeepResearch) {
+ // const newModelData = JSON.parse(JSON.stringify(model))
+ // newModelData.id = `${model.id}-deep-research`
+ // models.push(newModelData)
+ // }
+ }
+ res.json({
+ "object": "list",
+ "data": models
+ })
+}
+
+module.exports = {
+ handleGetModels
+}
\ No newline at end of file
diff --git a/src/middlewares/authorization.js b/src/middlewares/authorization.js
new file mode 100644
index 0000000000000000000000000000000000000000..359dd8b79dc6d4f5862a5dd0cf68bfbab9daad44
--- /dev/null
+++ b/src/middlewares/authorization.js
@@ -0,0 +1,61 @@
+const config = require('../config')
+
+/**
+ * 验证API Key是否有效
+ * @param {string} providedKey - 提供的API Key
+ * @returns {Object} 验证结果 { isValid: boolean, isAdmin: boolean }
+ */
+const validateApiKey = (providedKey) => {
+ if (!providedKey) {
+ return { isValid: false, isAdmin: false }
+ }
+
+ // 移除Bearer前缀
+ const cleanKey = providedKey.startsWith('Bearer ') ? providedKey.slice(7) : providedKey
+
+ // 检查是否在有效的API keys列表中
+ const isValid = config.apiKeys.includes(cleanKey)
+ const isAdmin = cleanKey === config.adminKey
+
+ return { isValid, isAdmin }
+}
+
+/**
+ * API Key验证中间件 - 验证任何有效的API Key
+ */
+const apiKeyVerify = (req, res, next) => {
+ const apiKey = req.headers['authorization'] || req.headers['Authorization'] || req.headers['x-api-key']
+ const { isValid, isAdmin } = validateApiKey(apiKey)
+
+ if (!isValid) {
+ return res.status(401).json({ error: 'Unauthorized' })
+ }
+
+ // 将权限信息附加到请求对象
+ req.isAdmin = isAdmin
+ req.apiKey = apiKey
+ next()
+}
+
+/**
+ * 管理员权限验证中间件 - 只允许管理员API Key
+ */
+const adminKeyVerify = (req, res, next) => {
+ const apiKey = req.headers['authorization'] || req.headers['Authorization'] || req.headers['x-api-key']
+ const { isValid, isAdmin } = validateApiKey(apiKey)
+
+ if (!isValid || !isAdmin) {
+ return res.status(403).json({ error: 'Admin access required' })
+ }
+
+ req.isAdmin = isAdmin
+ req.apiKey = apiKey
+ next()
+}
+
+module.exports = {
+ apiKeyVerify,
+ adminKeyVerify,
+ validateApiKey
+}
+
diff --git a/src/middlewares/chat-middleware.js b/src/middlewares/chat-middleware.js
new file mode 100644
index 0000000000000000000000000000000000000000..92dfe4882a2df567a26a2ec3a3cf020be69a8f3a
--- /dev/null
+++ b/src/middlewares/chat-middleware.js
@@ -0,0 +1,79 @@
+const { generateUUID } = require('../utils/tools.js')
+const { isChatType, isThinkingEnabled, parserModel, parserMessages } = require('../utils/chat-helpers.js')
+const { logger } = require('../utils/logger')
+
+/**
+ * 处理聊天请求体的中间件
+ * 解析和转换请求参数为内部格式
+ */
+const processRequestBody = async (req, res, next) => {
+ try {
+ // 构建请求体
+ const body = {
+ "stream": true,
+ "incremental_output": true,
+ "chat_type": "t2t",
+ "model": "qwen3-235b-a22b",
+ "messages": [],
+ "session_id": generateUUID(),
+ "id": generateUUID(),
+ "sub_chat_type": "t2t",
+ "chat_mode": "normal"
+ }
+
+ // 获取请求体原始数据
+ let {
+ messages, // 消息历史
+ model, // 模型
+ stream, // 流式输出
+ enable_thinking, // 是否启用思考
+ thinking_budget, // 思考预算
+ size //图片尺寸
+ } = req.body
+
+ // 处理 stream 参数
+ if (stream === true || stream === 'true') {
+ body.stream = true
+ } else {
+ body.stream = false
+ }
+
+ // 处理 chat_type 参数 : 聊天类型
+ body.chat_type = isChatType(model)
+
+ req.enable_web_search = body.chat_type === 'search' ? true : false
+
+ // 处理 model 参数 : 模型
+ body.model = parserModel(model)
+
+ // 处理 messages 参数 : 消息历史
+ body.messages = await parserMessages(messages, isThinkingEnabled(model, enable_thinking, thinking_budget), body.chat_type)
+
+ // 处理 enable_thinking 参数 : 是否启用思考
+ req.enable_thinking = isThinkingEnabled(model, enable_thinking, thinking_budget).thinking_enabled
+
+ // 处理 sub_chat_type 参数 : 子聊天类型
+ body.sub_chat_type = body.chat_type
+
+ // 处理图片尺寸
+ if (size) {
+ body.size = size
+ }
+
+ // 处理请求体,将body赋值给req.body
+ req.body = body
+
+ next()
+ } catch (e) {
+ logger.error('处理请求体时发生错误', 'MIDDLEWARE', '', e)
+ res.status(500)
+ .json({
+ status: 500,
+ message: "在处理请求体时发生错误 ~ ~ ~"
+ })
+ }
+}
+
+module.exports = {
+ processRequestBody
+}
diff --git a/src/models/models-map.js b/src/models/models-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e9b2d3c780f5b67b1c15f65574341cc091d4b99
--- /dev/null
+++ b/src/models/models-map.js
@@ -0,0 +1,52 @@
+const axios = require('axios')
+const accountManager = require('../utils/account.js')
+const { getSsxmodItna, getSsxmodItna2 } = require('../utils/ssxmod-manager')
+const { getProxyAgent, getChatBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
+
+let cachedModels = null
+let fetchPromise = null
+
+const getLatestModels = async (force = false) => {
+ // 如果有缓存且不强制刷新,直接返回
+ if (cachedModels && !force) {
+ return cachedModels
+ }
+
+ // 如果正在获取,返回当前的 Promise
+ if (fetchPromise) {
+ return fetchPromise
+ }
+
+ const chatBaseUrl = getChatBaseUrl()
+ const proxyAgent = getProxyAgent()
+
+ const requestConfig = {
+ headers: {
+ 'Authorization': `Bearer ${accountManager.getAccountToken()}`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ ...(getSsxmodItna() && { 'Cookie': `ssxmod_itna=${getSsxmodItna()};ssxmod_itna2=${getSsxmodItna2()}` })
+ }
+ }
+
+ // 添加代理配置
+ if (proxyAgent) {
+ requestConfig.httpsAgent = proxyAgent
+ requestConfig.proxy = false
+ }
+
+ fetchPromise = axios.get(`${chatBaseUrl}/api/models`, requestConfig).then(response => {
+ // console.log(response)
+ cachedModels = response.data.data
+ fetchPromise = null
+ return cachedModels
+ }).catch(error => {
+ console.error('Error fetching latest models:', error)
+ fetchPromise = null
+ return []
+ })
+
+ return fetchPromise
+}
+
+module.exports = { getLatestModels }
\ No newline at end of file
diff --git a/src/routes/accounts.js b/src/routes/accounts.js
new file mode 100644
index 0000000000000000000000000000000000000000..b54fc86042abc77ae0299d4256620ba509914292
--- /dev/null
+++ b/src/routes/accounts.js
@@ -0,0 +1,259 @@
+const express = require('express')
+const router = express.Router()
+const accountManager = require('../utils/account')
+const { logger } = require('../utils/logger')
+const { JwtDecode } = require('../utils/tools')
+const { adminKeyVerify } = require('../middlewares/authorization')
+const { deleteAccount, saveAccounts, refreshAccountToken } = require('../utils/setting')
+
+/**
+ * 获取所有账号(分页)
+ *
+ * @param {number} page 页码
+ * @param {number} pageSize 每页数量
+ * @returns {Object} 账号列表
+ */
+router.get('/getAllAccounts', adminKeyVerify, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1
+ const pageSize = parseInt(req.query.pageSize) || 1000
+ const start = (page - 1) * pageSize
+
+ // 获取所有账号键
+ const allAccounts = accountManager.getAllAccountKeys()
+ const total = allAccounts.length
+
+ // 分页处理
+ const paginatedAccounts = allAccounts.slice(start, start + pageSize)
+
+ // 获取每个账号的详细信息
+ const accounts = paginatedAccounts.map(account => {
+ return {
+ email: account.email,
+ password: account.password,
+ token: account.token,
+ expires: account.expires
+ }
+ })
+
+ res.json({
+ total,
+ page,
+ pageSize,
+ data: accounts
+ })
+ } catch (error) {
+ logger.error('获取账号列表失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+/**
+ * POST /setAccount
+ * 添加账号
+ *
+ * @param {string} email 邮箱
+ * @param {string} password 密码
+ * @returns {Object} 账号信息
+ */
+router.post('/setAccount', adminKeyVerify, async (req, res) => {
+ try {
+ const { email, password } = req.body
+
+ if (!email || !password) {
+ return res.status(400).json({ error: '邮箱和密码不能为空' })
+ }
+
+ // 检查账号是否已存在
+ const exists = accountManager.accountTokens.find(item => item.email === email)
+ if (exists) {
+ return res.status(409).json({ error: '账号已存在' })
+ }
+
+ const authToken = await accountManager.login(email, password)
+ if (!authToken) {
+ return res.status(401).json({ error: '登录失败' })
+ }
+ // 解析JWT
+ const decoded = JwtDecode(authToken)
+ const expires = decoded.exp
+
+ const success = await saveAccounts(email, password, authToken, expires)
+
+ if (success) {
+ res.status(200).json({
+ email,
+ message: '账号创建成功'
+ })
+ } else {
+ res.status(500).json({ error: '账号创建失败' })
+ }
+ } catch (error) {
+ logger.error('创建账号失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+/**
+ * DELETE /deleteAccount
+ * 删除账号
+ *
+ * @param {string} email 邮箱
+ * @returns {Object} 账号信息
+ */
+router.delete('/deleteAccount', adminKeyVerify, async (req, res) => {
+ try {
+ const { email } = req.body
+
+ // 检查账号是否存在
+ const exists = await accountManager.accountTokens.find(item => item.email === email)
+ if (!exists) {
+ return res.status(404).json({ error: '账号不存在' })
+ }
+
+ // 删除账号
+ const success = await deleteAccount(email)
+
+ if (success) {
+ res.json({ message: '账号删除成功' })
+ } else {
+ res.status(500).json({ error: '账号删除失败' })
+ }
+ } catch (error) {
+ logger.error('删除账号失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+
+/**
+ * POST /setAccounts
+ * 批量添加账号
+ *
+ * @param {string} accounts 账号列表
+ * @returns {Object} 账号信息
+ */
+router.post('/setAccounts', adminKeyVerify, async (req, res) => {
+ try {
+ let { accounts } = req.body
+ if (!accounts) {
+ return res.status(400).json({ error: '账号列表不能为空' })
+ }
+
+ accounts = accounts.replace(/[\r]/g, '\n')
+ accounts = accounts.split('\n').filter(item => item.trim() !== '')
+
+ for (const account of accounts) {
+ const [email, password] = account.split(':')
+ if (!email || !password) {
+ continue
+ }
+
+ const authToken = await accountManager.login(email, password)
+ if (!authToken) {
+ continue
+ }
+ // 解析JWT
+ const decoded = JwtDecode(authToken)
+ const expires = decoded.exp
+
+ const success = await saveAccounts(email, password, authToken, expires)
+ if (!success) {
+ continue
+ }
+ }
+
+ res.json({ message: '账号批量添加任务提交成功' })
+ } catch (error) {
+ logger.error('批量创建账号失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+/**
+ * POST /refreshAccount
+ * 刷新单个账号的令牌
+ *
+ * @param {string} email 邮箱
+ * @returns {Object} 刷新结果
+ */
+router.post('/refreshAccount', adminKeyVerify, async (req, res) => {
+ try {
+ const { email } = req.body
+
+ if (!email) {
+ return res.status(400).json({ error: '邮箱不能为空' })
+ }
+
+ // 检查账号是否存在
+ const exists = accountManager.accountTokens.find(item => item.email === email)
+ if (!exists) {
+ return res.status(404).json({ error: '账号不存在' })
+ }
+
+ // 刷新账号令牌
+ const success = await accountManager.refreshAccountToken(email)
+
+ if (success) {
+ res.json({
+ message: '账号令牌刷新成功',
+ email: email
+ })
+ } else {
+ res.status(500).json({ error: '账号令牌刷新失败' })
+ }
+ } catch (error) {
+ logger.error('刷新账号令牌失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+/**
+ * POST /refreshAllAccounts
+ * 刷新所有账号的令牌
+ *
+ * @param {number} thresholdHours 过期阈值(小时),默认24小时
+ * @returns {Object} 刷新结果
+ */
+router.post('/refreshAllAccounts', adminKeyVerify, async (req, res) => {
+ try {
+ const { thresholdHours = 24 } = req.body
+
+ // 执行批量刷新
+ const refreshedCount = await accountManager.autoRefreshTokens(thresholdHours)
+
+ res.json({
+ message: '批量刷新完成',
+ refreshedCount: refreshedCount,
+ thresholdHours: thresholdHours
+ })
+ } catch (error) {
+ logger.error('批量刷新账号令牌失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+/**
+ * POST /forceRefreshAllAccounts
+ * 强制刷新所有账号的令牌(不管是否即将过期)
+ *
+ * @returns {Object} 刷新结果
+ */
+router.post('/forceRefreshAllAccounts', adminKeyVerify, async (req, res) => {
+ try {
+ // 强制刷新所有账号(设置阈值为很大的值,确保所有账号都会被刷新)
+ const refreshedCount = await accountManager.autoRefreshTokens(8760) // 365天
+
+ res.json({
+ message: '强制刷新完成',
+ refreshedCount: refreshedCount,
+ totalAccounts: accountManager.getAllAccountKeys().length
+ })
+ } catch (error) {
+ logger.error('强制刷新账号令牌失败', 'ACCOUNT', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+
+module.exports = router
\ No newline at end of file
diff --git a/src/routes/chat.js b/src/routes/chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..16c78f07510b6d22ddbc1afdd67e7aee2a79de45
--- /dev/null
+++ b/src/routes/chat.js
@@ -0,0 +1,34 @@
+const express = require('express')
+const router = express.Router()
+const { apiKeyVerify } = require('../middlewares/authorization.js')
+const { processRequestBody } = require('../middlewares/chat-middleware.js')
+const { handleChatCompletion } = require('../controllers/chat.js')
+const { handleImageVideoCompletion } = require('../controllers/chat.image.video.js')
+
+const selectChatCompletion = (req, res, next) => {
+ const ChatCompletionMap = {
+ 't2t': handleChatCompletion,
+ 'search': handleChatCompletion,
+ 't2i': handleImageVideoCompletion,
+ 't2v': handleImageVideoCompletion,
+ 'image_edit': handleImageVideoCompletion,
+ // 'deep_research': handleDeepResearchCompletion
+ }
+
+ const chatType = req.body.chat_type
+ const chatCompletion = ChatCompletionMap[chatType]
+ if (chatCompletion) {
+ chatCompletion(req, res, next)
+ } else {
+ handleImageCompletion(req, res, next)
+ }
+}
+
+router.post('/v1/chat/completions',
+ apiKeyVerify,
+ processRequestBody,
+ selectChatCompletion
+)
+
+
+module.exports = router
\ No newline at end of file
diff --git a/src/routes/cli.chat.js b/src/routes/cli.chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..c46457db5b973f7d229d51c3dd86704af3d9329e
--- /dev/null
+++ b/src/routes/cli.chat.js
@@ -0,0 +1,39 @@
+const express = require('express')
+const router = express.Router()
+const { apiKeyVerify } = require('../middlewares/authorization.js')
+const { handleCliChatCompletion } = require('../controllers/cli.chat.js')
+const accountManager = require('../utils/account.js')
+
+router.post('/cli/v1/chat/completions',
+ apiKeyVerify,
+ async (req, res, next) => {
+ // 异步初始化新账号(不阻塞当前请求)
+ const noCliAccount = accountManager.accountTokens.filter(account => !account.cli_info)
+ if (noCliAccount.length > 0) {
+ const randomNewAccount = noCliAccount[Math.floor(Math.random() * noCliAccount.length)]
+ // 异步初始化,不等待结果
+ accountManager.initializeCliForAccount(randomNewAccount).catch(error => {
+ console.error(`异步初始化CLI账户失败 (${randomNewAccount.email}):`, error)
+ })
+ }
+
+ // 获取当前可用的CLI账户用于本次请求
+ const availableAccounts = accountManager.accountTokens.filter(account =>
+ account.cli_info && account.cli_info.request_number < 2000
+ )
+
+ if (availableAccounts.length === 0) {
+ return res.status(503).json({
+ error: '没有可用的CLI账户,请稍后重试'
+ })
+ }
+
+ // 随机选择一个可用账户用于本次请求
+ const randomAccount = availableAccounts[Math.floor(Math.random() * availableAccounts.length)]
+ req.account = randomAccount
+ next()
+ },
+ handleCliChatCompletion
+)
+
+module.exports = router
\ No newline at end of file
diff --git a/src/routes/models.js b/src/routes/models.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef5ad8377f8a367d1801a118795c2ee844f22f4c
--- /dev/null
+++ b/src/routes/models.js
@@ -0,0 +1,31 @@
+const express = require('express')
+const router = express.Router()
+const { apiKeyVerify } = require('../middlewares/authorization')
+const { handleGetModels } = require('../controllers/models.js')
+
+router.get('/v1/models', apiKeyVerify, handleGetModels)
+
+router.get('/models', handleGetModels)
+
+router.post('/cli/v1/models', async (req, res) => {
+ res.json({
+ object: 'list',
+ data: [
+ {
+ id: 'qwen3-coder-plus',
+ object: 'model',
+ created: 1719878112,
+ owned_by: 'qwen-code'
+ },
+ {
+ id: 'qwen3-coder-flash',
+ object: 'model',
+ created: 1719878112,
+ owned_by: 'qwen-code'
+ },
+ ]
+ })
+})
+
+
+module.exports = router
diff --git a/src/routes/settings.js b/src/routes/settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ab4f7e8f73491728f0ec805aa497bdfaaa79536
--- /dev/null
+++ b/src/routes/settings.js
@@ -0,0 +1,161 @@
+const express = require('express')
+const router = express.Router()
+const config = require('../config')
+const { apiKeyVerify, adminKeyVerify } = require('../middlewares/authorization')
+const { logger } = require('../utils/logger')
+
+
+router.get('/settings', adminKeyVerify, async (req, res) => {
+ // 分离管理员密钥和普通密钥
+ const regularKeys = config.apiKeys.filter(key => key !== config.adminKey)
+
+ res.json({
+ apiKey: config.apiKey, // 保持向后兼容
+ adminKey: config.adminKey,
+ regularKeys: regularKeys,
+ defaultHeaders: config.defaultHeaders,
+ defaultCookie: config.defaultCookie,
+ autoRefresh: config.autoRefresh,
+ autoRefreshInterval: config.autoRefreshInterval,
+ outThink: config.outThink,
+ searchInfoMode: config.searchInfoMode,
+ simpleModelMap: config.simpleModelMap
+ })
+})
+
+// 添加普通API Key
+router.post('/addRegularKey', adminKeyVerify, async (req, res) => {
+ try {
+ const { apiKey } = req.body
+ if (!apiKey) {
+ return res.status(400).json({ error: 'API Key不能为空' })
+ }
+
+ // 检查是否已存在
+ if (config.apiKeys.includes(apiKey)) {
+ return res.status(409).json({ error: 'API Key已存在' })
+ }
+
+ // 添加到配置中
+ config.apiKeys.push(apiKey)
+
+ res.json({ message: 'API Key添加成功' })
+ } catch (error) {
+ logger.error('添加API Key失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// 删除普通API Key
+router.post('/deleteRegularKey', adminKeyVerify, async (req, res) => {
+ try {
+ const { apiKey } = req.body
+ if (!apiKey) {
+ return res.status(400).json({ error: 'API Key不能为空' })
+ }
+
+ // 不能删除管理员密钥
+ if (apiKey === config.adminKey) {
+ return res.status(403).json({ error: '不能删除管理员密钥' })
+ }
+
+ // 从配置中移除
+ const index = config.apiKeys.indexOf(apiKey)
+ if (index === -1) {
+ return res.status(404).json({ error: 'API Key不存在' })
+ }
+
+ config.apiKeys.splice(index, 1)
+
+ res.json({ message: 'API Key删除成功' })
+ } catch (error) {
+ logger.error('删除API Key失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// 更新自动刷新设置
+router.post('/setAutoRefresh', adminKeyVerify, async (req, res) => {
+ try {
+ const { autoRefresh, autoRefreshInterval } = req.body
+
+ if (typeof autoRefresh !== 'boolean') {
+ return res.status(400).json({ error: '无效的自动刷新设置' })
+ }
+
+ if (autoRefreshInterval !== undefined) {
+ const interval = parseInt(autoRefreshInterval)
+ if (isNaN(interval) || interval < 0) {
+ return res.status(400).json({ error: '无效的自动刷新间隔' })
+ }
+ }
+ config.autoRefresh = autoRefresh
+ config.autoRefreshInterval = autoRefreshInterval || 6 * 60 * 60
+ res.json({
+ status: true,
+ message: '自动刷新设置更新成功'
+ })
+ } catch (error) {
+ logger.error('更新自动刷新设置失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// 更新思考输出设置
+router.post('/setOutThink', adminKeyVerify, async (req, res) => {
+ try {
+ const { outThink } = req.body;
+ if (typeof outThink !== 'boolean') {
+ return res.status(400).json({ error: '无效的思考输出设置' })
+ }
+
+ config.outThink = outThink
+ res.json({
+ status: true,
+ message: '思考输出设置更新成功'
+ })
+ } catch (error) {
+ logger.error('更新思考输出设置失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// 更新搜索信息模式
+router.post('/search-info-mode', adminKeyVerify, async (req, res) => {
+ try {
+ const { searchInfoMode } = req.body
+ if (!['table', 'text'].includes(searchInfoMode)) {
+ return res.status(400).json({ error: '无效的搜索信息模式' })
+ }
+
+ config.searchInfoMode = searchInfoMode
+ res.json({
+ status: true,
+ message: '搜索信息模式更新成功'
+ })
+ } catch (error) {
+ logger.error('更新搜索信息模式失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// 更新简化模型映射设置
+router.post('/simple-model-map', adminKeyVerify, async (req, res) => {
+ try {
+ const { simpleModelMap } = req.body
+ if (typeof simpleModelMap !== 'boolean') {
+ return res.status(400).json({ error: '无效的简化模型映射设置' })
+ }
+
+ config.simpleModelMap = simpleModelMap
+ res.json({
+ status: true,
+ message: '简化模型映射设置更新成功'
+ })
+ } catch (error) {
+ logger.error('更新简化模型映射设置失败', 'CONFIG', '', error)
+ res.status(500).json({ error: error.message })
+ }
+})
+
+module.exports = router
\ No newline at end of file
diff --git a/src/routes/verify.js b/src/routes/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf55a868b63002ac2f8347a35c080190c13d5a82
--- /dev/null
+++ b/src/routes/verify.js
@@ -0,0 +1,24 @@
+const express = require('express')
+const router = express.Router()
+const config = require('../config/index.js')
+const { validateApiKey } = require('../middlewares/authorization')
+
+router.post('/verify', (req, res) => {
+ const apiKey = req.body.apiKey
+ const { isValid, isAdmin } = validateApiKey(apiKey)
+
+ if (!isValid) {
+ return res.status(401).json({
+ status: 401,
+ message: 'Unauthorized'
+ })
+ }
+
+ res.status(200).json({
+ status: 200,
+ message: 'success',
+ isAdmin: isAdmin
+ })
+})
+
+module.exports = router
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..1578e3ad27721c607cb14a1f62bc970194ca763c
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,81 @@
+const express = require('express')
+const bodyParser = require('body-parser')
+const config = require('./config/index.js')
+const cors = require('cors')
+const { logger } = require('./utils/logger')
+const { initSsxmodManager } = require('./utils/ssxmod-manager')
+const app = express()
+const path = require('path')
+const fs = require('fs')
+const paths = require('./utils/paths')
+const modelsRouter = require('./routes/models.js')
+const chatRouter = require('./routes/chat.js')
+const cliChatRouter = require('./routes/cli.chat.js')
+const verifyRouter = require('./routes/verify.js')
+const accountsRouter = require('./routes/accounts.js')
+const settingsRouter = require('./routes/settings.js')
+
+if (config.dataSaveMode === 'file') {
+ if (!fs.existsSync(paths.dataFilePath)) {
+ fs.mkdirSync(paths.dataDir, { recursive: true })
+ fs.writeFileSync(paths.dataFilePath, JSON.stringify({"accounts": [] }, null, 2))
+ }
+}
+
+// 初始化 SSXMOD Cookie 管理器
+initSsxmodManager()
+
+app.use(bodyParser.json({ limit: '128mb' }))
+app.use(bodyParser.urlencoded({ limit: '128mb', extended: true }))
+app.use(cors())
+
+// API路由
+app.use(modelsRouter)
+app.use(chatRouter)
+app.use(cliChatRouter)
+app.use(verifyRouter)
+app.use('/api', accountsRouter)
+app.use('/api', settingsRouter)
+
+app.use(express.static(path.join(__dirname, '../public/dist')))
+
+app.get('*', (req, res) => {
+ res.sendFile(path.join(__dirname, '../public/dist/index.html'), (err) => {
+ if (err) {
+ logger.error('管理页面加载失败', 'SERVER', '', err)
+ res.status(500).send('服务器内部错误')
+ }
+ })
+})
+
+// 处理错误中间件(必须放在所有路由之后)
+app.use((err, req, res, next) => {
+ logger.error('服务器内部错误', 'SERVER', '', err)
+ res.status(500).send('服务器内部错误')
+})
+
+
+// 服务器启动信息
+const serverInfo = {
+ address: config.listenAddress || 'localhost',
+ port: config.listenPort,
+ outThink: config.outThink ? '开启' : '关闭',
+ searchInfoMode: config.searchInfoMode === 'table' ? '表格' : '文本',
+ dataSaveMode: config.dataSaveMode,
+ logLevel: config.logLevel,
+ enableFileLog: config.enableFileLog
+}
+
+if (config.listenAddress) {
+ app.listen(config.listenPort, config.listenAddress, () => {
+ logger.server('服务器启动成功', 'SERVER', serverInfo)
+ logger.info('开源地址: https://github.com/Rfym21/Qwen2API', 'INFO')
+ logger.info('电报群聊: https://t.me/nodejs_project', 'INFO')
+ })
+} else {
+ app.listen(config.listenPort, () => {
+ logger.server('服务器启动成功', 'SERVER', serverInfo)
+ logger.info('开源地址: https://github.com/Rfym21/Qwen2API', 'INFO')
+ logger.info('电报群聊: https://t.me/nodejs_project', 'INFO')
+ })
+}
diff --git a/src/start.js b/src/start.js
new file mode 100644
index 0000000000000000000000000000000000000000..518af3add2475894855767d226dddff6dd32d9d3
--- /dev/null
+++ b/src/start.js
@@ -0,0 +1,113 @@
+const cluster = require('cluster')
+const os = require('os')
+const { logger } = require('./utils/logger')
+
+// 加载环境变量
+require('dotenv').config()
+
+// 获取CPU核心数
+const cpuCores = os.cpus().length
+
+// 获取环境变量配置
+const PM2_INSTANCES = process.env.PM2_INSTANCES || '1'
+const SERVICE_PORT = process.env.SERVICE_PORT || 3000
+const NODE_ENV = process.env.NODE_ENV || 'production'
+
+// 解析进程数
+let instances
+if (PM2_INSTANCES === 'max') {
+ instances = cpuCores
+} else if (!isNaN(PM2_INSTANCES)) {
+ instances = parseInt(PM2_INSTANCES)
+} else {
+ instances = 1
+}
+
+// 限制进程数不能超过CPU核心数
+if (instances > cpuCores) {
+ logger.warn(`配置的进程数(${instances})超过CPU核心数(${cpuCores}),自动调整为${cpuCores}`, 'AUTO')
+ instances = cpuCores
+}
+
+logger.info('🚀 Qwen2API 智能启动', 'AUTO')
+logger.info(`CPU核心数: ${cpuCores}`, 'AUTO')
+logger.info(`配置的进程数: ${PM2_INSTANCES}`, 'AUTO')
+logger.info(`实际启动进程数: ${instances}`, 'AUTO')
+logger.info(`服务端口: ${SERVICE_PORT}`, 'AUTO')
+
+// 智能判断启动方式
+if (instances === 1) {
+ logger.info('📦 使用单进程模式启动', 'AUTO')
+ // 直接启动服务器
+ require('./server.js')
+} else {
+ // 检查是否通过PM2启动
+ if (process.env.PM2_USAGE || process.env.pm_id !== undefined) {
+ logger.info(`PM2进程启动 - 进程ID: ${process.pid}, 工作进程ID: ${process.env.pm_id || 'unknown'}`, 'PM2')
+ require('./server.js')
+ } else if (cluster.isMaster) {
+ logger.info(`🔥 使用Node.js集群模式启动 (${instances}个进程)`, 'AUTO')
+
+ logger.info(`启动主进程 - PID: ${process.pid}`, 'CLUSTER')
+ logger.info(`运行环境: ${NODE_ENV}`, 'CLUSTER')
+
+ // 创建工作进程
+ for (let i = 0; i < instances; i++) {
+ const worker = cluster.fork()
+ logger.info(`启动工作进程 ${i + 1}/${instances} - PID: ${worker.process.pid}`, 'CLUSTER')
+ }
+
+ // 监听工作进程退出
+ cluster.on('exit', (worker, code, signal) => {
+ logger.error(`工作进程 ${worker.process.pid} 已退出 - 退出码: ${code}, 信号: ${signal}`, 'CLUSTER')
+
+ // 自动重启工作进程
+ if (!worker.exitedAfterDisconnect) {
+ logger.info('正在重启工作进程...', 'CLUSTER')
+ const newWorker = cluster.fork()
+ logger.info(`新工作进程已启动 - PID: ${newWorker.process.pid}`, 'CLUSTER')
+ }
+ })
+
+ // 监听工作进程在线
+ cluster.on('online', (worker) => {
+ logger.info(`工作进程 ${worker.process.pid} 已上线`, 'CLUSTER')
+ })
+
+ // 监听工作进程断开连接
+ cluster.on('disconnect', (worker) => {
+ logger.warn(`工作进程 ${worker.process.pid} 已断开连接`, 'CLUSTER')
+ })
+
+ // 优雅关闭处理
+ process.on('SIGTERM', () => {
+ logger.info('收到SIGTERM信号,正在优雅关闭...', 'CLUSTER')
+ cluster.disconnect(() => {
+ process.exit(0)
+ })
+ })
+
+ process.on('SIGINT', () => {
+ logger.info('收到SIGINT信号,正在优雅关闭...', 'CLUSTER')
+ cluster.disconnect(() => {
+ process.exit(0)
+ })
+ })
+
+ } else {
+ // 工作进程逻辑
+ logger.info(`工作进程启动 - PID: ${process.pid}`, 'WORKER')
+ require('./server.js')
+
+ // 工作进程优雅关闭处理
+ process.on('SIGTERM', () => {
+ logger.info(`工作进程 ${process.pid} 收到SIGTERM信号,正在关闭...`, 'WORKER')
+ process.exit(0)
+ })
+
+ process.on('SIGINT', () => {
+ logger.info(`工作进程 ${process.pid} 收到SIGINT信号,正在关闭...`, 'WORKER')
+ process.exit(0)
+ })
+ }
+}
diff --git a/src/utils/account-rotator.js b/src/utils/account-rotator.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f2d7fc88f8caccff1feed0629d5796bc0ffaedf
--- /dev/null
+++ b/src/utils/account-rotator.js
@@ -0,0 +1,247 @@
+const { logger } = require('./logger')
+
+/**
+ * 账户轮询管理器
+ * 负责账户的轮询选择和负载均衡
+ */
+class AccountRotator {
+ constructor() {
+ this.accounts = []
+ this.currentIndex = 0
+ this.lastUsedTimes = new Map() // 记录每个账户的最后使用时间
+ this.failureCounts = new Map() // 记录每个账户的失败次数
+ this.maxFailures = 3 // 最大失败次数
+ this.cooldownPeriod = 5 * 60 * 1000 // 5分钟冷却期
+ }
+
+ /**
+ * 设置账户列表
+ * @param {Array} accounts - 账户列表
+ */
+ setAccounts(accounts) {
+ if (!Array.isArray(accounts)) {
+ logger.error('账户列表必须是数组', 'ACCOUNT')
+ throw new Error('账户列表必须是数组')
+ }
+
+ this.accounts = [...accounts]
+ this.currentIndex = 0
+
+ // 清理不存在账户的记录
+ this._cleanupRecords()
+ }
+
+ /**
+ * 获取下一个可用的账户令牌
+ * @returns {string|null} 账户令牌或null
+ */
+ getNextToken() {
+ if (this.accounts.length === 0) {
+ logger.error('没有可用的账户', 'ACCOUNT')
+ return null
+ }
+
+ const availableAccounts = this._getAvailableAccounts()
+ if (availableAccounts.length === 0) {
+ logger.warn('所有账户都不可用,使用轮询策略', 'ACCOUNT')
+ return this._getTokenByRoundRobin()
+ }
+
+ // 从可用账户中选择最少使用的
+ const selectedAccount = this._selectLeastUsedAccount(availableAccounts)
+ this._recordUsage(selectedAccount.email)
+
+ return selectedAccount.token
+ }
+
+ /**
+ * 获取指定邮箱的账户令牌
+ * @param {string} email - 邮箱地址
+ * @returns {string|null} 账户令牌或null
+ */
+ getTokenByEmail(email) {
+ const account = this.accounts.find(acc => acc.email === email)
+ if (!account) {
+ logger.error(`未找到邮箱为 ${email} 的账户`, 'ACCOUNT')
+ return null
+ }
+
+ if (!this._isAccountAvailable(account)) {
+ logger.warn(`账户 ${email} 当前不可用`, 'ACCOUNT')
+ return null
+ }
+
+ this._recordUsage(email)
+ return account.token
+ }
+
+ /**
+ * 记录账户使用失败
+ * @param {string} email - 邮箱地址
+ */
+ recordFailure(email) {
+ const currentFailures = this.failureCounts.get(email) || 0
+ this.failureCounts.set(email, currentFailures + 1)
+
+ if (currentFailures + 1 >= this.maxFailures) {
+ logger.warn(`账户 ${email} 失败次数达到上限,将进入冷却期`, 'ACCOUNT')
+ }
+ }
+
+ /**
+ * 重置账户失败计数
+ * @param {string} email - 邮箱地址
+ */
+ resetFailures(email) {
+ this.failureCounts.delete(email)
+ }
+
+ /**
+ * 获取账户统计信息
+ * @returns {Object} 统计信息
+ */
+ getStats() {
+ const total = this.accounts.length
+ const available = this._getAvailableAccounts().length
+ const inCooldown = total - available
+
+ const usageStats = {}
+ this.accounts.forEach(account => {
+ const email = account.email
+ usageStats[email] = {
+ failures: this.failureCounts.get(email) || 0,
+ lastUsed: this.lastUsedTimes.get(email) || null,
+ available: this._isAccountAvailable(account)
+ }
+ })
+
+ return {
+ total,
+ available,
+ inCooldown,
+ currentIndex: this.currentIndex,
+ usageStats
+ }
+ }
+
+ /**
+ * 获取可用账户列表
+ * @private
+ */
+ _getAvailableAccounts() {
+ return this.accounts.filter(account => this._isAccountAvailable(account))
+ }
+
+ /**
+ * 检查账户是否可用
+ * @param {Object} account - 账户对象
+ * @returns {boolean} 是否可用
+ * @private
+ */
+ _isAccountAvailable(account) {
+ if (!account.token) {
+ return false
+ }
+
+ const failures = this.failureCounts.get(account.email) || 0
+ if (failures >= this.maxFailures) {
+ const lastUsed = this.lastUsedTimes.get(account.email)
+ if (lastUsed && Date.now() - lastUsed < this.cooldownPeriod) {
+ return false // 仍在冷却期
+ } else {
+ // 冷却期结束,重置失败计数
+ this.failureCounts.delete(account.email)
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * 选择最少使用的账户
+ * @param {Array} accounts - 可用账户列表
+ * @returns {Object} 选中的账户
+ * @private
+ */
+ _selectLeastUsedAccount(accounts) {
+ if (accounts.length === 1) {
+ return accounts[0]
+ }
+
+ // 按最后使用时间排序,选择最久未使用的
+ return accounts.reduce((least, current) => {
+ const leastLastUsed = this.lastUsedTimes.get(least.email) || 0
+ const currentLastUsed = this.lastUsedTimes.get(current.email) || 0
+
+ return currentLastUsed < leastLastUsed ? current : least
+ })
+ }
+
+ /**
+ * 轮询策略获取令牌
+ * @returns {string|null} 账户令牌或null
+ * @private
+ */
+ _getTokenByRoundRobin() {
+ if (this.currentIndex >= this.accounts.length) {
+ this.currentIndex = 0
+ }
+
+ const account = this.accounts[this.currentIndex]
+ this.currentIndex++
+
+ if (account && account.token) {
+ this._recordUsage(account.email)
+ return account.token
+ }
+
+ // 如果当前账户无效,尝试下一个
+ if (this.currentIndex < this.accounts.length) {
+ return this._getTokenByRoundRobin()
+ }
+
+ return null
+ }
+
+ /**
+ * 记录账户使用
+ * @param {string} email - 邮箱地址
+ * @private
+ */
+ _recordUsage(email) {
+ this.lastUsedTimes.set(email, Date.now())
+ }
+
+ /**
+ * 清理不存在账户的记录
+ * @private
+ */
+ _cleanupRecords() {
+ const currentEmails = new Set(this.accounts.map(acc => acc.email))
+
+ // 清理失败计数记录
+ for (const email of this.failureCounts.keys()) {
+ if (!currentEmails.has(email)) {
+ this.failureCounts.delete(email)
+ }
+ }
+
+ // 清理使用时间记录
+ for (const email of this.lastUsedTimes.keys()) {
+ if (!currentEmails.has(email)) {
+ this.lastUsedTimes.delete(email)
+ }
+ }
+ }
+
+ /**
+ * 重置所有统计数据
+ */
+ reset() {
+ this.currentIndex = 0
+ this.lastUsedTimes.clear()
+ this.failureCounts.clear()
+ }
+}
+
+module.exports = AccountRotator
diff --git a/src/utils/account.js b/src/utils/account.js
new file mode 100644
index 0000000000000000000000000000000000000000..125d79aa8345f13519d127591dd2b46ac75c9063
--- /dev/null
+++ b/src/utils/account.js
@@ -0,0 +1,670 @@
+const config = require('../config/index.js')
+const DataPersistence = require('./data-persistence')
+const TokenManager = require('./token-manager')
+const AccountRotator = require('./account-rotator')
+const { logger } = require('./logger')
+/**
+ * 账户管理器
+ * 统一管理账户、令牌、模型等功能
+ */
+class Account {
+ constructor() {
+ // 初始化各个管理器
+ this.dataPersistence = new DataPersistence()
+ this.tokenManager = new TokenManager()
+ this.accountRotator = new AccountRotator()
+
+ // 账户数据
+ this.accountTokens = []
+ this.isInitialized = false
+
+ // 配置信息
+ this.defaultHeaders = config.defaultHeaders || {}
+
+ // cli请求次数定时刷新器
+ this.cliRequestNumberInterval = null
+ this.cliDailyResetInterval = null
+
+ // 初始化
+ this._initialize()
+ }
+
+ /**
+ * 异步初始化
+ * @private
+ */
+ async _initialize() {
+ try {
+ // 加载账户信息
+ await this.loadAccountTokens()
+
+ // 设置定期刷新令牌
+ if (config.autoRefresh) {
+ this.refreshInterval = setInterval(
+ () => this.autoRefreshTokens(),
+ (config.autoRefreshInterval || 21600) * 1000 // 默认6小时
+ )
+ }
+
+ this.isInitialized = true
+ logger.success(`账户管理器初始化完成,共加载 ${this.accountTokens.length} 个账户`, 'ACCOUNT')
+ } catch (error) {
+ logger.error('账户管理器初始化失败', 'ACCOUNT', '', error)
+ }
+ }
+
+ /**
+ * 加载账户令牌数据
+ * @returns {Promise}
+ */
+ async loadAccountTokens() {
+ try {
+ this.accountTokens = await this.dataPersistence.loadAccounts()
+
+ // 如果是环境变量模式,需要进行登录获取令牌
+ if (config.dataSaveMode === 'none' && this.accountTokens.length > 0) {
+ await this._loginEnvironmentAccounts()
+ }
+
+ // 验证和清理无效令牌
+ await this._validateAndCleanTokens()
+
+ // 更新账户轮询器
+ this.accountRotator.setAccounts(this.accountTokens)
+
+ // 初始化 CLI 账户,随机初始化一个账号
+ if (this.accountTokens.length > 0) {
+ const randomIndex = Math.floor(Math.random() * this.accountTokens.length)
+ const randomAccount = this.accountTokens[randomIndex]
+ logger.info(`初始化 CLI 账户, 随机初始化账号: ${randomAccount.email}`, 'ACCOUNT')
+ await this._initializeCliAccount(randomAccount)
+ }
+
+ // 设置cli定时器 每天00:00:00刷新请求次数
+ this._setupDailyResetTimer()
+
+ logger.success(`成功加载 ${this.accountTokens.length} 个账户`, 'ACCOUNT')
+ } catch (error) {
+ logger.error('加载账户令牌失败', 'ACCOUNT', '', error)
+ this.accountTokens = []
+ }
+ }
+
+ /**
+ * 为环境变量模式的账户进行登录
+ * @private
+ */
+ async _loginEnvironmentAccounts() {
+ const loginPromises = this.accountTokens.map(async (account) => {
+ if (!account.token && account.email && account.password) {
+ const token = await this.tokenManager.login(account.email, account.password)
+ if (token) {
+ const decoded = this.tokenManager.validateToken(token)
+ if (decoded) {
+ account.token = token
+ account.expires = decoded.exp
+ }
+ }
+ }
+ return account
+ })
+
+ this.accountTokens = await Promise.all(loginPromises)
+ }
+
+ /**
+ * 初始化CLI账户
+ * @param {Object} account - 账户对象
+ * @private
+ */
+ async _initializeCliAccount(account) {
+ try {
+ const cliManager = require('./cli.manager')
+ const cliAccount = await cliManager.initCliAccount(account.token)
+
+ if (cliAccount.access_token && cliAccount.refresh_token && cliAccount.expiry_date) {
+ account.cli_info = {
+ access_token: cliAccount.access_token,
+ refresh_token: cliAccount.refresh_token,
+ expiry_date: cliAccount.expiry_date,
+ refresh_token_interval: setInterval(async () => {
+ try {
+ const refreshToken = await cliManager.refreshAccessToken({
+ access_token: account.cli_info.access_token,
+ refresh_token: account.cli_info.refresh_token,
+ expiry_date: account.cli_info.expiry_date
+ })
+ if (refreshToken.access_token && refreshToken.refresh_token && refreshToken.expiry_date) {
+ account.cli_info.access_token = refreshToken.access_token
+ account.cli_info.refresh_token = refreshToken.refresh_token
+ account.cli_info.expiry_date = refreshToken.expiry_date
+ logger.info(`CLI账户 ${account.email} 令牌刷新成功`, 'CLI')
+ }
+ } catch (error) {
+ logger.error(`CLI账户 ${account.email} 令牌刷新失败`, 'CLI', '', error)
+ }
+ // 每2小时刷新一次
+ }, 1000 * 60 * 60 * 2),
+ request_number: 0
+ }
+ logger.success(`CLI账户 ${account.email} 初始化成功`, 'CLI')
+ } else {
+ logger.error(`CLI账户 ${account.email} 初始化失败:无效的响应数据`, 'CLI')
+ }
+ } catch (error) {
+ logger.error(`CLI账户 ${account.email} 初始化失败`, 'CLI', '', error)
+ }
+ }
+
+ /**
+ * 设置每日重置定时器
+ * @private
+ */
+ _setupDailyResetTimer() {
+ logger.info('设置CLI请求次数每日重置定时器', 'CLI')
+
+ // 计算到下一天00:00:00的毫秒数
+ const now = new Date()
+ const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0)
+ const timeDiff = tomorrow.getTime() - now.getTime()
+
+ logger.info(`距离下次重置还有 ${Math.round(timeDiff / 1000 / 60)} 分钟`, 'CLI')
+
+ // 首次执行使用setTimeout
+ this.cliRequestNumberInterval = setTimeout(() => {
+ // 重置所有CLI账户的请求次数
+ this._resetCliRequestNumbers()
+
+ // 设置每24小时执行一次的定时器
+ this.cliDailyResetInterval = setInterval(() => {
+ this._resetCliRequestNumbers()
+ }, 24 * 60 * 60 * 1000)
+ }, timeDiff)
+ }
+
+ /**
+ * 重置CLI请求次数
+ * @private
+ */
+ _resetCliRequestNumbers() {
+ const cliAccounts = this.accountTokens.filter(account => account.cli_info)
+ cliAccounts.forEach(account => {
+ account.cli_info.request_number = 0
+ })
+ logger.info(`已重置 ${cliAccounts.length} 个CLI账户的请求次数`, 'CLI')
+ }
+
+ /**
+ * 验证和清理无效令牌
+ * @private
+ */
+ async _validateAndCleanTokens() {
+ const validAccounts = []
+
+ for (const account of this.accountTokens) {
+ if (account.token && this.tokenManager.validateToken(account.token)) {
+ validAccounts.push(account)
+ } else if (account.email && account.password) {
+ // 尝试重新登录
+ logger.info(`令牌无效,尝试重新登录: ${account.email}`, 'TOKEN', '🔄')
+ const newToken = await this.tokenManager.login(account.email, account.password)
+ if (newToken) {
+ const decoded = this.tokenManager.validateToken(newToken)
+ if (decoded) {
+ account.token = newToken
+ account.expires = decoded.exp
+ validAccounts.push(account)
+ }
+ }
+ }
+ }
+
+ this.accountTokens = validAccounts
+ }
+
+
+ /**
+ * 自动刷新即将过期的令牌
+ * @param {number} thresholdHours - 过期阈值(小时)
+ * @returns {Promise} 成功刷新的令牌数量
+ */
+ async autoRefreshTokens(thresholdHours = 24) {
+ if (!this.isInitialized) {
+ logger.warn('账户管理器尚未初始化,跳过自动刷新', 'TOKEN')
+ return 0
+ }
+
+ logger.info('开始自动刷新令牌...', 'TOKEN', '🔄')
+
+ // 获取需要刷新的账户
+ const needsRefresh = this.accountTokens.filter(account =>
+ this.tokenManager.isTokenExpiringSoon(account.token, thresholdHours)
+ )
+
+ if (needsRefresh.length === 0) {
+ logger.info('没有需要刷新的令牌', 'TOKEN')
+ return 0
+ }
+
+ logger.info(`发现 ${needsRefresh.length} 个令牌需要刷新`, 'TOKEN')
+
+ let successCount = 0
+ let failedCount = 0
+
+ // 逐个刷新账户,每次成功后立即保存
+ for (const account of needsRefresh) {
+ try {
+ const updatedAccount = await this.tokenManager.refreshToken(account)
+ if (updatedAccount) {
+ // 立即更新内存中的账户数据
+ const index = this.accountTokens.findIndex(acc => acc.email === account.email)
+ if (index !== -1) {
+ this.accountTokens[index] = updatedAccount
+ }
+
+ // 立即保存到持久化存储
+ await this.dataPersistence.saveAccount(account.email, {
+ password: updatedAccount.password,
+ token: updatedAccount.token,
+ expires: updatedAccount.expires
+ })
+
+ // 重置失败计数
+ this.accountRotator.resetFailures(account.email)
+ successCount++
+
+ logger.info(`账户 ${account.email} 令牌刷新并保存成功 (${successCount}/${needsRefresh.length})`, 'TOKEN', '✅')
+ } else {
+ // 记录失败的账户
+ this.accountRotator.recordFailure(account.email)
+ failedCount++
+ logger.error(`账户 ${account.email} 令牌刷新失败 (${failedCount} 个失败)`, 'TOKEN', '❌')
+ }
+ } catch (error) {
+ this.accountRotator.recordFailure(account.email)
+ failedCount++
+ logger.error(`账户 ${account.email} 刷新过程中出错`, 'TOKEN', '', error)
+ }
+
+ // 添加延迟避免请求过于频繁
+ await this._delay(1000)
+ }
+
+ // 更新轮询器
+ this.accountRotator.setAccounts(this.accountTokens)
+
+ logger.success(`令牌刷新完成: 成功 ${successCount} 个,失败 ${failedCount} 个`, 'TOKEN')
+ return successCount
+ }
+
+ /**
+ * 获取可用的账户令牌
+ * @returns {string|null} 账户令牌或null
+ */
+ getAccountToken() {
+ if (!this.isInitialized) {
+ logger.warn('账户管理器尚未初始化完成', 'ACCOUNT')
+ return null
+ }
+
+ if (this.accountTokens.length === 0) {
+ logger.error('没有可用的账户令牌', 'ACCOUNT')
+ return null
+ }
+
+ const token = this.accountRotator.getNextToken()
+ if (!token) {
+ logger.error('所有账户令牌都不可用', 'ACCOUNT')
+ }
+
+ return token
+ }
+
+ /**
+ * 根据邮箱获取特定账户的令牌
+ * @param {string} email - 邮箱地址
+ * @returns {string|null} 账户令牌或null
+ */
+ getTokenByEmail(email) {
+ return this.accountRotator.getTokenByEmail(email)
+ }
+
+ /**
+ * 保存更新后的账户数据
+ * @param {Array} updatedAccounts - 更新后的账户列表
+ * @private
+ */
+ async _saveUpdatedAccounts(updatedAccounts) {
+ try {
+ for (const account of updatedAccounts) {
+ await this.dataPersistence.saveAccount(account.email, {
+ password: account.password,
+ token: account.token,
+ expires: account.expires
+ })
+ }
+ } catch (error) {
+ logger.error('保存更新后的账户数据失败', 'ACCOUNT', '', error)
+ }
+ }
+
+ /**
+ * 手动刷新指定账户的令牌
+ * @param {string} email - 邮箱地址
+ * @returns {Promise} 刷新是否成功
+ */
+ async refreshAccountToken(email) {
+ const account = this.accountTokens.find(acc => acc.email === email)
+ if (!account) {
+ logger.error(`未找到邮箱为 ${email} 的账户`, 'ACCOUNT')
+ return false
+ }
+
+ const updatedAccount = await this.tokenManager.refreshToken(account)
+ if (updatedAccount) {
+ // 更新内存中的数据
+ const index = this.accountTokens.findIndex(acc => acc.email === email)
+ if (index !== -1) {
+ this.accountTokens[index] = updatedAccount
+ }
+
+ // 保存到持久化存储
+ await this.dataPersistence.saveAccount(email, {
+ password: updatedAccount.password,
+ token: updatedAccount.token,
+ expires: updatedAccount.expires
+ })
+
+ // 重置失败计数
+ this.accountRotator.resetFailures(email)
+
+ return true
+ }
+
+ return false
+ }
+
+ // 更新销毁方法,清除定时器
+ destroy() {
+ if (this.saveInterval) {
+ clearInterval(this.saveInterval)
+ }
+ if (this.refreshInterval) {
+ clearInterval(this.refreshInterval)
+ }
+ }
+
+
+
+ /**
+ * 生成 Markdown 表格
+ * @param {Array} websites - 网站信息数组
+ * @param {string} mode - 模式 ('table' 或 'text')
+ * @returns {Promise} Markdown 字符串
+ */
+ async generateMarkdownTable(websites, mode) {
+ // 输入校验
+ if (!Array.isArray(websites) || websites.length === 0) {
+ return ''
+ }
+
+ let markdown = ''
+ if (mode === 'table') {
+ markdown += '| **序号** | **网站URL** | **来源** |\n'
+ markdown += '|:---|:---|:---|\n'
+ }
+
+ // 默认值
+ const DEFAULT_TITLE = '未知标题'
+ const DEFAULT_URL = 'https://www.baidu.com'
+ const DEFAULT_HOSTNAME = '未知来源'
+
+ // 表格内容
+ websites.forEach((site, index) => {
+ const { title, url, hostname } = site
+ // 处理字段值,若为空则使用默认值
+ const urlCell = `[${title || DEFAULT_TITLE}](${url || DEFAULT_URL})`
+ const hostnameCell = hostname || DEFAULT_HOSTNAME
+ if (mode === 'table') {
+ markdown += `| ${index + 1} | ${urlCell} | ${hostnameCell} |\n`
+ } else {
+ markdown += `[${index + 1}] ${urlCell} | 来源: ${hostnameCell}\n`
+ }
+ })
+
+ return markdown
+ }
+
+
+
+ /**
+ * 获取所有账户信息
+ * @returns {Array} 账户列表
+ */
+ getAllAccountKeys() {
+ return this.accountTokens
+ }
+
+ /**
+ * 用户登录(委托给 TokenManager)
+ * @param {string} email - 邮箱
+ * @param {string} password - 密码
+ * @returns {Promise} 令牌或null
+ */
+ async login(email, password) {
+ return await this.tokenManager.login(email, password)
+ }
+
+ /**
+ * 获取账户健康状态统计
+ * @returns {Object} 健康状态统计
+ */
+ getHealthStats() {
+ const tokenStats = this.tokenManager.getTokenHealthStats(this.accountTokens)
+ const rotatorStats = this.accountRotator.getStats()
+
+ return {
+ accounts: tokenStats,
+ rotation: rotatorStats,
+ initialized: this.isInitialized
+ }
+ }
+
+ /**
+ * 记录账户使用失败
+ * @param {string} email - 邮箱地址
+ */
+ recordAccountFailure(email) {
+ this.accountRotator.recordFailure(email)
+ }
+
+ /**
+ * 重置账户失败计数
+ * @param {string} email - 邮箱地址
+ */
+ resetAccountFailures(email) {
+ this.accountRotator.resetFailures(email)
+ }
+
+ /**
+ * 添加新账户
+ * @param {string} email - 邮箱
+ * @param {string} password - 密码
+ * @returns {Promise} 添加是否成功
+ */
+ async addAccount(email, password) {
+ try {
+ // 检查账户是否已存在
+ const existingAccount = this.accountTokens.find(acc => acc.email === email)
+ if (existingAccount) {
+ logger.warn(`账户 ${email} 已存在`, 'ACCOUNT')
+ return false
+ }
+
+ // 尝试登录获取令牌
+ const token = await this.tokenManager.login(email, password)
+ if (!token) {
+ logger.error(`账户 ${email} 登录失败,无法添加`, 'ACCOUNT')
+ return false
+ }
+
+ const decoded = this.tokenManager.validateToken(token)
+ if (!decoded) {
+ logger.error(`账户 ${email} 令牌无效,无法添加`, 'ACCOUNT')
+ return false
+ }
+
+ const newAccount = {
+ email,
+ password,
+ token,
+ expires: decoded.exp
+ }
+
+ // 添加到内存
+ this.accountTokens.push(newAccount)
+
+ // 保存到持久化存储
+ await this.dataPersistence.saveAccount(email, newAccount)
+
+ // 更新轮询器
+ this.accountRotator.setAccounts(this.accountTokens)
+
+ logger.success(`成功添加账户: ${email}`, 'ACCOUNT')
+ return true
+ } catch (error) {
+ logger.error(`添加账户失败 (${email})`, 'ACCOUNT', '', error)
+ return false
+ }
+ }
+
+ /**
+ * 移除账户
+ * @param {string} email - 邮箱地址
+ * @returns {Promise} 移除是否成功
+ */
+ async removeAccount(email) {
+ try {
+ const index = this.accountTokens.findIndex(acc => acc.email === email)
+ if (index === -1) {
+ logger.warn(`账户 ${email} 不存在`, 'ACCOUNT')
+ return false
+ }
+
+ // 从内存中移除
+ this.accountTokens.splice(index, 1)
+
+ // 更新轮询器
+ this.accountRotator.setAccounts(this.accountTokens)
+
+ logger.success(`成功移除账户: ${email}`, 'ACCOUNT')
+ return true
+ } catch (error) {
+ logger.error(`移除账户失败 (${email})`, 'ACCOUNT', '', error)
+ return false
+ }
+ }
+
+ /**
+ * 删除账户(向后兼容)
+ * @param {string} email - 邮箱地址
+ * @returns {boolean} 删除是否成功
+ */
+ deleteAccount(email) {
+ const index = this.accountTokens.findIndex(t => t.email === email)
+ if (index !== -1) {
+ this.accountTokens.splice(index, 1)
+ this.accountRotator.setAccounts(this.accountTokens)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * 为指定账户初始化CLI信息(公共方法)
+ * @param {Object} account - 账户对象
+ * @returns {Promise} 初始化是否成功
+ */
+ async initializeCliForAccount(account) {
+ if (!account) {
+ logger.error('账户对象不能为空', 'CLI')
+ return false
+ }
+
+ try {
+ await this._initializeCliAccount(account)
+ return true
+ } catch (error) {
+ logger.error(`为账户 ${account.email} 初始化CLI失败`, 'CLI', '', error)
+ return false
+ }
+ }
+
+ /**
+ * 延迟函数
+ * @param {number} ms - 延迟毫秒数
+ * @private
+ */
+ async _delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+ }
+
+ /**
+ * 清理资源
+ */
+ destroy() {
+ // 清理自动刷新定时器
+ if (this.refreshInterval) {
+ clearInterval(this.refreshInterval)
+ this.refreshInterval = null
+ }
+
+ // 清理CLI请求次数重置定时器
+ if (this.cliRequestNumberInterval) {
+ clearTimeout(this.cliRequestNumberInterval)
+ this.cliRequestNumberInterval = null
+ }
+
+ if (this.cliDailyResetInterval) {
+ clearInterval(this.cliDailyResetInterval)
+ this.cliDailyResetInterval = null
+ }
+
+ // 清理所有CLI账户的刷新定时器
+ this.accountTokens.forEach(account => {
+ if (account.cli_info && account.cli_info.refresh_token_interval) {
+ clearInterval(account.cli_info.refresh_token_interval)
+ account.cli_info.refresh_token_interval = null
+ }
+ })
+
+ this.accountRotator.reset()
+ logger.info('账户管理器已清理资源', 'ACCOUNT', '🧹')
+ }
+
+}
+
+if (!(process.env.API_KEY || config.apiKey)) {
+ logger.error('请务必设置 API_KEY 环境变量', 'CONFIG', '⚙️')
+ process.exit(1)
+}
+
+const accountManager = new Account()
+
+// 添加进程退出时的清理
+process.on('exit', () => {
+ if (accountManager) {
+ accountManager.destroy()
+ }
+})
+
+// 处理意外退出
+process.on('SIGINT', () => {
+ if (accountManager) {
+ accountManager.destroy()
+ }
+ process.exit(0)
+})
+
+
+module.exports = accountManager
\ No newline at end of file
diff --git a/src/utils/chat-helpers.js b/src/utils/chat-helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..59f64bfc7bf302809b452190ecdf5822caa83a6b
--- /dev/null
+++ b/src/utils/chat-helpers.js
@@ -0,0 +1,376 @@
+const { logger } = require('./logger');
+const { sha256Encrypt, generateUUID } = require('./tools.js');
+const { uploadFileToQwenOss } = require('./upload.js');
+const accountManager = require('./account.js');
+const CacheManager = require('./img-caches.js');
+
+/**
+ * 判断聊天类型
+ * @param {string} model - 模型名称
+ * @param {boolean} search - 是否搜索模式
+ * @returns {string} 聊天类型 ('search' 或 't2t')
+ */
+const isChatType = (model) => {
+ if (!model) return 't2t';
+ if (model.includes('-search')) {
+ return 'search';
+ } else if (model.includes('-image-edit')) {
+ return 'image_edit';
+ } else if (model.includes('-image')) {
+ return 't2i';
+ } else if (model.includes('-video')) {
+ return 't2v';
+ } else if (model.includes('-deep-research')) {
+ return 'deep_research';
+ } else {
+ return 't2t';
+ }
+}
+
+/**
+ * 判断是否启用思考模式
+ * @param {string} model - 模型名称
+ * @param {boolean} enable_thinking - 是否启用思考
+ * @param {number} thinking_budget - 思考预算
+ * @returns {object} 思考配置对象
+ */
+const isThinkingEnabled = (model, enable_thinking, thinking_budget) => {
+ const thinking_config = {
+ "output_schema": "phase",
+ "thinking_enabled": false,
+ "thinking_budget": 81920
+ }
+
+ if (!model) return thinking_config;
+
+ if (model.includes('-thinking') || enable_thinking) {
+ thinking_config.thinking_enabled = true;
+ }
+
+ if (thinking_budget && Number(thinking_budget) !== Number.NaN && Number(thinking_budget) > 0 && Number(thinking_budget) < 38912) {
+ thinking_config.budget = Number(thinking_budget);
+ }
+
+ return thinking_config;
+}
+
+/**
+ * 解析模型名称,移除特殊后缀
+ * @param {string} model - 原始模型名称
+ * @returns {string} 解析后的模型名称
+ */
+const parserModel = (model) => {
+ if (!model) return 'qwen3-coder-plus';
+
+ try {
+ model = String(model);
+ model = model.replace('-search', '');
+ model = model.replace('-thinking', '');
+ model = model.replace('-edit', '');
+ model = model.replace('-video', '');
+ model = model.replace('-deep-research', '');
+ model = model.replace('-image', '');
+ return model;
+ } catch (e) {
+ return 'qwen3-coder-plus';
+ }
+}
+
+/**
+ * 从消息中提取文本内容
+ * @param {string|Array} content - 消息内容
+ * @returns {string} 提取的文本
+ */
+const extractTextFromContent = (content) => {
+ if (typeof content === 'string') {
+ return content;
+ } else if (Array.isArray(content)) {
+ const textParts = content
+ .filter(item => item.type === 'text')
+ .map(item => item.text || '');
+ return textParts.join(' ');
+ }
+ return '';
+}
+
+/**
+ * 格式化消息为文本(包含角色标注)
+ * @param {object} message - 单条消息
+ * @returns {string} 格式化后的消息文本
+ */
+const formatSingleMessage = (message) => {
+ const role = message.role;
+ const content = extractTextFromContent(message.content);
+ return content.trim() ? `${role}:${content}` : '';
+}
+
+/**
+ * 格式化历史消息为文本前缀
+ * @param {Array} messages - 消息数组(不包含最后一条)
+ * @returns {string} 格式化后的历史消息
+ */
+const formatHistoryMessages = (messages) => {
+ const formattedParts = [];
+
+ for (let message of messages) {
+ const formatted = formatSingleMessage(message);
+ if (formatted) {
+ formattedParts.push(formatted);
+ }
+ }
+
+ return formattedParts.length > 0 ? formattedParts.join(';') : '';
+}
+
+/**
+ * 解析消息格式,处理图片上传和消息结构
+ * @param {Array} messages - 原始消息数组
+ * @param {object} thinking_config - 思考配置
+ * @param {string} chat_type - 聊天类型
+ * @returns {Promise} 解析后的消息数组
+ */
+const parserMessages = async (messages, thinking_config, chat_type) => {
+ try {
+ const feature_config = thinking_config;
+ const imgCacheManager = new CacheManager();
+
+ // 如果只有一条消息,使用原有逻辑处理(不标注角色)
+ if (messages.length <= 1) {
+ logger.network('单条消息,使用原格式处理', 'PARSER');
+ return await processOriginalLogic(messages, thinking_config, chat_type, imgCacheManager);
+ }
+
+ // 多条消息的情况:分离历史消息和最后一条消息
+ logger.network('多条消息,格式化处理并标注角色', 'PARSER');
+ const historyMessages = messages.slice(0, -1);
+ const lastMessage = messages[messages.length - 1];
+
+ // 格式化历史消息为文本前缀
+ const historyText = formatHistoryMessages(historyMessages);
+
+ // 处理最后一条消息
+ let finalContent = [];
+ let lastMessageText = '';
+ const lastMessageRole = lastMessage.role;
+
+ if (typeof lastMessage.content === 'string') {
+ lastMessageText = lastMessage.content;
+ } else if (Array.isArray(lastMessage.content)) {
+ // 处理最后一条消息中的内容
+ for (let item of lastMessage.content) {
+ if (item.type === 'text') {
+ lastMessageText += item.text || '';
+ } else if (item.type === 'image' || item.type === 'image_url') {
+ // 处理图片上传
+ let base64 = null;
+ if (item.type === 'image_url') {
+ base64 = item.image_url.url;
+ }
+
+ if (base64) {
+ const regex = /data:(.+);base64,/;
+ const fileType = base64.match(regex);
+ const fileExtension = fileType && fileType[1] ? fileType[1].split('/')[1] || 'png' : 'png';
+ const filename = `${generateUUID()}.${fileExtension}`;
+ base64 = base64.replace(regex, '');
+ const signature = sha256Encrypt(base64);
+
+ try {
+ const buffer = Buffer.from(base64, 'base64');
+ const cacheIsExist = imgCacheManager.cacheIsExist(signature);
+
+ if (cacheIsExist) {
+ finalContent.push({
+ type: 'image',
+ image: imgCacheManager.getCache(signature).url
+ });
+ } else {
+ const uploadResult = await uploadFileToQwenOss(buffer, filename, accountManager.getAccountToken());
+ if (uploadResult && uploadResult.status === 200) {
+ finalContent.push({
+ type: 'image',
+ image: uploadResult.file_url
+ });
+ imgCacheManager.addCache(signature, uploadResult.file_url);
+ }
+ }
+ } catch (error) {
+ logger.error('图片上传失败', 'UPLOAD', '', error);
+ }
+ }
+ }
+ }
+ }
+
+ // 组合最终内容:历史文本 + 当前消息(带角色标注)
+ let combinedText = '';
+ if (historyText) {
+ combinedText = historyText + ';';
+ }
+ // 添加最后一条消息,带角色标注
+ if (lastMessageText.trim()) {
+ combinedText += `${lastMessageRole}:${lastMessageText}`;
+ }
+
+ // 如果有图片,创建包含文本和图片的content数组
+ if (finalContent.length > 0) {
+ finalContent.unshift({
+ type: 'text',
+ text: combinedText,
+ chat_type: 't2t',
+ feature_config: {
+ "output_schema": "phase",
+ "thinking_enabled": false,
+ }
+ });
+
+ return [
+ {
+ "role": "user",
+ "content": finalContent,
+ "chat_type": chat_type,
+ "extra": {},
+ "feature_config": feature_config
+ }
+ ];
+ } else {
+ // 纯文本情况
+ return [
+ {
+ "role": "user",
+ "content": combinedText,
+ "chat_type": chat_type,
+ "extra": {},
+ "feature_config": feature_config
+ }
+ ];
+ }
+
+ } catch (e) {
+ logger.error('消息解析失败', 'PARSER', '', e);
+ return [
+ {
+ "role": "user",
+ "content": "直接返回字符串: '聊天历史处理有误...'",
+ "chat_type": "t2t",
+ "extra": {},
+ "feature_config": {
+ "output_schema": "phase",
+ "enabled": false,
+ }
+ }
+ ];
+ }
+}
+
+/**
+ * 原有的单条消息处理逻辑
+ * @param {Array} messages - 消息数组
+ * @param {object} thinking_config - 思考配置
+ * @param {string} chat_type - 聊天类型
+ * @param {object} imgCacheManager - 图片缓存管理器
+ * @returns {Promise} 处理后的消息数组
+ */
+const processOriginalLogic = async (messages, thinking_config, chat_type, imgCacheManager) => {
+ const feature_config = thinking_config;
+
+ for (let message of messages) {
+ if (message.role === 'user' || message.role === 'assistant') {
+ message.chat_type = "t2t";
+ message.extra = {};
+ message.feature_config = {
+ "output_schema": "phase",
+ "thinking_enabled": false,
+ };
+
+ if (!Array.isArray(message.content)) continue;
+
+ const newContent = [];
+
+ for (let item of message.content) {
+ if (item.type === 'image' || item.type === 'image_url') {
+ let base64 = null;
+ if (item.type === 'image_url') {
+ base64 = item.image_url.url;
+ }
+ if (base64) {
+ const regex = /data:(.+);base64,/;
+ const fileType = base64.match(regex);
+ const fileExtension = fileType && fileType[1] ? fileType[1].split('/')[1] || 'png' : 'png';
+ const filename = `${generateUUID()}.${fileExtension}`;
+ base64 = base64.replace(regex, '');
+ const signature = sha256Encrypt(base64);
+
+ try {
+ const buffer = Buffer.from(base64, 'base64');
+ const cacheIsExist = imgCacheManager.cacheIsExist(signature);
+ if (cacheIsExist) {
+ delete item.image_url;
+ item.type = 'image';
+ item.image = imgCacheManager.getCache(signature).url;
+ newContent.push(item);
+ } else {
+ const uploadResult = await uploadFileToQwenOss(buffer, filename, accountManager.getAccountToken());
+ if (uploadResult && uploadResult.status === 200) {
+ delete item.image_url;
+ item.type = 'image';
+ item.image = uploadResult.file_url;
+ imgCacheManager.addCache(signature, uploadResult.file_url);
+ newContent.push(item);
+ }
+ }
+
+ } catch (error) {
+ logger.error('图片上传失败', 'UPLOAD', '', error);
+ }
+ }
+ } else if (item.type === 'text') {
+ item.chat_type = 't2t';
+ item.feature_config = {
+ "output_schema": "phase",
+ "thinking_enabled": false,
+ };
+
+ if (newContent.length >= 2) {
+ messages.push({
+ "role": "user",
+ "content": item.text,
+ "chat_type": "t2t",
+ "extra": {},
+ "feature_config": {
+ "output_schema": "phase",
+ "thinking_enabled": false,
+ }
+ });
+ } else {
+ newContent.push(item);
+ }
+ }
+ }
+ } else {
+ if (Array.isArray(message.content)) {
+ let system_prompt = '';
+ for (let item of message.content) {
+ if (item.type === 'text') {
+ system_prompt += item.text;
+ }
+ }
+ if (system_prompt) {
+ message.content = system_prompt;
+ }
+ }
+ }
+ }
+
+ messages[messages.length - 1].feature_config = feature_config;
+ messages[messages.length - 1].chat_type = chat_type;
+
+ return messages;
+}
+
+module.exports = {
+ isChatType,
+ isThinkingEnabled,
+ parserModel,
+ parserMessages
+}
diff --git a/src/utils/cli.manager.js b/src/utils/cli.manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..4178d2b852b4c6d8aabdb86c97a2d73c29f6c3a9
--- /dev/null
+++ b/src/utils/cli.manager.js
@@ -0,0 +1,279 @@
+const crypto = require('crypto')
+const { getProxyAgent, getChatBaseUrl, applyProxyToFetchOptions } = require('./proxy-helper')
+
+/**
+ * 为 PKCE 生成随机代码验证器
+ * @returns {string} 43-128个字符的随机字符串
+ */
+function generateCodeVerifier() {
+ return crypto.randomBytes(32).toString('base64url')
+}
+
+/**
+ * 使用 SHA-256 从代码验证器生成代码挑战
+ * @param {string} codeVerifier - 代码验证器字符串
+ * @returns {string} 代码挑战字符串
+ */
+function generateCodeChallenge(codeVerifier) {
+ const hash = crypto.createHash('sha256')
+ hash.update(codeVerifier)
+ return hash.digest('base64url')
+}
+
+/**
+ * 生成 PKCE 代码验证器和挑战对
+ * @returns {Object} 包含 code_verifier 和 code_challenge 的对象
+ */
+function generatePKCEPair() {
+ const codeVerifier = generateCodeVerifier()
+ const codeChallenge = generateCodeChallenge(codeVerifier)
+ return {
+ code_verifier: codeVerifier,
+ code_challenge: codeChallenge
+ }
+}
+
+class CliAuthManager {
+ /**
+ * 启动 OAuth 设备授权流程
+ * @returns {Promise