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 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
+ [![npm version](https://img.shields.io/npm/v/open-claude-code-proxy.svg?style=flat-square)](https://www.npmjs.com/package/open-claude-code-proxy)
10
+ [![npm downloads](https://img.shields.io/npm/dm/open-claude-code-proxy.svg?style=flat-square)](https://www.npmjs.com/package/open-claude-code-proxy)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
12
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?style=flat-square)](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