clash-linux commited on
Commit
121e67d
·
verified ·
1 Parent(s): d571243

Upload 14 files

Browse files
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependency directories
2
+ node_modules/
3
+
4
+ # Optional npm cache directory
5
+ .npm
6
+
7
+ # dotenv environment variable files
8
+ .env
9
+ .env.local
10
+ .env.development.local
11
+ .env.test.local
12
+ .env.production.local
13
+
14
+ # Config directory
15
+ config/
16
+
17
+ # Log files
18
+ logs/
19
+ *.log
20
+ npm-debug.log*
21
+
22
+ package-lock.json
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine
2
+
3
+ # 设置工作目录
4
+ # The WORKDIR instruction creates the directory if it doesn't exist
5
+ # and sets it as the current working directory.
6
+ # By default, it's created by root. We will chown contents later.
7
+ WORKDIR /app
8
+
9
+ # 复制package.json和package-lock.json
10
+ # The 'node' user and 'node' group are typically pre-defined in node alpine images with UID/GID 1000
11
+ COPY --chown=node:node package*.json ./
12
+
13
+ # 安装依赖
14
+ # Running npm install as root is common to ensure all global and local dependencies are installed correctly.
15
+ # The node_modules directory will be owned by root.
16
+ # If you need the node_modules to be owned by the node user,
17
+ # you could run npm install after USER node, but ensure /app is writable by node.
18
+ RUN npm install
19
+
20
+ # 复制源代码
21
+ COPY --chown=node:node . .
22
+
23
+ # 切换到 "node" 用户
24
+ USER node
25
+
26
+ # 暴露端口
27
+ EXPOSE 3000
28
+
29
+ # 启动命令
30
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,11 +1,176 @@
1
  ---
2
- title: Api
3
- emoji: 🌖
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
- license: mit
9
  ---
 
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PromptLayer API Proxy
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 3000
 
8
  ---
9
+ <div align="center">
10
 
