open-claude-code-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/claude-proxy +284 -0
- package/package.json +36 -0
- package/server.js +478 -0
- package/start.sh +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 lkyxuan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Open Claude Code Proxy
|
|
4
|
+
|
|
5
|
+
**Route API requests through the official Claude Code client**
|
|
6
|
+
|
|
7
|
+
[English](#english) | [中文](#中文)
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/open-claude-code-proxy)
|
|
10
|
+
[](https://www.npmjs.com/package/open-claude-code-proxy)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://nodejs.org/)
|
|
13
|
+
|
|
14
|
+
<img src="https://api.star-history.com/svg?repos=lkyxuan/open-claude-code-proxy&type=Date" alt="Star History Chart" width="600">
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<a name="english"></a>
|
|
21
|
+
|
|
22
|
+
## English
|
|
23
|
+
|
|
24
|
+
### What is this?
|
|
25
|
+
|
|
26
|
+
A local proxy server that forwards Anthropic API requests through the **official Claude Code CLI**. This allows you to use Claude in any app that supports the Anthropic API (like [OpenCode](https://opencode.ai), Cursor, etc.) while leveraging your existing Claude Code authentication.
|
|
27
|
+
|
|
28
|
+
### How it works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Your App (OpenCode/Cursor/etc.)
|
|
32
|
+
↓ POST /v1/messages
|
|
33
|
+
localhost:12346 (This Proxy)
|
|
34
|
+
↓ Spawns `claude --print`
|
|
35
|
+
Claude Code CLI (Official Client)
|
|
36
|
+
↓ Uses your login session
|
|
37
|
+
Anthropic API
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Quick Start
|
|
41
|
+
|
|
42
|
+
#### Option 1: Use npx (Recommended)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx open-claude-code-proxy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Option 2: Install globally
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g open-claude-code-proxy
|
|
52
|
+
claude-proxy start
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Option 3: Clone and run
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/lkyxuan/open-claude-code-proxy.git
|
|
59
|
+
cd open-claude-code-proxy
|
|
60
|
+
./claude-proxy start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Prerequisites
|
|
64
|
+
|
|
65
|
+
1. **Install Claude Code CLI**
|
|
66
|
+
```bash
|
|
67
|
+
npm install -g @anthropic-ai/claude-code
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. **Login to Claude Code**
|
|
71
|
+
```bash
|
|
72
|
+
claude auth login
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. **Configure Claude Code environment** (if using a relay service)
|
|
76
|
+
```bash
|
|
77
|
+
# Add to ~/.zshrc or ~/.bashrc
|
|
78
|
+
export ANTHROPIC_BASE_URL="https://your-relay-service.com/v1"
|
|
79
|
+
export ANTHROPIC_API_KEY="your-api-key"
|
|
80
|
+
source ~/.zshrc
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Configure Your Client
|
|
84
|
+
|
|
85
|
+
Point your app to the local proxy:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"baseURL": "http://localhost:12346",
|
|
90
|
+
"apiKey": "any-string-works"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> **Note**: The API key can be any string - authentication is handled by your Claude Code session.
|
|
95
|
+
|
|
96
|
+
### Commands
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---------|-------------|
|
|
100
|
+
| `claude-proxy start` | Start the proxy (background) |
|
|
101
|
+
| `claude-proxy stop` | Stop the proxy |
|
|
102
|
+
| `claude-proxy restart` | Restart the proxy |
|
|
103
|
+
| `claude-proxy status` | Check status |
|
|
104
|
+
| `claude-proxy logs -f` | View logs (live) |
|
|
105
|
+
| `claude-proxy test` | Test connectivity |
|
|
106
|
+
|
|
107
|
+
### API Endpoints
|
|
108
|
+
|
|
109
|
+
- `GET /health` - Health check
|
|
110
|
+
- `POST /v1/messages` - Messages API (Anthropic-compatible)
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
<a name="中文"></a>
|
|
115
|
+
|
|
116
|
+
## 中文
|
|
117
|
+
|
|
118
|
+
### 这是什么?
|
|
119
|
+
|
|
120
|
+
一个本地代理服务器,将 Anthropic API 请求通过**官方 Claude Code CLI** 转发。这允许你在任何支持 Anthropic API 的应用(如 [OpenCode](https://opencode.ai)、Cursor 等)中使用 Claude,同时利用现有的 Claude Code 登录会话。
|
|
121
|
+
|
|
122
|
+
### 工作原理
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
你的应用 (OpenCode/Cursor/等)
|
|
126
|
+
↓ POST /v1/messages
|
|
127
|
+
localhost:12346 (本代理)
|
|
128
|
+
↓ 调用 `claude --print`
|
|
129
|
+
Claude Code CLI (官方客户端)
|
|
130
|
+
↓ 使用你的登录会话
|
|
131
|
+
Anthropic API
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 快速开始
|
|
135
|
+
|
|
136
|
+
#### 方式 1: 使用 npx(推荐)
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx open-claude-code-proxy
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### 方式 2: 全局安装
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm install -g open-claude-code-proxy
|
|
146
|
+
claude-proxy start
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### 方式 3: 克隆运行
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
git clone https://github.com/lkyxuan/open-claude-code-proxy.git
|
|
153
|
+
cd open-claude-code-proxy
|
|
154
|
+
./claude-proxy start
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 前置条件
|
|
158
|
+
|
|
159
|
+
1. **安装 Claude Code CLI**
|
|
160
|
+
```bash
|
|
161
|
+
npm install -g @anthropic-ai/claude-code
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
2. **登录 Claude Code**
|
|
165
|
+
```bash
|
|
166
|
+
claude auth login
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
3. **配置 Claude Code 环境变量**(如使用中转服务)
|
|
170
|
+
```bash
|
|
171
|
+
# 添加到 ~/.zshrc 或 ~/.bashrc
|
|
172
|
+
export ANTHROPIC_BASE_URL="https://your-relay-service.com/v1"
|
|
173
|
+
export ANTHROPIC_API_KEY="your-api-key"
|
|
174
|
+
source ~/.zshrc
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 配置客户端
|
|
178
|
+
|
|
179
|
+
将你的应用指向本地代理:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"baseURL": "http://localhost:12346",
|
|
184
|
+
"apiKey": "任意字符串"
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **注意**:API Key 可以是任意字符串,实际认证由 Claude Code 会话处理。
|
|
189
|
+
|
|
190
|
+
### 命令说明
|
|
191
|
+
|
|
192
|
+
| 命令 | 说明 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `claude-proxy start` | 启动服务(后台运行) |
|
|
195
|
+
| `claude-proxy stop` | 停止服务 |
|
|
196
|
+
| `claude-proxy restart` | 重启服务 |
|
|
197
|
+
| `claude-proxy status` | 查看状态 |
|
|
198
|
+
| `claude-proxy logs -f` | 查看日志(实时) |
|
|
199
|
+
| `claude-proxy test` | 测试连接 |
|
|
200
|
+
|
|
201
|
+
### API 端点
|
|
202
|
+
|
|
203
|
+
- `GET /health` - 健康检查
|
|
204
|
+
- `POST /v1/messages` - 消息接口(兼容 Anthropic API)
|
|
205
|
+
|
|
206
|
+
### 常见问题
|
|
207
|
+
|
|
208
|
+
| 问题 | 解决方案 |
|
|
209
|
+
|------|----------|
|
|
210
|
+
| 提示需要登录 | 运行 `claude auth login` 重新登录 |
|
|
211
|
+
| 连接失败 | 检查环境变量 `echo $ANTHROPIC_BASE_URL` |
|
|
212
|
+
| 端口被占用 | 修改环境变量 `export PORT=12347` |
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
<div align="center">
|
|
217
|
+
|
|
218
|
+
### Contributing
|
|
219
|
+
|
|
220
|
+
PRs and issues are welcome!
|
|
221
|
+
|
|
222
|
+
### License
|
|
223
|
+
|
|
224
|
+
[MIT](./LICENSE) © 2025
|
|
225
|
+
|
|
226
|
+
</div>
|
package/claude-proxy
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Claude Local Proxy 控制脚本
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
SERVER_JS="$SCRIPT_DIR/server.js"
|
|
7
|
+
PID_FILE="$SCRIPT_DIR/.claude-proxy.pid"
|
|
8
|
+
LOG_FILE="$SCRIPT_DIR/proxy.log"
|
|
9
|
+
|
|
10
|
+
# 颜色定义
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
BLUE='\033[0;34m'
|
|
15
|
+
NC='\033[0m' # No Color
|
|
16
|
+
|
|
17
|
+
# 打印带颜色的消息
|
|
18
|
+
info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
|
19
|
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
|
20
|
+
warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
|
21
|
+
error() { echo -e "${RED}✗${NC} $1"; }
|
|
22
|
+
|
|
23
|
+
# 检查服务是否运行
|
|
24
|
+
is_running() {
|
|
25
|
+
if [ -f "$PID_FILE" ]; then
|
|
26
|
+
PID=$(cat "$PID_FILE")
|
|
27
|
+
if ps -p "$PID" > /dev/null 2>&1; then
|
|
28
|
+
return 0
|
|
29
|
+
else
|
|
30
|
+
# PID 文件存在但进程不存在,清理
|
|
31
|
+
rm -f "$PID_FILE"
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# 检查环境
|
|
39
|
+
check_environment() {
|
|
40
|
+
info "检查运行环境..."
|
|
41
|
+
|
|
42
|
+
# 检查 Node.js
|
|
43
|
+
if ! command -v node &> /dev/null; then
|
|
44
|
+
error "Node.js 未安装"
|
|
45
|
+
echo "请先安装 Node.js: https://nodejs.org/"
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
success "Node.js: $(node --version)"
|
|
49
|
+
|
|
50
|
+
# 检查 Claude Code
|
|
51
|
+
if ! command -v claude &> /dev/null; then
|
|
52
|
+
error "Claude Code 未安装或不在 PATH 中"
|
|
53
|
+
echo ""
|
|
54
|
+
echo "安装方式:"
|
|
55
|
+
echo " 1. npm 安装: npm install -g @anthropic-ai/claude-code"
|
|
56
|
+
echo " 2. 或确保 native 安装的 claude 在 PATH 中"
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
CLAUDE_PATH=$(which claude)
|
|
61
|
+
success "Claude Code: $CLAUDE_PATH"
|
|
62
|
+
|
|
63
|
+
# 检查环境变量
|
|
64
|
+
if [ -z "$ANTHROPIC_BASE_URL" ]; then
|
|
65
|
+
warning "ANTHROPIC_BASE_URL 未设置"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "如需使用中转服务,请设置环境变量:"
|
|
68
|
+
echo " export ANTHROPIC_BASE_URL=\"https://your-relay-service.com/v1\""
|
|
69
|
+
echo " export ANTHROPIC_API_KEY=\"your-api-key\""
|
|
70
|
+
echo ""
|
|
71
|
+
echo "然后执行: source ~/.zshrc # 或 source ~/.bashrc"
|
|
72
|
+
echo ""
|
|
73
|
+
else
|
|
74
|
+
success "ANTHROPIC_BASE_URL: $ANTHROPIC_BASE_URL"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# 测试 Claude Code 连接
|
|
81
|
+
test_claude() {
|
|
82
|
+
info "测试 Claude Code 连接..."
|
|
83
|
+
|
|
84
|
+
if ! check_environment; then
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# 测试简单命令
|
|
89
|
+
if timeout 10s claude --print "test" &> /dev/null; then
|
|
90
|
+
success "Claude Code 连接正常"
|
|
91
|
+
return 0
|
|
92
|
+
else
|
|
93
|
+
error "Claude Code 连接失败"
|
|
94
|
+
echo ""
|
|
95
|
+
echo "可能的原因:"
|
|
96
|
+
echo " 1. Claude Code 未登录(运行: claude auth login)"
|
|
97
|
+
echo " 2. 环境变量配置错误"
|
|
98
|
+
echo " 3. 网络连接问题"
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# 启动服务
|
|
104
|
+
start() {
|
|
105
|
+
if is_running; then
|
|
106
|
+
warning "服务已在运行 (PID: $(cat $PID_FILE))"
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
info "启动 Claude Local Proxy..."
|
|
111
|
+
|
|
112
|
+
# 环境检查
|
|
113
|
+
if ! check_environment; then
|
|
114
|
+
error "环境检查失败,无法启动"
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# 后台启动
|
|
119
|
+
nohup node "$SERVER_JS" > "$LOG_FILE" 2>&1 &
|
|
120
|
+
PID=$!
|
|
121
|
+
echo $PID > "$PID_FILE"
|
|
122
|
+
|
|
123
|
+
# 等待启动
|
|
124
|
+
sleep 2
|
|
125
|
+
|
|
126
|
+
if is_running; then
|
|
127
|
+
success "服务已启动 (PID: $PID)"
|
|
128
|
+
success "监听地址: http://localhost:12346"
|
|
129
|
+
echo ""
|
|
130
|
+
info "查看日志: $0 logs"
|
|
131
|
+
info "查看状态: $0 status"
|
|
132
|
+
return 0
|
|
133
|
+
else
|
|
134
|
+
error "服务启动失败"
|
|
135
|
+
echo ""
|
|
136
|
+
echo "查看日志获取详细信息:"
|
|
137
|
+
echo " tail -f $LOG_FILE"
|
|
138
|
+
return 1
|
|
139
|
+
fi
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# 停止服务
|
|
143
|
+
stop() {
|
|
144
|
+
if ! is_running; then
|
|
145
|
+
warning "服务未运行"
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
PID=$(cat "$PID_FILE")
|
|
150
|
+
info "停止服务 (PID: $PID)..."
|
|
151
|
+
|
|
152
|
+
kill "$PID" 2>/dev/null
|
|
153
|
+
|
|
154
|
+
# 等待进程结束
|
|
155
|
+
for i in {1..10}; do
|
|
156
|
+
if ! ps -p "$PID" > /dev/null 2>&1; then
|
|
157
|
+
break
|
|
158
|
+
fi
|
|
159
|
+
sleep 0.5
|
|
160
|
+
done
|
|
161
|
+
|
|
162
|
+
# 强制结束
|
|
163
|
+
if ps -p "$PID" > /dev/null 2>&1; then
|
|
164
|
+
warning "强制结束进程..."
|
|
165
|
+
kill -9 "$PID" 2>/dev/null
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
rm -f "$PID_FILE"
|
|
169
|
+
success "服务已停止"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# 重启服务
|
|
173
|
+
restart() {
|
|
174
|
+
info "重启服务..."
|
|
175
|
+
stop
|
|
176
|
+
sleep 1
|
|
177
|
+
start
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# 查看状态
|
|
181
|
+
status() {
|
|
182
|
+
if is_running; then
|
|
183
|
+
PID=$(cat "$PID_FILE")
|
|
184
|
+
success "服务运行中"
|
|
185
|
+
echo ""
|
|
186
|
+
echo " PID: $PID"
|
|
187
|
+
echo " 地址: http://localhost:12346"
|
|
188
|
+
echo " 日志: $LOG_FILE"
|
|
189
|
+
echo ""
|
|
190
|
+
|
|
191
|
+
# 显示最近的日志
|
|
192
|
+
if [ -f "$LOG_FILE" ]; then
|
|
193
|
+
echo "最近日志:"
|
|
194
|
+
echo "─────────────────────────────────────"
|
|
195
|
+
tail -n 5 "$LOG_FILE"
|
|
196
|
+
fi
|
|
197
|
+
else
|
|
198
|
+
warning "服务未运行"
|
|
199
|
+
fi
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# 查看日志
|
|
203
|
+
logs() {
|
|
204
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
205
|
+
warning "日志文件不存在: $LOG_FILE"
|
|
206
|
+
return 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
if [ "$1" = "-f" ] || [ "$1" = "--follow" ]; then
|
|
210
|
+
info "实时查看日志 (Ctrl+C 退出)..."
|
|
211
|
+
tail -f "$LOG_FILE"
|
|
212
|
+
else
|
|
213
|
+
cat "$LOG_FILE"
|
|
214
|
+
fi
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# 显示帮助
|
|
218
|
+
help() {
|
|
219
|
+
cat << EOF
|
|
220
|
+
Claude Local Proxy 控制脚本
|
|
221
|
+
|
|
222
|
+
用法: $0 <命令> [选项]
|
|
223
|
+
|
|
224
|
+
命令:
|
|
225
|
+
start 启动服务
|
|
226
|
+
stop 停止服务
|
|
227
|
+
restart 重启服务
|
|
228
|
+
status 查看运行状态
|
|
229
|
+
logs 查看日志
|
|
230
|
+
logs -f 实时查看日志
|
|
231
|
+
test 测试环境和连接
|
|
232
|
+
help 显示此帮助信息
|
|
233
|
+
|
|
234
|
+
示例:
|
|
235
|
+
$0 start # 启动服务
|
|
236
|
+
$0 status # 查看状态
|
|
237
|
+
$0 logs -f # 实时查看日志
|
|
238
|
+
$0 restart # 重启服务
|
|
239
|
+
|
|
240
|
+
配置文件:
|
|
241
|
+
客户端配置 (opencode/cursor):
|
|
242
|
+
{
|
|
243
|
+
"baseURL": "http://localhost:12346",
|
|
244
|
+
"apiKey": "sk-any-key-works"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
更多信息: https://github.com/your-repo/claude-local-proxy
|
|
248
|
+
EOF
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# 主函数
|
|
252
|
+
main() {
|
|
253
|
+
case "${1:-}" in
|
|
254
|
+
start)
|
|
255
|
+
start
|
|
256
|
+
;;
|
|
257
|
+
stop)
|
|
258
|
+
stop
|
|
259
|
+
;;
|
|
260
|
+
restart)
|
|
261
|
+
restart
|
|
262
|
+
;;
|
|
263
|
+
status)
|
|
264
|
+
status
|
|
265
|
+
;;
|
|
266
|
+
logs)
|
|
267
|
+
logs "${2:-}"
|
|
268
|
+
;;
|
|
269
|
+
test)
|
|
270
|
+
test_claude
|
|
271
|
+
;;
|
|
272
|
+
help|--help|-h)
|
|
273
|
+
help
|
|
274
|
+
;;
|
|
275
|
+
*)
|
|
276
|
+
error "未知命令: ${1:-}"
|
|
277
|
+
echo ""
|
|
278
|
+
help
|
|
279
|
+
exit 1
|
|
280
|
+
;;
|
|
281
|
+
esac
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
main "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open-claude-code-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local proxy that forwards API requests through the official Claude Code CLI",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"open-claude-code-proxy": "server.js",
|
|
8
|
+
"claude-local-proxy": "claude-proxy"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node server.js",
|
|
12
|
+
"dev": "node server.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/lkyxuan/open-claude-code-proxy.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"proxy",
|
|
23
|
+
"api",
|
|
24
|
+
"opencode",
|
|
25
|
+
"cursor"
|
|
26
|
+
],
|
|
27
|
+
"author": "lkyxuan",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/lkyxuan/open-claude-code-proxy/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/lkyxuan/open-claude-code-proxy#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const PORT = process.env.PORT || 12346;
|
|
9
|
+
const LOG_FILE = path.join(__dirname, 'proxy.log');
|
|
10
|
+
|
|
11
|
+
// 日志函数 - 同时输出到控制台和文件
|
|
12
|
+
function log(message, data = null) {
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
let logLine = `[${timestamp}] ${message}`;
|
|
15
|
+
if (data) {
|
|
16
|
+
logLine += ' ' + JSON.stringify(data, null, 2);
|
|
17
|
+
}
|
|
18
|
+
console.log(logLine);
|
|
19
|
+
fs.appendFileSync(LOG_FILE, logLine + '\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 启动时清空日志
|
|
23
|
+
fs.writeFileSync(LOG_FILE, `=== Claude Local Proxy 启动于 ${new Date().toISOString()} ===\n`);
|
|
24
|
+
|
|
25
|
+
// 创建 HTTP 服务器
|
|
26
|
+
const server = http.createServer(async (req, res) => {
|
|
27
|
+
// CORS 支持
|
|
28
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
29
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
30
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, anthropic-version, x-api-key');
|
|
31
|
+
|
|
32
|
+
if (req.method === 'OPTIONS') {
|
|
33
|
+
res.writeHead(200);
|
|
34
|
+
res.end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 健康检查
|
|
39
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
40
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
41
|
+
res.end(JSON.stringify({ status: 'ok', service: 'claude-local-proxy' }));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 处理 /messages 和 /v1/messages 请求
|
|
46
|
+
if (req.method === 'POST' && (req.url === '/v1/messages' || req.url === '/messages')) {
|
|
47
|
+
let body = '';
|
|
48
|
+
|
|
49
|
+
req.on('data', chunk => {
|
|
50
|
+
body += chunk.toString();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
req.on('end', async () => {
|
|
54
|
+
try {
|
|
55
|
+
// 提取 API Key(支持多种格式)
|
|
56
|
+
const apiKey = req.headers['x-api-key'] ||
|
|
57
|
+
req.headers['authorization']?.replace('Bearer ', '') ||
|
|
58
|
+
req.headers['anthropic-api-key'] ||
|
|
59
|
+
'anonymous';
|
|
60
|
+
|
|
61
|
+
const requestData = JSON.parse(body);
|
|
62
|
+
|
|
63
|
+
// 限制日志大小,保留最近的请求(超过 100KB 截断)
|
|
64
|
+
try {
|
|
65
|
+
const stats = fs.statSync(LOG_FILE);
|
|
66
|
+
if (stats.size > 100 * 1024) {
|
|
67
|
+
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
|
68
|
+
// 保留最后 50KB
|
|
69
|
+
fs.writeFileSync(LOG_FILE, content.slice(-50 * 1024));
|
|
70
|
+
}
|
|
71
|
+
} catch (e) { /* 文件不存在时忽略 */ }
|
|
72
|
+
|
|
73
|
+
// 详细记录请求信息(用于调试)
|
|
74
|
+
log('========== 收到请求 ==========');
|
|
75
|
+
log('1️⃣ User-Agent', {
|
|
76
|
+
'user-agent': req.headers['user-agent']
|
|
77
|
+
});
|
|
78
|
+
log('2️⃣ 必需 Headers', {
|
|
79
|
+
'x-app': req.headers['x-app'],
|
|
80
|
+
'anthropic-beta': req.headers['anthropic-beta'],
|
|
81
|
+
'anthropic-version': req.headers['anthropic-version']
|
|
82
|
+
});
|
|
83
|
+
log('3️⃣ Stainless SDK Headers', {
|
|
84
|
+
'x-stainless-retry-count': req.headers['x-stainless-retry-count'],
|
|
85
|
+
'x-stainless-timeout': req.headers['x-stainless-timeout'],
|
|
86
|
+
'x-stainless-lang': req.headers['x-stainless-lang'],
|
|
87
|
+
'x-stainless-package-version': req.headers['x-stainless-package-version'],
|
|
88
|
+
'x-stainless-os': req.headers['x-stainless-os'],
|
|
89
|
+
'x-stainless-arch': req.headers['x-stainless-arch'],
|
|
90
|
+
'x-stainless-runtime': req.headers['x-stainless-runtime'],
|
|
91
|
+
'x-stainless-runtime-version': req.headers['x-stainless-runtime-version'],
|
|
92
|
+
'anthropic-dangerous-direct-browser-access': req.headers['anthropic-dangerous-direct-browser-access']
|
|
93
|
+
});
|
|
94
|
+
log('4️⃣ Body metadata', {
|
|
95
|
+
metadata: requestData.metadata,
|
|
96
|
+
user_id_in_metadata: requestData.metadata?.user_id
|
|
97
|
+
});
|
|
98
|
+
log('5️⃣ System Prompt', {
|
|
99
|
+
system: typeof requestData.system === 'string'
|
|
100
|
+
? requestData.system.substring(0, 200) + '...'
|
|
101
|
+
: requestData.system || 'none'
|
|
102
|
+
});
|
|
103
|
+
log('6️⃣ 用户消息', {
|
|
104
|
+
messages: requestData.messages?.map(m => ({
|
|
105
|
+
role: m.role,
|
|
106
|
+
content: typeof m.content === 'string'
|
|
107
|
+
? m.content.substring(0, 300)
|
|
108
|
+
: Array.isArray(m.content)
|
|
109
|
+
? m.content.map(c => c.type === 'text' ? c.text?.substring(0, 300) : `[${c.type}]`).join(' ')
|
|
110
|
+
: '[complex]'
|
|
111
|
+
}))
|
|
112
|
+
});
|
|
113
|
+
log('其他信息', {
|
|
114
|
+
apiKey: apiKey.substring(0, 20) + '...',
|
|
115
|
+
model: requestData.model,
|
|
116
|
+
messagesCount: requestData.messages?.length,
|
|
117
|
+
stream: requestData.stream
|
|
118
|
+
});
|
|
119
|
+
log('================================');
|
|
120
|
+
|
|
121
|
+
// 检查是否需要流式响应
|
|
122
|
+
const isStream = requestData.stream === true;
|
|
123
|
+
|
|
124
|
+
if (isStream) {
|
|
125
|
+
await handleStreamRequest(requestData, res);
|
|
126
|
+
} else {
|
|
127
|
+
await handleNormalRequest(requestData, res);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('[错误]', error);
|
|
131
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
132
|
+
res.end(JSON.stringify({ error: { message: error.message } }));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
// 记录未知请求,方便调试
|
|
137
|
+
console.log(`[${new Date().toISOString()}] 未处理的请求: ${req.method} ${req.url}`);
|
|
138
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
139
|
+
res.end(JSON.stringify({ error: { message: 'Not Found' } }));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 处理普通请求(非流式)
|
|
144
|
+
async function handleNormalRequest(requestData, res) {
|
|
145
|
+
const messages = requestData.messages || [];
|
|
146
|
+
|
|
147
|
+
// 构建 prompt:把 messages 数组转成对话格式
|
|
148
|
+
const prompt = buildPromptFromMessages(messages);
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
// 使用 JSON 输出格式,支持 tool_use
|
|
152
|
+
const claude = spawn('claude', [
|
|
153
|
+
'--print',
|
|
154
|
+
'--output-format', 'json',
|
|
155
|
+
prompt
|
|
156
|
+
], {
|
|
157
|
+
env: { ...process.env },
|
|
158
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
let output = '';
|
|
162
|
+
let errorOutput = '';
|
|
163
|
+
|
|
164
|
+
claude.stdout.on('data', (data) => {
|
|
165
|
+
output += data.toString();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
claude.stderr.on('data', (data) => {
|
|
169
|
+
errorOutput += data.toString();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
claude.on('close', (code) => {
|
|
173
|
+
if (code !== 0) {
|
|
174
|
+
console.error('[Claude 错误]', errorOutput);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// 解析 Claude 的 JSON 输出
|
|
179
|
+
const claudeResponse = JSON.parse(output.trim());
|
|
180
|
+
|
|
181
|
+
// 构建 Anthropic API 格式的响应
|
|
182
|
+
const response = {
|
|
183
|
+
id: claudeResponse.id || `msg_${Date.now()}`,
|
|
184
|
+
type: 'message',
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
content: claudeResponse.content || [{ type: 'text', text: output.trim() }],
|
|
187
|
+
model: requestData.model || 'claude-sonnet-4-20250514',
|
|
188
|
+
stop_reason: claudeResponse.stop_reason || 'end_turn',
|
|
189
|
+
stop_sequence: claudeResponse.stop_sequence || null,
|
|
190
|
+
usage: claudeResponse.usage || {
|
|
191
|
+
input_tokens: 0,
|
|
192
|
+
output_tokens: 0
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
197
|
+
res.end(JSON.stringify(response));
|
|
198
|
+
resolve();
|
|
199
|
+
} catch (parseError) {
|
|
200
|
+
// 如果 JSON 解析失败,fallback 到纯文本模式
|
|
201
|
+
console.error('[JSON 解析错误]', parseError);
|
|
202
|
+
|
|
203
|
+
const response = {
|
|
204
|
+
id: `msg_${Date.now()}`,
|
|
205
|
+
type: 'message',
|
|
206
|
+
role: 'assistant',
|
|
207
|
+
content: [{ type: 'text', text: output.trim() }],
|
|
208
|
+
model: requestData.model || 'claude-sonnet-4-20250514',
|
|
209
|
+
stop_reason: 'end_turn',
|
|
210
|
+
stop_sequence: null,
|
|
211
|
+
usage: {
|
|
212
|
+
input_tokens: 0,
|
|
213
|
+
output_tokens: 0
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
218
|
+
res.end(JSON.stringify(response));
|
|
219
|
+
resolve();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
claude.on('error', (error) => {
|
|
224
|
+
console.error('[Spawn 错误]', error);
|
|
225
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
226
|
+
res.end(JSON.stringify({ error: { message: error.message } }));
|
|
227
|
+
reject(error);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 处理流式请求
|
|
233
|
+
async function handleStreamRequest(requestData, res) {
|
|
234
|
+
const messages = requestData.messages || [];
|
|
235
|
+
|
|
236
|
+
// 使用 stream-json 格式与 Claude Code 交互
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
const claude = spawn('claude', [
|
|
239
|
+
'--print',
|
|
240
|
+
'--input-format', 'stream-json',
|
|
241
|
+
'--output-format', 'stream-json',
|
|
242
|
+
'--verbose'
|
|
243
|
+
], {
|
|
244
|
+
env: { ...process.env },
|
|
245
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 设置 SSE 响应头
|
|
249
|
+
res.writeHead(200, {
|
|
250
|
+
'Content-Type': 'text/event-stream',
|
|
251
|
+
'Cache-Control': 'no-cache',
|
|
252
|
+
'Connection': 'keep-alive'
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 发送消息开始事件
|
|
256
|
+
const messageId = `msg_${Date.now()}`;
|
|
257
|
+
sendSSE(res, 'message_start', {
|
|
258
|
+
type: 'message_start',
|
|
259
|
+
message: {
|
|
260
|
+
id: messageId,
|
|
261
|
+
type: 'message',
|
|
262
|
+
role: 'assistant',
|
|
263
|
+
content: [],
|
|
264
|
+
model: requestData.model || 'claude-sonnet-4-20250514',
|
|
265
|
+
stop_reason: null,
|
|
266
|
+
stop_sequence: null,
|
|
267
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 发送 content block 开始
|
|
272
|
+
sendSSE(res, 'content_block_start', {
|
|
273
|
+
type: 'content_block_start',
|
|
274
|
+
index: 0,
|
|
275
|
+
content_block: { type: 'text', text: '' }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
let buffer = '';
|
|
279
|
+
let fullText = '';
|
|
280
|
+
let contentBlockIndex = 0;
|
|
281
|
+
let currentBlockType = null;
|
|
282
|
+
|
|
283
|
+
claude.stdout.on('data', (data) => {
|
|
284
|
+
buffer += data.toString();
|
|
285
|
+
|
|
286
|
+
// 解析 JSON 行
|
|
287
|
+
const lines = buffer.split('\n');
|
|
288
|
+
buffer = lines.pop() || ''; // 保留不完整的行
|
|
289
|
+
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (!line.trim()) continue;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const json = JSON.parse(line);
|
|
295
|
+
|
|
296
|
+
if (json.type === 'assistant' && json.message?.content) {
|
|
297
|
+
for (const content of json.message.content) {
|
|
298
|
+
// 处理文本内容
|
|
299
|
+
if (content.type === 'text' && content.text) {
|
|
300
|
+
// 增量发送文本
|
|
301
|
+
const newText = content.text.slice(fullText.length);
|
|
302
|
+
if (newText) {
|
|
303
|
+
fullText = content.text;
|
|
304
|
+
sendSSE(res, 'content_block_delta', {
|
|
305
|
+
type: 'content_block_delta',
|
|
306
|
+
index: 0,
|
|
307
|
+
delta: { type: 'text_delta', text: newText }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 处理工具调用
|
|
313
|
+
if (content.type === 'tool_use') {
|
|
314
|
+
// 发送前一个 block 结束(如果有)
|
|
315
|
+
if (currentBlockType) {
|
|
316
|
+
sendSSE(res, 'content_block_stop', {
|
|
317
|
+
type: 'content_block_stop',
|
|
318
|
+
index: contentBlockIndex
|
|
319
|
+
});
|
|
320
|
+
contentBlockIndex++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 发送新的 tool_use block 开始
|
|
324
|
+
sendSSE(res, 'content_block_start', {
|
|
325
|
+
type: 'content_block_start',
|
|
326
|
+
index: contentBlockIndex,
|
|
327
|
+
content_block: {
|
|
328
|
+
type: 'tool_use',
|
|
329
|
+
id: content.id,
|
|
330
|
+
name: content.name,
|
|
331
|
+
input: {}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// 发送工具输入
|
|
336
|
+
sendSSE(res, 'content_block_delta', {
|
|
337
|
+
type: 'content_block_delta',
|
|
338
|
+
index: contentBlockIndex,
|
|
339
|
+
delta: {
|
|
340
|
+
type: 'input_json_delta',
|
|
341
|
+
partial_json: JSON.stringify(content.input)
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
currentBlockType = 'tool_use';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch (e) {
|
|
350
|
+
// 忽略解析错误
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
claude.stderr.on('data', (data) => {
|
|
356
|
+
console.error('[Claude stderr]', data.toString());
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
claude.on('close', (code) => {
|
|
360
|
+
// 发送结束事件
|
|
361
|
+
sendSSE(res, 'content_block_stop', {
|
|
362
|
+
type: 'content_block_stop',
|
|
363
|
+
index: 0
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
sendSSE(res, 'message_delta', {
|
|
367
|
+
type: 'message_delta',
|
|
368
|
+
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
|
369
|
+
usage: { output_tokens: 0 }
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
sendSSE(res, 'message_stop', { type: 'message_stop' });
|
|
373
|
+
|
|
374
|
+
res.end();
|
|
375
|
+
resolve();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
claude.on('error', (error) => {
|
|
379
|
+
console.error('[Spawn 错误]', error);
|
|
380
|
+
res.end();
|
|
381
|
+
reject(error);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 发送用户消息给 Claude
|
|
385
|
+
const lastUserMessage = messages.filter(m => m.role === 'user').pop();
|
|
386
|
+
if (lastUserMessage) {
|
|
387
|
+
const input = JSON.stringify({
|
|
388
|
+
type: 'user',
|
|
389
|
+
message: lastUserMessage
|
|
390
|
+
}) + '\n';
|
|
391
|
+
|
|
392
|
+
claude.stdin.write(input);
|
|
393
|
+
claude.stdin.end();
|
|
394
|
+
} else {
|
|
395
|
+
claude.stdin.end();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 把 messages 数组转成对话格式的 prompt
|
|
401
|
+
function buildPromptFromMessages(messages) {
|
|
402
|
+
if (!messages || messages.length === 0) {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 如果只有一条消息,直接返回内容
|
|
407
|
+
if (messages.length === 1) {
|
|
408
|
+
return getMessageContent(messages[0]);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 多条消息,构建对话上下文
|
|
412
|
+
let prompt = '以下是之前的对话历史:\n\n';
|
|
413
|
+
|
|
414
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
415
|
+
const msg = messages[i];
|
|
416
|
+
const role = msg.role === 'user' ? 'User' : 'Assistant';
|
|
417
|
+
prompt += `${role}: ${getMessageContent(msg)}\n\n`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 最后一条消息作为当前请求
|
|
421
|
+
const lastMessage = messages[messages.length - 1];
|
|
422
|
+
prompt += `\n请基于上述对话历史,回复以下消息:\n\n${getMessageContent(lastMessage)}`;
|
|
423
|
+
|
|
424
|
+
return prompt;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 获取消息内容(支持 string 和 array 格式)
|
|
428
|
+
function getMessageContent(message) {
|
|
429
|
+
if (typeof message.content === 'string') {
|
|
430
|
+
return message.content;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (Array.isArray(message.content)) {
|
|
434
|
+
return message.content
|
|
435
|
+
.filter(c => c.type === 'text')
|
|
436
|
+
.map(c => c.text)
|
|
437
|
+
.join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return '';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 发送 SSE 事件
|
|
444
|
+
function sendSSE(res, event, data) {
|
|
445
|
+
res.write(`event: ${event}\n`);
|
|
446
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 启动服务器
|
|
450
|
+
server.listen(PORT, () => {
|
|
451
|
+
console.log(`
|
|
452
|
+
╔════════════════════════════════════════════════╗
|
|
453
|
+
║ Claude Local Proxy 已启动 ║
|
|
454
|
+
╠════════════════════════════════════════════════╣
|
|
455
|
+
║ 监听地址: http://localhost:${PORT} ║
|
|
456
|
+
║ API 端点: http://localhost:${PORT}/v1/messages ║
|
|
457
|
+
╠════════════════════════════════════════════════╣
|
|
458
|
+
║ 配置说明: ║
|
|
459
|
+
║ • baseURL: "http://localhost:${PORT}" ║
|
|
460
|
+
║ • API Key: 任意字符串(不验证) ║
|
|
461
|
+
║ ║
|
|
462
|
+
║ 示例配置 (opencode/cursor): ║
|
|
463
|
+
║ { ║
|
|
464
|
+
║ "baseURL": "http://localhost:${PORT}", ║
|
|
465
|
+
║ "apiKey": "sk-any-key-works" ║
|
|
466
|
+
║ } ║
|
|
467
|
+
╚════════════════════════════════════════════════╝
|
|
468
|
+
`);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// 优雅退出
|
|
472
|
+
process.on('SIGINT', () => {
|
|
473
|
+
console.log('\n正在关闭服务...');
|
|
474
|
+
server.close(() => {
|
|
475
|
+
console.log('服务已关闭');
|
|
476
|
+
process.exit(0);
|
|
477
|
+
});
|
|
478
|
+
});
|
package/start.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Claude Local Proxy 启动脚本
|
|
4
|
+
|
|
5
|
+
cd "$(dirname "$0")"
|
|
6
|
+
|
|
7
|
+
# 检查 Claude Code 是否安装
|
|
8
|
+
# 直接测试 claude 命令是否可用(支持 npm 和 native 安装)
|
|
9
|
+
if ! claude --help &> /dev/null; then
|
|
10
|
+
echo "❌ 错误: Claude Code 未安装或不在 PATH 中"
|
|
11
|
+
echo ""
|
|
12
|
+
echo "安装方式:"
|
|
13
|
+
echo " 1. npm 安装: npm install -g @anthropic-ai/claude-code"
|
|
14
|
+
echo " 2. 或确保 native 安装的 claude 在 PATH 中"
|
|
15
|
+
echo ""
|
|
16
|
+
echo "提示: 如果已安装但找不到,请检查 PATH 环境变量"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# 显示找到的 claude 路径
|
|
21
|
+
CLAUDE_PATH=$(which claude 2>/dev/null || command -v claude 2>/dev/null || echo "claude")
|
|
22
|
+
echo "✓ 找到 Claude Code: $CLAUDE_PATH"
|
|
23
|
+
|
|
24
|
+
# 检查 Claude Code 环境变量
|
|
25
|
+
if [ -z "$ANTHROPIC_BASE_URL" ]; then
|
|
26
|
+
echo "⚠️ 提示: ANTHROPIC_BASE_URL 未设置"
|
|
27
|
+
echo "如需使用中转服务,请先设置环境变量:"
|
|
28
|
+
echo " export ANTHROPIC_BASE_URL=\"https://your-relay-service.com/v1\""
|
|
29
|
+
echo " export ANTHROPIC_API_KEY=\"your-api-key\""
|
|
30
|
+
echo ""
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo "🚀 启动 Claude Local Proxy..."
|
|
34
|
+
node server.js
|