11
+ # 🚀 PromptLayer API 代理服务
12
+
13
+ [![GitHub stars](https://img.shields.io/github/stars/Rfym21/PromptlayerProxy?style=social)](https://github.com/Rfym21/PromptlayerProxy)
14
+ [![Docker Pulls](https://img.shields.io/docker/pulls/rfym21/promptlayer-proxy)](https://hub.docker.com/r/rfym21/promptlayer-proxy)
15
+
16
+ *一个强大的 PromptLayer API 代理服务,支持多种主流 AI 模型*
17
+
18
+ **🔗 [交流群](https://t.me/nodejs_project) | 🐳 [Docker Hub](https://hub.docker.com/r/rfym21/promptlayer-proxy)**
19
+
20
+ </div>
21
+
22
+ ## ✨ 功能特点
23
+
24
+ <div align="center">
25
+
26
+ | 功能 | 状态 | 描述 |
27
+ |------|------|------|
28
+ | 🔄 **OpenAI API 兼容** | ✅ | 完全兼容 OpenAI API 格式 |
29
+ | 🌊 **流式输出** | ✅ | 支持实时流式响应 |
30
+ | 🖼️ **图像处理** | ✅ | 支持图像上传和识别 |
31
+ | ⚖️ **负载均衡** | ✅ | 多账户轮询负载均衡 |
32
+ | 🐳 **容器化部署** | ✅ | Docker 一键部署 |
33
+ | 🔄 **自动刷新** | ✅ | 智能 Token 自动刷新 |
34
+ | 🛠️ **Tools 支持** | ✅ | 支持Tools参数 |
35
+ | 🔌 **其他参数(温度,Max_Tokens)** | ✅ | 支持配置其他参数,设置的参数将覆盖默认参数 |
36
+
37
+ </div>
38
+
39
+ ---
40
+
41
+ ## 🤖 支持的模型
42
+
43
+ <div align="center">
44
+
45
+ | 🏷️ 模型名称 | 📊 最大输出长度 | 🧠 思考长度 | 📈 类型 |
46
+ |-----------|-------------|---------|-------|
47
+ | 🔮 `claude-3-7-sonnet-20250219` | `64,000` | `-` | Anthropic |
48
+ | 🧠 `claude-3-7-sonnet-20250219-thinking` | `64,000` | `32,000` | Anthropic |
49
+ | 🔮 `claude-sonnet-4-20250514` | `64,000` | `-` | Anthropic |
50
+ | 🧠 `claude-sonnet-4-20250514-thinking` | `64,000` | `32,000` | Anthropic |
51
+ | 🔮 `claude-opus-4-20250514` | `32,000` | `-` | Anthropic |
52
+ | 🧠 `claude-opus-4-20250514-thinking` | `32,000` | `16,000` | Anthropic |
53
+ | 🤖 `o4-mini` | `100,000` | `-` | OpenAI |
54
+ | 🤖 `chatgpt-4o-latest` | `-` | `-` | OpenAI |
55
+ | 🤖 `gpt-4.1` | `-` | `-` | OpenAI |
56
+ | 🤖 `gpt-4.5-preview` | `-` | `-` | OpenAI |
57
+
58
+ </div>
59
+
60
+ ---
61
+
62
+ ## 🚀 快速开始
63
+
64
+ ### 方式一:🐳 Docker Compose(推荐)
65
+
66
+ #### 📥 **Step 1**: 下载配置文件
67
+
68
+ ```bash
69
+ curl -o docker-compose.yml https://raw.githubusercontent.com/Rfym21/PromptlayerProxy/refs/heads/main/docker-compose.yml
70
+ ```
71
+
72
+ #### ⚙️ **Step 2**: 配置环境变量
73
+
74
+ 在 `docker-compose.yml` 文件中设置以下参数:
75
+
76
+ ```yaml
77
+ services:
78
+ promptlayer-proxy:
79
+ image: rfym21/promptlayer-proxy:latest
80
+ container_name: promptlayer-proxy
81
+ restart: always
82
+ ports:
83
+ - "3000:3000"
84
+ environment:
85
+ # 🔐 PromptLayer 账号密码
86
+ - ACCOUNTS=your_account1:your_password1,your_account2:your_password2...
87
+ # 🔑 API 认证密钥
88
+ - AUTH_TOKEN=your_auth_token_here
89
+ ```
90
+
91
+ #### 🚀 **Step 3**: 启动服务
92
+
93
+ ```bash
94
+ docker-compose up -d
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 方式二:🐳 Docker CLI
100
+
101
+ ```bash
102
+ docker run -d \
103
+ --name promptlayer-proxy \
104
+ -p 3000:3000 \
105
+ -e ACCOUNTS=your_account:your_password \
106
+ -e AUTH_TOKEN=your_auth_token_here \
107
+ rfym21/promptlayer-proxy:latest
108
+ ```
109
+
110
+ ---
111
+
112
+ ### 方式三:🤗 Hugging Face Spaces
113
+
114
+ 1. **创建 Space**:
115
+ * 访问 [Hugging Face Spaces](https://huggingface.co/new-space) 并创建一个新的 Space。
116
+ * 选择 "Docker" 作为 SDK。
117
+ * 选择适合的硬件(通常免费套餐已足够)。
118
+
119
+ 2. **配置 Secrets**:
120
+ * 在 Space 的 "Settings" 标签页中,找到 "Secrets and variables" 部分。
121
+ * 添加以下 Secrets:
122
+ * `ACCOUNTS`: 你的 PromptLayer 账号和密码,格式同 Docker 部署方式,例如 `your_account1:your_password1,your_account2:your_password2`。
123
+ * `AUTH_TOKEN`: 你设置的 API 认证密钥。
124
+
125
+ 3. **上传文件**:
126
+ * 将本项目的所有文件(包括 `Dockerfile`, `package.json`, `src` 目录等)上传到你的 Space Git 仓库中。
127
+ * 确保 `README.md` 文件头部的 YAML 配置如下(如果通过 Hugging Face 界面创建 Docker Space,会自动生成类似配置,请确保 `app_port` 为 `3000`):
128
+ ```yaml
129
+ ---
130
+ title: PromptLayer API Proxy
131
+ emoji: 🚀
132
+ colorFrom: blue
133
+ colorTo: green
134
+ sdk: docker
135
+ app_port: 3000
136
+ ---
137
+ ```
138
+
139
+ 4. **部署**:
140
+ * Hugging Face Spaces 会自动根据 `Dockerfile` 构建并部署你的应用。
141
+ * 部署完成后,你的服务将在 Space 的 URL 上可用。
142
+
143
+ ---
144
+
145
+ ### 方式四:💻 本地开发
146
+
147
+ #### 📦 **Step 1**: 安装依赖
148
+
149
+ ```bash
150
+ npm install
151
+ ```
152
+
153
+ #### 📝 **Step 2**: 环境配置
154
+
155
+ 创建 `.env` 文件:
156
+
157
+ ```env
158
+ ACCOUNTS=your_account:your_password
159
+ AUTH_TOKEN=your_auth_token_here
160
+ ```
161
+
162
+ #### 🏃 **Step 3**: 启动开发模式
163
+
164
+ ```bash
165
+ npm run dev
166
+ ```
167
+
168
+ ---
169
+
170
+ <div align="center">
171
+
172
+ ## 💬 交流与支持
173
+
174
+ [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/nodejs_project)
175
+
176
+ </div>
docker-compose.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ promptlayer-proxy:
3
+ image: rfym21/promptlayer-proxy:latest
4
+ container_name: promptlayer-proxy
5
+ restart: always
6
+ ports:
7
+ - "3000:3000"
8
+ environment:
9
+ - ACCOUNTS=your_account:your_password # 填入promptlayer账号密码,账号密码之间用:隔开,多个账号用逗号分隔
10
+ - AUTH_TOKEN=your_auth_token_here # 设置API认证密钥
package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "promptlayer-proxy",
3
+ "version": "3.0.1",
4
+ "description": "",
5
+ "main": "src/server.js",
6
+ "scripts": {
7
+ "start": "node src/server.js",
8
+ "dev": "nodemon src/server.js"
9
+ },
10
+ "keywords": [
11
+ "api",
12
+ "promptlayer",
13
+ "express"
14
+ ],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "axios": "^1.6.5",
19
+ "dotenv": "^16.3.1",
20
+ "express": "^4.18.2",
21
+ "form-data": "^4.0.2",
22
+ "uuid": "^11.1.0",
23
+ "ws": "^8.16.0"
24
+ },
25
+ "devDependencies": {
26
+ "nodemon": "^3.0.3"
27
+ }
28
+ }
src/lib/caches.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const crypto = require('crypto')
2
+
3
+ const imageCache = new Map()
4
+
5
+ function computeHash(base64Data) {
6
+ const base64Content = base64Data.includes('base64,')
7
+ ? base64Data.split('base64,')[1]
8
+ : base64Data
9
+
10
+ return crypto.createHash('sha256')
11
+ .update(base64Content)
12
+ .digest('hex')
13
+ }
14
+
15
+ function hasImage(base64Data) {
16
+ const hash = computeHash(base64Data)
17
+ return imageCache.has(hash)
18
+ }
19
+
20
+ function getImageUrl(base64Data) {
21
+ const hash = computeHash(base64Data)
22
+ if (imageCache.has(hash)) {
23
+ return imageCache.get(hash)
24
+ }
25
+ return null
26
+ }
27
+
28
+ function addImage(base64Data, imageUrl) {
29
+ const hash = computeHash(base64Data)
30
+ if (!imageCache.has(hash)) {
31
+ imageCache.set(hash, imageUrl)
32
+ }
33
+ }
34
+
35
+
36
+
37
+ module.exports = {
38
+ hasImage,
39
+ getImageUrl,
40
+ addImage
41
+ }
src/lib/manager.js ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const axios = require("axios")
2
+ require('dotenv').config()
3
+
4
+ class Manager {
5
+ constructor(accounts) {
6
+ this.accounts = []
7
+ this.init(accounts)
8
+ this.current_account = 0
9
+ this.interval = setInterval(() => {
10
+ this.refreshToken()
11
+ }, 1000 * 60 * 60 * 24 * 5)
12
+ }
13
+
14
+ async init(accounts) {
15
+ accounts = accounts.split(",").filter(account => account.trim() !== "")
16
+ for (const account of accounts) {
17
+ const [username, password] = account.split(":")
18
+ this.accounts.push({
19
+ username,
20
+ password,
21
+ token: null,
22
+ clientId: null,
23
+ workspaceId: null,
24
+ access_token: null,
25
+ refresh: null
26
+ })
27
+ }
28
+ }
29
+
30
+ async login(username, password) {
31
+ try {
32
+ const response = await axios.post("https://api.promptlayer.com/login", {
33
+ email: username,
34
+ password: password
35
+ }, {
36
+ headers: {
37
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
38
+ }
39
+ })
40
+
41
+ if (response.data) {
42
+ return response.data.access_token
43
+ }
44
+ return false
45
+ } catch (error) {
46
+ if (error.status === 429) {
47
+ await new Promise(resolve => setTimeout(resolve, 1000))
48
+ return this.login(username, password)
49
+ }
50
+ return false
51
+ }
52
+ }
53
+
54
+ async getClientId(token) {
55
+ try {
56
+ const response = await axios.post("https://api.promptlayer.com/ws-token-request", null, {
57
+ headers: {
58
+ Authorization: "Bearer " + token
59
+ }
60
+ })
61
+ if (response.data.success) {
62
+ const access_token = response.data.token_details.token
63
+ const clientId = response.data.token_details.clientId
64
+ return { access_token, clientId }
65
+ }
66
+ } catch (error) {
67
+ // console.error('获取clientId失败:', error)
68
+ return false
69
+ }
70
+ }
71
+
72
+ async getWorkspaceId(token) {
73
+ try {
74
+ const response = await axios.get("https://api.promptlayer.com/workspaces", {
75
+ headers: {
76
+ Authorization: "Bearer " + token
77
+ }
78
+ })
79
+ if (response.data.success && response.data.workspaces.length > 0) {
80
+ const workspaceId = response.data.workspaces[0].id
81
+ return workspaceId
82
+ }
83
+ } catch (error) {
84
+ // console.error('获取workspaceId失败:', error)
85
+ return false
86
+ }
87
+ }
88
+
89
+ async initAccount(account) {
90
+ const token = await this.login(account.username, account.password)
91
+ if (!token) {
92
+ return false
93
+ }
94
+ const { clientId, access_token } = await this.getClientId(token)
95
+ if (!clientId || !access_token) {
96
+ return false
97
+ }
98
+ const workspaceId = await this.getWorkspaceId(token)
99
+ if (!workspaceId) {
100
+ return false
101
+ }
102
+ account.token = token
103
+ account.clientId = clientId
104
+ account.workspaceId = workspaceId
105
+ account.access_token = access_token
106
+ account.refresh = setInterval(async () => {
107
+ const { access_token, clientId } = await this.getClientId(account.token)
108
+ account.access_token = access_token
109
+ account.clientId = clientId
110
+ console.log(`${account.username} 刷新token成功`)
111
+ }, 1000 * 60 * 30)
112
+ return account
113
+ }
114
+
115
+ async getAccount() {
116
+ const account = this.accounts[this.current_account]
117
+ if (!account) {
118
+ console.error('没有可用的账户')
119
+ return null
120
+ }
121
+
122
+ if (!account.token) {
123
+ console.log(`初始化账户: ${account.username}`)
124
+ const initialized = await this.initAccount(account)
125
+ if (!initialized) {
126
+ console.error(`账户初始化失败: ${account.username}`)
127
+ // 尝试下一个账户
128
+ this.current_account++
129
+ if (this.current_account >= this.accounts.length) {
130
+ this.current_account = 0
131
+ }
132
+ // 递归尝试下一个账户
133
+ return await this.getAccount()
134
+ }
135
+ }
136
+
137
+ console.log(`当前账户: ${account.username}, Token: ${account.token ? 'Valid' : 'Invalid'}`)
138
+ this.current_account++
139
+ if (this.current_account >= this.accounts.length) {
140
+ this.current_account = 0
141
+ }
142
+ return account
143
+ }
144
+
145
+ async refreshToken() {
146
+ this.accounts = []
147
+ this.init(process.env.ACCOUNTS)
148
+ }
149
+
150
+
151
+ }
152
+
153
+
154
+ if (!process.env.ACCOUNTS || process.env.ACCOUNTS === "" || process.env.AUTH_TOKEN === undefined) {
155
+ console.error("ACCOUNTS 或 AUTH_TOKEN 未设置")
156
+ process.exit(1)
157
+ }
158
+
159
+ const manager = new Manager(process.env.ACCOUNTS)
160
+
161
+ module.exports = manager
src/lib/model-map.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const modelMap = {
2
+ "claude-3-7-sonnet-20250219": {
3
+ "provider": "anthropic",
4
+ "name": "claude-3-7-sonnet-latest",
5
+ "model_config_display_name": null,
6
+ "parameters": {
7
+ "max_tokens": 64000,
8
+ "temperature": 1,
9
+ "top_k": 0,
10
+ "top_p": 0
11
+ }
12
+ },
13
+ "claude-3-7-sonnet-20250219-thinking": {
14
+ "provider": "anthropic",
15
+ "name": "claude-3-7-sonnet-latest",
16
+ "model_config_display_name": null,
17
+ "parameters": {
18
+ "max_tokens": 64000,
19
+ "thinking": {
20
+ "type": "enabled",
21
+ "budget_tokens": 32000
22
+ }
23
+ }
24
+ },
25
+ "claude-sonnet-4-20250514": {
26
+ "provider": "anthropic",
27
+ "name": "claude-sonnet-4-20250514",
28
+ "model_config_display_name": null,
29
+ "parameters": {
30
+ "max_tokens": 64000,
31
+ "temperature": 1,
32
+ "top_k": 0,
33
+ "top_p": 0
34
+ }
35
+ },
36
+ "claude-sonnet-4-20250514-thinking": {
37
+ "provider": "anthropic",
38
+ "name": "claude-sonnet-4-20250514",
39
+ "model_config_display_name": null,
40
+ "parameters": {
41
+ "max_tokens": 64000,
42
+ "thinking": {
43
+ "type": "enabled",
44
+ "budget_tokens": 32000
45
+ }
46
+ }
47
+ },
48
+ "claude-opus-4-20250514": {
49
+ "provider": "anthropic",
50
+ "name": "claude-opus-4-20250514",
51
+ "model_config_display_name": null,
52
+ "parameters": {
53
+ "max_tokens": 32000,
54
+ "temperature": 1,
55
+ "top_k": 0,
56
+ "top_p": 0
57
+ }
58
+ },
59
+ "claude-opus-4-20250514-thinking": {
60
+ "provider": "anthropic",
61
+ "name": "claude-opus-4-20250514",
62
+ "model_config_display_name": null,
63
+ "parameters": {
64
+ "max_tokens": 32000,
65
+ "thinking": {
66
+ "type": "enabled",
67
+ "budget_tokens": 16000
68
+ }
69
+ }
70
+ },
71
+ "o4-mini": {
72
+ "provider": "openai",
73
+ "name": "o4-mini",
74
+ "model_config_display_name": null,
75
+ "parameters": {
76
+ "response_format": {
77
+ "type": "text"
78
+ },
79
+ "reasoning_effort": "high",
80
+ "max_completion_tokens": 100000
81
+ }
82
+ },
83
+ "o3": {
84
+ "provider": "openai",
85
+ "name": "o3",
86
+ "model_config_display_name": null,
87
+ "parameters": {
88
+ "response_format": {
89
+ "type": "text"
90
+ },
91
+ "reasoning_effort": "high",
92
+ "max_completion_tokens": 100000
93
+ }
94
+ },
95
+ "chatgpt-4o-latest": {
96
+ "provider": "openai",
97
+ "name": "chatgpt-4o-latest",
98
+ "model_config_display_name": null,
99
+ "parameters": {
100
+ "temperature": 1,
101
+ "seed": 0,
102
+ "response_format": null,
103
+ "top_p": 1,
104
+ "frequency_penalty": 0,
105
+ "presence_penalty": 0
106
+ }
107
+ },
108
+ "gpt-4.1": {
109
+ "provider": "openai",
110
+ "name": "gpt-4.1",
111
+ "model_config_display_name": null,
112
+ "parameters": {
113
+ "temperature": 1,
114
+ "seed": 0,
115
+ "response_format": null,
116
+ "top_p": 1
117
+ }
118
+ },
119
+ "gpt-4.5-preview": {
120
+ "provider": "openai",
121
+ "name": "gpt-4.5-preview",
122
+ "model_config_display_name": null,
123
+ "parameters": {
124
+ "temperature": 1,
125
+ "seed": 0,
126
+ "response_format": null,
127
+ "top_p": 1,
128
+ "frequency_penalty": 0,
129
+ "presence_penalty": 0
130
+ }
131
+ }
132
+
133
+ }
134
+
135
+ module.exports = modelMap
src/lib/tools.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
2
+
3
+ function isJsonString(str) {
4
+ try {
5
+ JSON.parse(str)
6
+ return true
7
+ } catch (e) {
8
+ return false
9
+ }
10
+ }
11
+
12
+
13
+ module.exports = {
14
+ sleep,
15
+ isJsonString
16
+ }
src/lib/upload.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const axios = require('axios')
2
+ const FormData = require('form-data')
3
+ const imageCache = require('./caches')
4
+ const manager = require('./manager')
5
+
6
+
7
+ async function uploadFileBuffer(fileBuffer) {
8
+ try {
9
+ const account = await manager.getAccount()
10
+
11
+ // 检查account是否存在
12
+ if (!account) {
13
+ console.error('无法获取有效的账户')
14
+ return { success: false, error: '账户获取失败' }
15
+ }
16
+
17
+ const authToken = account.token
18
+
19
+ // 检查token是否存在
20
+ if (!authToken) {
21
+ console.error('无法获取有效的认证token')
22
+ return { success: false, error: '认证失败' }
23
+ }
24
+
25
+ // 转换为base64用于缓存检查
26
+ const base64Data = fileBuffer.toString('base64')
27
+
28
+ // 检查缓存中是否已存在此图片
29
+ const cachedUrl = imageCache.getImageUrl(base64Data)
30
+ if (cachedUrl) {
31
+ console.log('使用缓存的图片URL:', cachedUrl)
32
+ return { success: true, file_url: cachedUrl }
33
+ }
34
+
35
+ // 创建表单数据
36
+ const form = new FormData()
37
+
38
+ // 添加文件内容到表单,使用正确的文件名和content-type
39
+ form.append('file', fileBuffer, {
40
+ filename: `image_${Date.now()}.png`,
41
+ contentType: 'image/png'
42
+ })
43
+
44
+ // 设置请求头,添加必要的浏览器相关头信息
45
+ const headers = {
46
+ ...form.getHeaders(),
47
+ 'Authorization': `Bearer ${authToken}`,
48
+ 'Accept': '*/*',
49
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
50
+ 'Origin': 'https://dashboard.promptlayer.com',
51
+ 'Referer': 'https://dashboard.promptlayer.com/',
52
+ 'Sec-Ch-Ua': '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"',
53
+ 'Sec-Ch-Ua-Mobile': '?0',
54
+ 'Sec-Ch-Ua-Platform': '"Windows"',
55
+ 'Sec-Fetch-Dest': 'empty',
56
+ 'Sec-Fetch-Mode': 'cors',
57
+ 'Sec-Fetch-Site': 'same-site',
58
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0',
59
+ }
60
+
61
+ console.log('开始上传图片,大小:', fileBuffer.length, 'bytes')
62
+
63
+ // 发送请求
64
+ const response = await axios.post('https://api.promptlayer.com/upload', form, {
65
+ headers,
66
+ timeout: 30000 // 30秒超时
67
+ })
68
+
69
+ // 如果上传成功,添加到缓存
70
+ if (response.data && response.data.success && response.data.file_url) {
71
+ imageCache.addImage(base64Data, response.data.file_url)
72
+ console.log('图片上传成功:', response.data.file_url)
73
+ }
74
+
75
+ // 返回响应数据
76
+ return response.data
77
+ } catch (error) {
78
+ console.error('图片上传失败:', {
79
+ status: error.response?.status,
80
+ statusText: error.response?.statusText,
81
+ data: error.response?.data,
82
+ message: error.message
83
+ })
84
+ return { success: false, error: error.message }
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ uploadFileBuffer
90
+ }
src/routes/chat.js ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const axios = require('axios')
3
+ const WebSocket = require('ws')
4
+ const router = express.Router()
5
+ const { v4: uuidv4 } = require('uuid')
6
+ const { uploadFileBuffer } = require('../lib/upload')
7
+ const verify = require('./verify')
8
+ const modelMap = require('../lib/model-map')
9
+
10
+
11
+ async function parseMessages(req, res, next) {
12
+ const messages = req.body.messages
13
+ if (!Array.isArray(messages)) {
14
+ req.processedMessages = []
15
+ return next()
16
+ }
17
+
18
+ try {
19
+ const transformedMessages = await Promise.all(messages.map(async (msg) => {
20
+ const message = {
21
+ role: msg.role,
22
+ tool_calls: [],
23
+ template_format: "f-string"
24
+ }
25
+
26
+ if (Array.isArray(msg.content)) {
27
+ const contentItems = await Promise.all(msg.content.map(async (item) => {
28
+ if (item.type === "text") {
29
+ return {
30
+ type: "text",
31
+ text: item.text
32
+ }
33
+ }
34
+ else if (item.type === "image_url") {
35
+ try {
36
+ const base64Match = item.image_url.url.match(/^data:image\/\w+;base64,(.+)$/)
37
+ if (base64Match) {
38
+ const base64 = base64Match[1]
39
+ const data = Buffer.from(base64, 'base64')
40
+ const uploadResult = await uploadFileBuffer(data)
41
+
42
+ return {
43
+ type: "media",
44
+ media: {
45
+ "type": "image",
46
+ "url": uploadResult.file_url,
47
+ "title": `image_${Date.now()}.png`
48
+ }
49
+ }
50
+ } else {
51
+ return {
52
+ type: "media",
53
+ media: {
54
+ "type": "image",
55
+ "url": item.image_url.url,
56
+ "title": "external_image"
57
+ }
58
+ }
59
+ }
60
+ } catch (error) {
61
+ console.error("处理图像时出错:", error)
62
+ return {
63
+ type: "text",
64
+ text: "[图像处理失败]"
65
+ }
66
+ }
67
+ } else {
68
+ return {
69
+ type: "text",
70
+ text: JSON.stringify(item)
71
+ }
72
+ }
73
+ }))
74
+
75
+ message.content = contentItems
76
+ } else {
77
+ message.content = [
78
+ {
79
+ type: "text",
80
+ text: msg.content || ""
81
+ }
82
+ ]
83
+ }
84
+
85
+ return message
86
+ }))
87
+
88
+ req.body.messages = transformedMessages
89
+ return next()
90
+ } catch (error) {
91
+ console.error("处理消息时出错:", error.status)
92
+ req.body.messages = []
93
+ return next(error)
94
+ }
95
+ }
96
+
97
+ async function getChatID(req, res) {
98
+ try {
99
+ const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/playground_sessions'
100
+ const headers = { Authorization: "Bearer " + req.account.token }
101
+ const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"]
102
+ let data = {
103
+ "id": uuidv4(),
104
+ "name": "Not implemented",
105
+ "prompt_blueprint": {
106
+ "inference_client_name": null,
107
+ "metadata": {
108
+ "model": model_data
109
+ },
110
+ "prompt_template": {
111
+ "type": "chat",
112
+ "messages": req.body.messages,
113
+ "tools": req.body.tools || [],
114
+ "tool_choice": req.body.tool_choice || "none",
115
+ "input_variables": [],
116
+ "functions": [],
117
+ "function_call": null
118
+ },
119
+ "provider_base_url_name": null
120
+ },
121
+ "input_variables": []
122
+ }
123
+
124
+ for (const item in req.body) {
125
+ if (item === "messages" || item === "model" || item === "stream") {
126
+ continue
127
+ } else if (model_data.parameters[item]) {
128
+ model_data.parameters[item] = req.body[item]
129
+ }
130
+ }
131
+ data.prompt_blueprint.metadata.model = model_data
132
+ console.log(`模型参数: ${data.prompt_blueprint.metadata.model}`)
133
+
134
+ const response = await axios.put(url, data, { headers })
135
+ if (response.data.success) {
136
+ console.log(`生成会话ID成功: ${response.data.playground_session.id}`)
137
+ req.chatID = response.data.playground_session.id
138
+ return response.data.playground_session.id
139
+ } else {
140
+ return false
141
+ }
142
+ } catch (error) {
143
+ // console.error("错误:", error.response?.data)
144
+ res.status(500).json({
145
+ "error": {
146
+ "message": error.message || "服务器内部错误",
147
+ "type": "server_error",
148
+ "param": null,
149
+ "code": "server_error"
150
+ }
151
+ })
152
+ return false
153
+ }
154
+ }
155
+
156
+ async function sentRequest(req, res) {
157
+ try {
158
+ const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/run_groups'
159
+ const headers = { Authorization: "Bearer " + req.account.token }
160
+ const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"]
161
+ let data = {
162
+ "id": uuidv4(),
163
+ "playground_session_id": req.chatID,
164
+ "shared_prompt_blueprint": {
165
+ "inference_client_name": null,
166
+ "metadata": {
167
+ "model": model_data
168
+ },
169
+ "prompt_template": {
170
+ "type": "chat",
171
+ "messages": req.body.messages,
172
+ "tools": req.body.tools || [],
173
+ "tool_choice": req.body.tool_choice || "none",
174
+ "input_variables": [],
175
+ "functions": [],
176
+ "function_call": null
177
+ },
178
+ "provider_base_url_name": null
179
+ },
180
+ "individual_run_requests": [
181
+ {
182
+ "input_variables": {},
183
+ "run_group_position": 1
184
+ }
185
+ ]
186
+ }
187
+
188
+ for (const item in req.body) {
189
+ if (item === "messages" || item === "model" || item === "stream") {
190
+ continue
191
+ } else if (model_data.parameters[item]) {
192
+ model_data.parameters[item] = req.body[item]
193
+ }
194
+ }
195
+ data.shared_prompt_blueprint.metadata.model = model_data
196
+
197
+ const response = await axios.post(url, data, { headers })
198
+ if (response.data.success) {
199
+ return response.data.run_group.individual_run_requests[0].id
200
+ } else {
201
+ return false
202
+ }
203
+ } catch (error) {
204
+ // console.error("错误:", error.response?.data)
205
+ res.status(500).json({
206
+ "error": {
207
+ "message": error.message || "服务器内部错误",
208
+ "type": "server_error",
209
+ "param": null,
210
+ "code": "server_error"
211
+ }
212
+ })
213
+ }
214
+ }
215
+
216
+ // 聊天完成路由
217
+ router.post('/v1/chat/completions', verify, parseMessages, async (req, res) => {
218
+ // console.log(JSON.stringify(req.body))
219
+
220
+ try {
221
+
222
+ const setHeader = () => {
223
+ try {
224
+ if (req.body.stream === true) {
225
+ res.setHeader('Content-Type', 'text/event-stream')
226
+ res.setHeader('Cache-Control', 'no-cache')
227
+ res.setHeader('Connection', 'keep-alive')
228
+ } else {
229
+ res.setHeader('Content-Type', 'application/json')
230
+ }
231
+ } catch (error) {
232
+ // console.error("设置响应头时出错:", error)
233
+ }
234
+ }
235
+
236
+ const { access_token, clientId } = req.account
237
+ // 生成会话ID
238
+ await getChatID(req, res)
239
+
240
+ // 发送的数据
241
+ const sendAction = `{"action":10,"channel":"user:${clientId}","params":{"agent":"react-hooks/2.0.2"}}`
242
+ // 构建 WebSocket URL
243
+ const wsUrl = `wss://realtime.ably.io/?access_token=${encodeURIComponent(access_token)}&clientId=${clientId}&format=json&heartbeats=true&v=3&agent=ably-js%2F2.0.2%20browser`
244
+ // 创建 WebSocket 连接
245
+ const ws = new WebSocket(wsUrl)
246
+
247
+ // 状态详细
248
+ let ThinkingLastContent = ""
249
+ let TextLastContent = ""
250
+ let ThinkingStart = false
251
+ let ThinkingEnd = false
252
+ let RequestID = ""
253
+ let MessageID = "chatcmpl-" + uuidv4()
254
+ let streamChunk = {
255
+ "id": MessageID,
256
+ "object": "chat.completion.chunk",
257
+ "system_fingerprint": "fp_44709d6fcb",
258
+ "created": Math.floor(Date.now() / 1000),
259
+ "model": req.body.model,
260
+ "choices": [
261
+ {
262
+ "index": 0,
263
+ "delta": {
264
+ "content": null
265
+ },
266
+ "finish_reason": null
267
+ }
268
+ ]
269
+ }
270
+
271
+ ws.on('open', async () => {
272
+ ws.send(sendAction)
273
+ RequestID = await sentRequest(req, res)
274
+ setHeader()
275
+ })
276
+
277
+ ws.on('message', async (data) => {
278
+ try {
279
+ data = data.toString()
280
+ // console.log(JSON.parse(data))
281
+ let ContentText = JSON.parse(data)?.messages?.[0]
282
+ let ContentData = JSON.parse(ContentText?.data)
283
+ const isRequestID = ContentData?.individual_run_request_id
284
+ if (isRequestID != RequestID || !isRequestID) return
285
+
286
+ let output = ""
287
+
288
+ if (ContentText?.name === "UPDATE_LAST_MESSAGE") {
289
+ const MessageArray = ContentData?.payload?.message?.content
290
+ for (const item of MessageArray) {
291
+
292
+ if (item.type === "text") {
293
+ output = item.text.replace(TextLastContent, "")
294
+ if (ThinkingStart && !ThinkingEnd) {
295
+ ThinkingEnd = true
296
+ output = `${output}\n\n</think>`
297
+ }
298
+ TextLastContent = item.text
299
+ }
300
+ else if (item.type === "thinking" && MessageArray.length === 1) {
301
+ output = item.thinking.replace(ThinkingLastContent, "")
302
+ if (!ThinkingStart) {
303
+ ThinkingStart = true
304
+ output = `<think>\n\n${output}`
305
+ }
306
+ ThinkingLastContent = item.thinking
307
+ }
308
+
309
+ }
310
+
311
+ if (req.body.stream === true) {
312
+ streamChunk.choices[0].delta.content = output
313
+ res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
314
+ }
315
+
316
+ }
317
+ else if (ContentText?.name === "INDIVIDUAL_RUN_COMPLETE") {
318
+
319
+ if (req.body.stream !== true) {
320
+ output = ThinkingLastContent ? `<think>\n\n${ThinkingLastContent}\n\n</think>\n\n${TextLastContent}` : TextLastContent
321
+ }
322
+
323
+ if (ThinkingLastContent === "" && TextLastContent === "") {
324
+ output = "该模型在发送请求时遇到错误: \n1. 请检查请求参数,模型支持参数和默认参数可在/v1/models下查看\n2. 参数设置大小是否超过模型限制\n3. 模型当前官网此模型可能负载过高,可以切换别的模型尝试,这属于正常现象\n4. Anthropic系列模型的temperature的取值为0-1,请勿设置超过1的值\n5. 交流与支持群: https://t.me/nodejs_project"
325
+ streamChunk.choices[0].delta.content = output
326
+ res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
327
+ }
328
+
329
+ if (!req.body.stream || req.body.stream !== true) {
330
+ let responseJson = {
331
+ "id": MessageID,
332
+ "object": "chat.completion",
333
+ "created": Math.floor(Date.now() / 1000),
334
+ "system_fingerprint": "fp_44709d6fcb",
335
+ "model": req.body.model,
336
+ "choices": [
337
+ {
338
+ "index": 0,
339
+ "message": {
340
+ "role": "assistant",
341
+ "content": output
342
+ },
343
+ "finish_reason": "stop"
344
+ }
345
+ ],
346
+ "usage": {
347
+ "prompt_tokens": 0,
348
+ "completion_tokens": 0,
349
+ "total_tokens": 0
350
+ }
351
+ }
352
+
353
+ res.json(responseJson)
354
+ ws.close()
355
+ return
356
+ } else {
357
+ // 流式响应:发送结束标记
358
+ let finalChunk = {
359
+ "id": MessageID,
360
+ "object": "chat.completion.chunk",
361
+ "system_fingerprint": "fp_44709d6fcb",
362
+ "created": Math.floor(Date.now() / 1000),
363
+ "model": req.body.model,
364
+ "choices": [
365
+ {
366
+ "index": 0,
367
+ "delta": {},
368
+ "finish_reason": "stop"
369
+ }
370
+ ]
371
+ }
372
+
373
+ res.write(`data: ${JSON.stringify(finalChunk)}\n\n`)
374
+ res.write(`data: [DONE]\n\n`)
375
+ res.end()
376
+ }
377
+ ws.close()
378
+ }
379
+
380
+ } catch (err) {
381
+ // console.error("处理WebSocket消息出错:", err)
382
+ }
383
+ })
384
+
385
+ ws.on('error', (err) => {
386
+ // 标准OpenAI错误响应格式
387
+ res.status(500).json({
388
+ "error": {
389
+ "message": err.message,
390
+ "type": "server_error",
391
+ "param": null,
392
+ "code": "server_error"
393
+ }
394
+ })
395
+ })
396
+
397
+ setTimeout(() => {
398
+ if (ws.readyState === WebSocket.OPEN) {
399
+ ws.close()
400
+ if (!res.headersSent) {
401
+ // 标准OpenAI超时错误响应格式
402
+ res.status(504).json({
403
+ "error": {
404
+ "message": "请求超时",
405
+ "type": "timeout",
406
+ "param": null,
407
+ "code": "timeout_error"
408
+ }
409
+ })
410
+ }
411
+ }
412
+ }, 300 * 1000)
413
+
414
+ } catch (error) {
415
+ console.error("错误:", error)
416
+ // 标准OpenAI通用错误响应格式
417
+ res.status(500).json({
418
+ "error": {
419
+ "message": error.message || "服务器内部错误",
420
+ "type": "server_error",
421
+ "param": null,
422
+ "code": "server_error"
423
+ }
424
+ })
425
+ }
426
+ })
427
+
428
+ module.exports = router
src/routes/models.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const router = express.Router()
3
+ const modelMap = require('../lib/model-map')
4
+
5
+ router.get('/v1/models', (req, res) => {
6
+ console.log('Available model keys in modelMap:', Object.keys(modelMap));
7
+
8
+ const result = Object.keys(modelMap).map((id) => {
9
+ const model_data = {
10
+ id,
11
+ object: "model",
12
+ created: 1626777600,
13
+ owned_by: modelMap[id].provider
14
+ }
15
+ if (modelMap[id].parameters) {
16
+ for (const item in modelMap[id].parameters) {
17
+ model_data[item] = modelMap[id].parameters[item]
18
+ }
19
+ }
20
+ return model_data
21
+ })
22
+
23
+ res.json({
24
+ object: "list",
25
+ data: result
26
+ })
27
+ })
28
+
29
+ module.exports = router
src/routes/verify.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const manager = require('../lib/manager')
2
+ const verify = async (req, res, next) => {
3
+ const authorization = req.headers.authorization
4
+ if (!authorization) {
5
+ return res.status(401).json({ message: 'Unauthorized' })
6
+ }
7
+
8
+ const token = authorization.replace('Bearer ', '')
9
+
10
+ if (token === process.env.AUTH_TOKEN) {
11
+ try {
12
+ req.account = await manager.getAccount()
13
+ if (!req.account) {
14
+ return res.status(503).json({
15
+ error: {
16
+ message: '服务暂时不可用,无法获取有效账户',
17
+ type: 'service_unavailable',
18
+ code: 'account_unavailable'
19
+ }
20
+ })
21
+ }
22
+ // console.log(`身份校验成功,使用账号=> ${JSON.stringify(req.account)}`)
23
+ next()
24
+ } catch (error) {
25
+ console.error('获取账户时出错:', error)
26
+ return res.status(503).json({
27
+ error: {
28
+ message: '服务暂时不可用',
29
+ type: 'service_unavailable',
30
+ code: 'internal_error'
31
+ }
32
+ })
33
+ }
34
+ } else {
35
+ return res.status(401).json({ message: 'Unauthorized' })
36
+ }
37
+ }
38
+
39
+ module.exports = verify
src/server.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const modelsRoute = require('./routes/models')
3
+ const chatRoute = require('./routes/chat')
4
+ require('dotenv').config()
5
+
6
+ // 创建 Express 应用
7
+ const app = express()
8
+
9
+ // 中间件配置
10
+ app.use(express.json({ limit: "50mb" }))
11
+ app.use(express.urlencoded({ limit: "50mb", extended: true }))
12
+
13
+
14
+ // 错误处理
15
+ app.use((err, req, res, next) => {
16
+ console.error(err.stack)
17
+ res.status(500).send('服务器错误')
18
+ })
19
+
20
+ // 注册路由
21
+ app.use(modelsRoute)
22
+ app.use(chatRoute)
23
+
24
+ // 初始化账户系统并启动服务器
25
+ const PORT = process.env.PORT || 3000
26
+
27
+
28
+ app.listen(PORT, () => {
29
+ console.log(`服务器运行在 http://localhost:${PORT}`)
30
+ })
31
+
32
+
33
+ module.exports = app