open-claude-code-proxy 1.0.0 → 1.1.1
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/README.md +98 -0
- package/claude-proxy +20 -5
- package/cli.js +307 -0
- package/lib/config.js +122 -0
- package/lib/opencode-config.js +228 -0
- package/lib/port-utils.js +67 -0
- package/package.json +4 -3
- package/server.js +0 -0
package/README.md
CHANGED
|
@@ -80,6 +80,43 @@ cd open-claude-code-proxy
|
|
|
80
80
|
source ~/.zshrc
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### Port Configuration
|
|
84
|
+
|
|
85
|
+
The proxy supports flexible port configuration:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Use command line argument
|
|
89
|
+
claude-local-proxy --port 8080
|
|
90
|
+
claude-local-proxy -p 8080
|
|
91
|
+
|
|
92
|
+
# Or use environment variable
|
|
93
|
+
PORT=8080 claude-local-proxy
|
|
94
|
+
|
|
95
|
+
# Or let it use saved config (~/.claude-proxy/config.json)
|
|
96
|
+
claude-local-proxy
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Port Priority**: CLI argument > Config file > Environment variable > Default (12346)
|
|
100
|
+
|
|
101
|
+
On first run, you'll be prompted to customize the port. Your choice is saved for future use.
|
|
102
|
+
|
|
103
|
+
### OpenCode Auto-Configuration
|
|
104
|
+
|
|
105
|
+
The proxy can automatically configure [OpenCode](https://opencode.ai) for you:
|
|
106
|
+
|
|
107
|
+
- Detects OpenCode config at `~/.config/opencode/opencode.json`
|
|
108
|
+
- Updates `provider.anthropic.options.baseURL` to match your proxy port
|
|
109
|
+
- Creates automatic backups before modifying (keeps last 3)
|
|
110
|
+
- Prompts before making changes (use `--skip-opencode` to skip)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Auto-configure OpenCode
|
|
114
|
+
claude-local-proxy -p 8080
|
|
115
|
+
|
|
116
|
+
# Skip OpenCode configuration
|
|
117
|
+
claude-local-proxy -p 8080 --skip-opencode
|
|
118
|
+
```
|
|
119
|
+
|
|
83
120
|
### Configure Your Client
|
|
84
121
|
|
|
85
122
|
Point your app to the local proxy:
|
|
@@ -93,6 +130,18 @@ Point your app to the local proxy:
|
|
|
93
130
|
|
|
94
131
|
> **Note**: The API key can be any string - authentication is handled by your Claude Code session.
|
|
95
132
|
|
|
133
|
+
### CLI Options
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Usage: claude-local-proxy [options]
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
-p, --port <port> Server port (1024-65535)
|
|
140
|
+
--skip-opencode Skip OpenCode auto-configuration
|
|
141
|
+
-h, --help Show help
|
|
142
|
+
-v, --version Show version
|
|
143
|
+
```
|
|
144
|
+
|
|
96
145
|
### Commands
|
|
97
146
|
|
|
98
147
|
| Command | Description |
|
|
@@ -174,6 +223,43 @@ cd open-claude-code-proxy
|
|
|
174
223
|
source ~/.zshrc
|
|
175
224
|
```
|
|
176
225
|
|
|
226
|
+
### 端口配置
|
|
227
|
+
|
|
228
|
+
代理支持灵活的端口配置方式:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# 使用命令行参数
|
|
232
|
+
claude-local-proxy --port 8080
|
|
233
|
+
claude-local-proxy -p 8080
|
|
234
|
+
|
|
235
|
+
# 或使用环境变量
|
|
236
|
+
PORT=8080 claude-local-proxy
|
|
237
|
+
|
|
238
|
+
# 或使用已保存的配置 (~/.claude-proxy/config.json)
|
|
239
|
+
claude-local-proxy
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**端口优先级**:命令行参数 > 配置文件 > 环境变量 > 默认值 (12346)
|
|
243
|
+
|
|
244
|
+
首次运行时会提示你自定义端口,你的选择会被保存供后续使用。
|
|
245
|
+
|
|
246
|
+
### OpenCode 自动配置
|
|
247
|
+
|
|
248
|
+
代理可以自动为 [OpenCode](https://opencode.ai) 配置连接:
|
|
249
|
+
|
|
250
|
+
- 自动检测 `~/.config/opencode/opencode.json` 配置文件
|
|
251
|
+
- 更新 `provider.anthropic.options.baseURL` 为代理端口
|
|
252
|
+
- 修改前自动备份(保留最近 3 个备份)
|
|
253
|
+
- 修改前会询问确认(使用 `--skip-opencode` 跳过)
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# 自动配置 OpenCode
|
|
257
|
+
claude-local-proxy -p 8080
|
|
258
|
+
|
|
259
|
+
# 跳过 OpenCode 配置
|
|
260
|
+
claude-local-proxy -p 8080 --skip-opencode
|
|
261
|
+
```
|
|
262
|
+
|
|
177
263
|
### 配置客户端
|
|
178
264
|
|
|
179
265
|
将你的应用指向本地代理:
|
|
@@ -187,6 +273,18 @@ cd open-claude-code-proxy
|
|
|
187
273
|
|
|
188
274
|
> **注意**:API Key 可以是任意字符串,实际认证由 Claude Code 会话处理。
|
|
189
275
|
|
|
276
|
+
### CLI 选项
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
用法: claude-local-proxy [选项]
|
|
280
|
+
|
|
281
|
+
选项:
|
|
282
|
+
-p, --port <port> 服务器端口 (1024-65535)
|
|
283
|
+
--skip-opencode 跳过 OpenCode 自动配置
|
|
284
|
+
-h, --help 显示帮助
|
|
285
|
+
-v, --version 显示版本
|
|
286
|
+
```
|
|
287
|
+
|
|
190
288
|
### 命令说明
|
|
191
289
|
|
|
192
290
|
| 命令 | 说明 |
|
package/claude-proxy
CHANGED
|
@@ -6,6 +6,21 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
6
6
|
SERVER_JS="$SCRIPT_DIR/server.js"
|
|
7
7
|
PID_FILE="$SCRIPT_DIR/.claude-proxy.pid"
|
|
8
8
|
LOG_FILE="$SCRIPT_DIR/proxy.log"
|
|
9
|
+
CONFIG_FILE="$HOME/.claude-proxy/config.json"
|
|
10
|
+
|
|
11
|
+
# 从配置文件读取端口
|
|
12
|
+
get_port() {
|
|
13
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
14
|
+
PORT=$(cat "$CONFIG_FILE" | grep -o '"port":[[:space:]]*[0-9]*' | grep -o '[0-9]*')
|
|
15
|
+
if [ -n "$PORT" ]; then
|
|
16
|
+
echo "$PORT"
|
|
17
|
+
return
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
echo "${PORT:-12346}"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
PROXY_PORT=$(get_port)
|
|
9
24
|
|
|
10
25
|
# 颜色定义
|
|
11
26
|
RED='\033[0;31m'
|
|
@@ -115,8 +130,8 @@ start() {
|
|
|
115
130
|
return 1
|
|
116
131
|
fi
|
|
117
132
|
|
|
118
|
-
#
|
|
119
|
-
nohup node "$SERVER_JS" > "$LOG_FILE" 2>&1 &
|
|
133
|
+
# 后台启动(使用配置的端口)
|
|
134
|
+
PORT=$PROXY_PORT nohup node "$SERVER_JS" > "$LOG_FILE" 2>&1 &
|
|
120
135
|
PID=$!
|
|
121
136
|
echo $PID > "$PID_FILE"
|
|
122
137
|
|
|
@@ -125,7 +140,7 @@ start() {
|
|
|
125
140
|
|
|
126
141
|
if is_running; then
|
|
127
142
|
success "服务已启动 (PID: $PID)"
|
|
128
|
-
success "监听地址: http://localhost
|
|
143
|
+
success "监听地址: http://localhost:$PROXY_PORT"
|
|
129
144
|
echo ""
|
|
130
145
|
info "查看日志: $0 logs"
|
|
131
146
|
info "查看状态: $0 status"
|
|
@@ -184,7 +199,7 @@ status() {
|
|
|
184
199
|
success "服务运行中"
|
|
185
200
|
echo ""
|
|
186
201
|
echo " PID: $PID"
|
|
187
|
-
echo " 地址: http://localhost
|
|
202
|
+
echo " 地址: http://localhost:$PROXY_PORT"
|
|
188
203
|
echo " 日志: $LOG_FILE"
|
|
189
204
|
echo ""
|
|
190
205
|
|
|
@@ -240,7 +255,7 @@ Claude Local Proxy 控制脚本
|
|
|
240
255
|
配置文件:
|
|
241
256
|
客户端配置 (opencode/cursor):
|
|
242
257
|
{
|
|
243
|
-
"baseURL": "http://localhost
|
|
258
|
+
"baseURL": "http://localhost:$PROXY_PORT",
|
|
244
259
|
"apiKey": "sk-any-key-works"
|
|
245
260
|
}
|
|
246
261
|
|
package/cli.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Local Proxy CLI
|
|
5
|
+
* 交互式命令行入口
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const config = require('./lib/config');
|
|
10
|
+
const portUtils = require('./lib/port-utils');
|
|
11
|
+
const opencodeConfig = require('./lib/opencode-config');
|
|
12
|
+
|
|
13
|
+
const VERSION = '1.0.0';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 解析命令行参数
|
|
17
|
+
* @returns {{port: number|null, help: boolean, version: boolean, skipOpencode: boolean}}
|
|
18
|
+
*/
|
|
19
|
+
function parseArgs() {
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const result = {
|
|
22
|
+
port: null,
|
|
23
|
+
help: false,
|
|
24
|
+
version: false,
|
|
25
|
+
skipOpencode: false
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
|
|
31
|
+
if (arg === '--help' || arg === '-h') {
|
|
32
|
+
result.help = true;
|
|
33
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
34
|
+
result.version = true;
|
|
35
|
+
} else if (arg === '--skip-opencode') {
|
|
36
|
+
result.skipOpencode = true;
|
|
37
|
+
} else if (arg === '--port' || arg === '-p') {
|
|
38
|
+
const portValue = args[++i];
|
|
39
|
+
if (portValue) {
|
|
40
|
+
result.port = parseInt(portValue, 10);
|
|
41
|
+
}
|
|
42
|
+
} else if (arg.startsWith('--port=')) {
|
|
43
|
+
result.port = parseInt(arg.split('=')[1], 10);
|
|
44
|
+
} else if (arg.startsWith('-p=')) {
|
|
45
|
+
result.port = parseInt(arg.split('=')[1], 10);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 显示帮助信息
|
|
54
|
+
*/
|
|
55
|
+
function showHelp() {
|
|
56
|
+
console.log(`
|
|
57
|
+
Claude Local Proxy - 本地代理服务器
|
|
58
|
+
|
|
59
|
+
用法:
|
|
60
|
+
claude-local-proxy [选项]
|
|
61
|
+
|
|
62
|
+
选项:
|
|
63
|
+
-p, --port <port> 指定服务器监听端口 (1024-65535)
|
|
64
|
+
--skip-opencode 跳过 OpenCode 配置自动更新
|
|
65
|
+
-h, --help 显示帮助信息
|
|
66
|
+
-v, --version 显示版本号
|
|
67
|
+
|
|
68
|
+
端口优先级:
|
|
69
|
+
1. 命令行参数 (--port)
|
|
70
|
+
2. 配置文件 (~/.claude-proxy/config.json)
|
|
71
|
+
3. 环境变量 (PORT)
|
|
72
|
+
4. 默认值 (12346)
|
|
73
|
+
|
|
74
|
+
配置文件位置:
|
|
75
|
+
${config.CONFIG_FILE}
|
|
76
|
+
|
|
77
|
+
示例:
|
|
78
|
+
claude-local-proxy # 使用默认端口或已保存的配置
|
|
79
|
+
claude-local-proxy -p 8080 # 使用端口 8080
|
|
80
|
+
claude-local-proxy --port=9000 # 使用端口 9000
|
|
81
|
+
claude-local-proxy --skip-opencode # 跳过 OpenCode 配置更新
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 创建 readline 接口
|
|
87
|
+
*/
|
|
88
|
+
function createReadlineInterface() {
|
|
89
|
+
return readline.createInterface({
|
|
90
|
+
input: process.stdin,
|
|
91
|
+
output: process.stdout
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 异步询问用户输入
|
|
97
|
+
* @param {readline.Interface} rl readline 接口
|
|
98
|
+
* @param {string} question 问题
|
|
99
|
+
* @returns {Promise<string>}
|
|
100
|
+
*/
|
|
101
|
+
function ask(rl, question) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
rl.question(question, (answer) => {
|
|
104
|
+
resolve(answer.trim());
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 交互式获取端口
|
|
111
|
+
* @param {readline.Interface} rl readline 接口
|
|
112
|
+
* @param {number} defaultPort 默认端口
|
|
113
|
+
* @returns {Promise<number>}
|
|
114
|
+
*/
|
|
115
|
+
async function promptForPort(rl, defaultPort) {
|
|
116
|
+
let attempts = 0;
|
|
117
|
+
const maxAttempts = 3;
|
|
118
|
+
|
|
119
|
+
while (attempts < maxAttempts) {
|
|
120
|
+
const answer = await ask(rl, `请输入端口号 (默认: ${defaultPort}): `);
|
|
121
|
+
|
|
122
|
+
if (!answer) {
|
|
123
|
+
return defaultPort;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const port = parseInt(answer, 10);
|
|
127
|
+
const validation = config.validatePort(port);
|
|
128
|
+
|
|
129
|
+
if (validation.valid) {
|
|
130
|
+
const available = await portUtils.isPortAvailable(port);
|
|
131
|
+
if (available) {
|
|
132
|
+
return port;
|
|
133
|
+
} else {
|
|
134
|
+
console.log(`\n${portUtils.getPortOccupiedMessage(port)}`);
|
|
135
|
+
attempts++;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log(`\n错误: ${validation.error}`);
|
|
139
|
+
attempts++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (attempts < maxAttempts) {
|
|
143
|
+
console.log(`请重新输入 (剩余尝试次数: ${maxAttempts - attempts})\n`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n已达到最大尝试次数,将使用默认端口 ${defaultPort}`);
|
|
148
|
+
return defaultPort;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 询问是否更新 OpenCode 配置
|
|
153
|
+
* @param {readline.Interface} rl readline 接口
|
|
154
|
+
* @returns {Promise<boolean>}
|
|
155
|
+
*/
|
|
156
|
+
async function askUpdateOpenCode(rl) {
|
|
157
|
+
const answer = await ask(rl, '是否自动更新 OpenCode 配置? (Y/n): ');
|
|
158
|
+
return answer.toLowerCase() !== 'n';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 询问备份失败时是否继续
|
|
163
|
+
* @param {readline.Interface} rl readline 接口
|
|
164
|
+
* @returns {Promise<boolean>}
|
|
165
|
+
*/
|
|
166
|
+
async function askContinueWithoutBackup(rl) {
|
|
167
|
+
const answer = await ask(rl, '备份失败,是否仍然继续更新配置? (y/N): ');
|
|
168
|
+
return answer.toLowerCase() === 'y';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 主函数
|
|
173
|
+
*/
|
|
174
|
+
async function main() {
|
|
175
|
+
const args = parseArgs();
|
|
176
|
+
|
|
177
|
+
// 显示版本
|
|
178
|
+
if (args.version) {
|
|
179
|
+
console.log(`claude-local-proxy v${VERSION}`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 显示帮助
|
|
184
|
+
if (args.help) {
|
|
185
|
+
showHelp();
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 验证 CLI 端口参数
|
|
190
|
+
if (args.port !== null) {
|
|
191
|
+
const validation = config.validatePort(args.port);
|
|
192
|
+
if (!validation.valid) {
|
|
193
|
+
console.error(`错误: ${validation.error}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let finalPort;
|
|
199
|
+
let rl = null;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// 确定端口
|
|
203
|
+
if (args.port !== null) {
|
|
204
|
+
// CLI 参数指定了端口
|
|
205
|
+
finalPort = args.port;
|
|
206
|
+
} else {
|
|
207
|
+
// 获取配置的端口
|
|
208
|
+
finalPort = config.getPort();
|
|
209
|
+
|
|
210
|
+
// 检查端口是否可用
|
|
211
|
+
const isAvailable = await portUtils.isPortAvailable(finalPort);
|
|
212
|
+
|
|
213
|
+
if (!isAvailable) {
|
|
214
|
+
console.log(`\n默认端口 ${finalPort} 已被占用。`);
|
|
215
|
+
|
|
216
|
+
rl = createReadlineInterface();
|
|
217
|
+
|
|
218
|
+
// 交互式获取新端口
|
|
219
|
+
finalPort = await promptForPort(rl, config.DEFAULT_PORT);
|
|
220
|
+
} else if (config.isFirstRun()) {
|
|
221
|
+
// 首次运行,询问是否自定义端口
|
|
222
|
+
console.log('\n欢迎使用 Claude Local Proxy!');
|
|
223
|
+
console.log('这是首次运行,您可以自定义服务器端口。\n');
|
|
224
|
+
|
|
225
|
+
rl = createReadlineInterface();
|
|
226
|
+
|
|
227
|
+
const answer = await ask(rl, `使用默认端口 ${config.DEFAULT_PORT}? (Y/n): `);
|
|
228
|
+
|
|
229
|
+
if (answer.toLowerCase() === 'n') {
|
|
230
|
+
finalPort = await promptForPort(rl, config.DEFAULT_PORT);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 再次检查最终端口是否可用
|
|
236
|
+
const finalAvailable = await portUtils.isPortAvailable(finalPort);
|
|
237
|
+
if (!finalAvailable) {
|
|
238
|
+
console.error(`\n错误: 端口 ${finalPort} 不可用。`);
|
|
239
|
+
console.log(portUtils.getPortOccupiedMessage(finalPort));
|
|
240
|
+
if (rl) rl.close();
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 保存端口配置
|
|
245
|
+
config.savePort(finalPort);
|
|
246
|
+
console.log(`\n端口配置已保存: ${finalPort}`);
|
|
247
|
+
|
|
248
|
+
// OpenCode 配置更新
|
|
249
|
+
if (!args.skipOpencode) {
|
|
250
|
+
const opencodeDetect = opencodeConfig.detectOpenCodeConfig();
|
|
251
|
+
|
|
252
|
+
if (opencodeDetect.exists) {
|
|
253
|
+
let shouldUpdate = true;
|
|
254
|
+
|
|
255
|
+
// 如果有交互能力,询问用户
|
|
256
|
+
if (!rl && process.stdin.isTTY) {
|
|
257
|
+
rl = createReadlineInterface();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (rl) {
|
|
261
|
+
shouldUpdate = await askUpdateOpenCode(rl);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (shouldUpdate) {
|
|
265
|
+
const updateResult = opencodeConfig.updateOpenCodeBaseURL(finalPort);
|
|
266
|
+
|
|
267
|
+
if (updateResult.success) {
|
|
268
|
+
console.log(`\n${updateResult.message}`);
|
|
269
|
+
if (updateResult.backupPath) {
|
|
270
|
+
console.log(`备份文件: ${updateResult.backupPath}`);
|
|
271
|
+
}
|
|
272
|
+
console.log('\n请重启 OpenCode 以使配置生效。');
|
|
273
|
+
} else {
|
|
274
|
+
console.log(`\n警告: ${updateResult.message}`);
|
|
275
|
+
console.log(opencodeConfig.getManualConfigGuide(finalPort));
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
console.log('\n跳过 OpenCode 配置更新。');
|
|
279
|
+
console.log(opencodeConfig.getManualConfigGuide(finalPort));
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
console.log('\n未检测到 OpenCode 配置文件。');
|
|
283
|
+
console.log(opencodeConfig.getManualConfigGuide(finalPort));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (rl) {
|
|
288
|
+
rl.close();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 设置环境变量并启动服务器
|
|
292
|
+
process.env.PORT = finalPort.toString();
|
|
293
|
+
|
|
294
|
+
console.log('\n正在启动代理服务器...\n');
|
|
295
|
+
|
|
296
|
+
// 加载并运行服务器
|
|
297
|
+
require('./server');
|
|
298
|
+
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('启动失败:', error.message);
|
|
301
|
+
if (rl) rl.close();
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 运行
|
|
307
|
+
main();
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置文件管理模块
|
|
3
|
+
* 负责读写 ~/.claude-proxy/config.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.claude-proxy');
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
12
|
+
const DEFAULT_PORT = 12346;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 确保配置目录存在
|
|
16
|
+
*/
|
|
17
|
+
function ensureConfigDir() {
|
|
18
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
19
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 读取配置文件
|
|
25
|
+
* @returns {Object} 配置对象
|
|
26
|
+
*/
|
|
27
|
+
function readConfig() {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
30
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
31
|
+
return JSON.parse(content);
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn(`警告: 配置文件读取失败 - ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 写入配置文件
|
|
41
|
+
* @param {Object} config 配置对象
|
|
42
|
+
*/
|
|
43
|
+
function writeConfig(config) {
|
|
44
|
+
ensureConfigDir();
|
|
45
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取端口配置
|
|
50
|
+
* 优先级: CLI 参数 > 配置文件 > 环境变量 > 默认值
|
|
51
|
+
* @param {number|null} cliPort CLI 参数指定的端口
|
|
52
|
+
* @returns {number} 端口号
|
|
53
|
+
*/
|
|
54
|
+
function getPort(cliPort = null) {
|
|
55
|
+
// 1. CLI 参数优先级最高
|
|
56
|
+
if (cliPort !== null && cliPort !== undefined) {
|
|
57
|
+
return cliPort;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. 配置文件
|
|
61
|
+
const config = readConfig();
|
|
62
|
+
if (config.port) {
|
|
63
|
+
return config.port;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. 环境变量
|
|
67
|
+
if (process.env.PORT) {
|
|
68
|
+
const envPort = parseInt(process.env.PORT, 10);
|
|
69
|
+
if (!isNaN(envPort)) {
|
|
70
|
+
return envPort;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. 默认值
|
|
75
|
+
return DEFAULT_PORT;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 保存端口配置
|
|
80
|
+
* @param {number} port 端口号
|
|
81
|
+
*/
|
|
82
|
+
function savePort(port) {
|
|
83
|
+
const config = readConfig();
|
|
84
|
+
config.port = port;
|
|
85
|
+
writeConfig(config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 检查是否首次运行(配置文件不存在)
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function isFirstRun() {
|
|
93
|
+
return !fs.existsSync(CONFIG_FILE);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 验证端口号是否有效
|
|
98
|
+
* @param {number} port 端口号
|
|
99
|
+
* @returns {{valid: boolean, error?: string}}
|
|
100
|
+
*/
|
|
101
|
+
function validatePort(port) {
|
|
102
|
+
const num = parseInt(port, 10);
|
|
103
|
+
if (isNaN(num)) {
|
|
104
|
+
return { valid: false, error: '端口必须是数字' };
|
|
105
|
+
}
|
|
106
|
+
if (num < 1024 || num > 65535) {
|
|
107
|
+
return { valid: false, error: '端口范围必须在 1024-65535 之间' };
|
|
108
|
+
}
|
|
109
|
+
return { valid: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
CONFIG_DIR,
|
|
114
|
+
CONFIG_FILE,
|
|
115
|
+
DEFAULT_PORT,
|
|
116
|
+
readConfig,
|
|
117
|
+
writeConfig,
|
|
118
|
+
getPort,
|
|
119
|
+
savePort,
|
|
120
|
+
isFirstRun,
|
|
121
|
+
validatePort
|
|
122
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 配置管理模块
|
|
3
|
+
* 负责检测、备份和更新 OpenCode 配置文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 获取 OpenCode 配置文件路径(跨平台)
|
|
12
|
+
* @returns {string} 配置文件路径
|
|
13
|
+
*/
|
|
14
|
+
function getOpenCodeConfigPath() {
|
|
15
|
+
const homedir = os.homedir();
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
return path.join(homedir, '.config', 'opencode', 'opencode.json');
|
|
18
|
+
}
|
|
19
|
+
// macOS / Linux
|
|
20
|
+
return path.join(homedir, '.config', 'opencode', 'opencode.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 检测 OpenCode 配置文件是否存在
|
|
25
|
+
* @returns {{exists: boolean, path: string}}
|
|
26
|
+
*/
|
|
27
|
+
function detectOpenCodeConfig() {
|
|
28
|
+
const configPath = getOpenCodeConfigPath();
|
|
29
|
+
return {
|
|
30
|
+
exists: fs.existsSync(configPath),
|
|
31
|
+
path: configPath
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 读取 OpenCode 配置
|
|
37
|
+
* @returns {{success: boolean, config?: Object, error?: string, path: string}}
|
|
38
|
+
*/
|
|
39
|
+
function readOpenCodeConfig() {
|
|
40
|
+
const configPath = getOpenCodeConfigPath();
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(configPath)) {
|
|
43
|
+
return { success: false, error: '配置文件不存在', path: configPath };
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
46
|
+
const config = JSON.parse(content);
|
|
47
|
+
return { success: true, config, path: configPath };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { success: false, error: error.message, path: configPath };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 创建备份文件
|
|
55
|
+
* @param {string} configPath 配置文件路径
|
|
56
|
+
* @returns {{success: boolean, backupPath?: string, error?: string}}
|
|
57
|
+
*/
|
|
58
|
+
function createBackup(configPath) {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(configPath)) {
|
|
61
|
+
return { success: false, error: '配置文件不存在' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dir = path.dirname(configPath);
|
|
65
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
66
|
+
const backupPath = path.join(dir, `opencode.json.backup.${timestamp}`);
|
|
67
|
+
|
|
68
|
+
// 复制文件
|
|
69
|
+
fs.copyFileSync(configPath, backupPath);
|
|
70
|
+
|
|
71
|
+
// 清理旧备份,只保留最近 3 个
|
|
72
|
+
cleanupOldBackups(dir, 3);
|
|
73
|
+
|
|
74
|
+
return { success: true, backupPath };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { success: false, error: error.message };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 清理旧备份文件
|
|
82
|
+
* @param {string} dir 目录路径
|
|
83
|
+
* @param {number} keepCount 保留数量
|
|
84
|
+
*/
|
|
85
|
+
function cleanupOldBackups(dir, keepCount) {
|
|
86
|
+
try {
|
|
87
|
+
const files = fs.readdirSync(dir)
|
|
88
|
+
.filter(f => f.startsWith('opencode.json.backup.'))
|
|
89
|
+
.map(f => ({
|
|
90
|
+
name: f,
|
|
91
|
+
path: path.join(dir, f),
|
|
92
|
+
mtime: fs.statSync(path.join(dir, f)).mtime
|
|
93
|
+
}))
|
|
94
|
+
.sort((a, b) => b.mtime - a.mtime); // 按修改时间降序
|
|
95
|
+
|
|
96
|
+
// 删除多余的备份
|
|
97
|
+
for (let i = keepCount; i < files.length; i++) {
|
|
98
|
+
fs.unlinkSync(files[i].path);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// 清理失败不影响主流程
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 更新 OpenCode 配置中的 baseURL
|
|
107
|
+
* @param {number} port 端口号
|
|
108
|
+
* @param {boolean} createBackupFirst 是否先创建备份
|
|
109
|
+
* @returns {{success: boolean, message: string, backupPath?: string, configPath?: string}}
|
|
110
|
+
*/
|
|
111
|
+
function updateOpenCodeBaseURL(port, createBackupFirst = true) {
|
|
112
|
+
const configPath = getOpenCodeConfigPath();
|
|
113
|
+
const newBaseURL = `http://localhost:${port}`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// 检查文件是否存在
|
|
117
|
+
if (!fs.existsSync(configPath)) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
message: 'OpenCode 配置文件不存在',
|
|
121
|
+
configPath
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 读取现有配置
|
|
126
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
127
|
+
let config;
|
|
128
|
+
try {
|
|
129
|
+
config = JSON.parse(content);
|
|
130
|
+
} catch {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
message: 'OpenCode 配置文件 JSON 格式无效',
|
|
134
|
+
configPath
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 检查当前 baseURL
|
|
139
|
+
const currentBaseURL = config?.provider?.anthropic?.options?.baseURL;
|
|
140
|
+
if (currentBaseURL === newBaseURL) {
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
message: `OpenCode 配置已是最新 (baseURL: ${newBaseURL})`,
|
|
144
|
+
configPath
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 创建备份
|
|
149
|
+
let backupPath = null;
|
|
150
|
+
if (createBackupFirst) {
|
|
151
|
+
const backupResult = createBackup(configPath);
|
|
152
|
+
if (!backupResult.success) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
message: `备份失败: ${backupResult.error}`,
|
|
156
|
+
configPath
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
backupPath = backupResult.backupPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 确保嵌套结构存在
|
|
163
|
+
if (!config.provider) config.provider = {};
|
|
164
|
+
if (!config.provider.anthropic) config.provider.anthropic = {};
|
|
165
|
+
if (!config.provider.anthropic.options) config.provider.anthropic.options = {};
|
|
166
|
+
|
|
167
|
+
// 更新 baseURL
|
|
168
|
+
config.provider.anthropic.options.baseURL = newBaseURL;
|
|
169
|
+
|
|
170
|
+
// 写回配置文件
|
|
171
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
message: `OpenCode 配置已更新 (baseURL: ${newBaseURL})`,
|
|
176
|
+
backupPath,
|
|
177
|
+
configPath
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code === 'EACCES') {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
message: '权限不足,无法写入 OpenCode 配置文件',
|
|
184
|
+
configPath
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
message: `更新失败: ${error.message}`,
|
|
190
|
+
configPath
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 获取手动配置指南
|
|
197
|
+
* @param {number} port 端口号
|
|
198
|
+
* @returns {string} 配置指南文本
|
|
199
|
+
*/
|
|
200
|
+
function getManualConfigGuide(port) {
|
|
201
|
+
const configPath = getOpenCodeConfigPath();
|
|
202
|
+
return `
|
|
203
|
+
手动配置 OpenCode:
|
|
204
|
+
1. 编辑配置文件: ${configPath}
|
|
205
|
+
2. 添加或修改以下配置:
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
"provider": {
|
|
209
|
+
"anthropic": {
|
|
210
|
+
"options": {
|
|
211
|
+
"baseURL": "http://localhost:${port}"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
3. 重启 OpenCode 使配置生效
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
getOpenCodeConfigPath,
|
|
223
|
+
detectOpenCodeConfig,
|
|
224
|
+
readOpenCodeConfig,
|
|
225
|
+
createBackup,
|
|
226
|
+
updateOpenCodeBaseURL,
|
|
227
|
+
getManualConfigGuide
|
|
228
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端口检测工具模块
|
|
3
|
+
* 负责检测端口可用性
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const net = require('net');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 检测端口是否可用
|
|
10
|
+
* @param {number} port 端口号
|
|
11
|
+
* @returns {Promise<boolean>} 端口是否可用
|
|
12
|
+
*/
|
|
13
|
+
function isPortAvailable(port) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const server = net.createServer();
|
|
16
|
+
|
|
17
|
+
server.once('error', (err) => {
|
|
18
|
+
if (err.code === 'EADDRINUSE') {
|
|
19
|
+
resolve(false);
|
|
20
|
+
} else {
|
|
21
|
+
resolve(false);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
server.once('listening', () => {
|
|
26
|
+
server.close();
|
|
27
|
+
resolve(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 不指定地址,检测所有接口
|
|
31
|
+
server.listen(port);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查找可用端口(从指定端口开始递增查找)
|
|
37
|
+
* @param {number} startPort 起始端口
|
|
38
|
+
* @param {number} maxAttempts 最大尝试次数
|
|
39
|
+
* @returns {Promise<number|null>} 可用端口或 null
|
|
40
|
+
*/
|
|
41
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
42
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
43
|
+
const port = startPort + i;
|
|
44
|
+
if (port > 65535) break;
|
|
45
|
+
|
|
46
|
+
const available = await isPortAvailable(port);
|
|
47
|
+
if (available) {
|
|
48
|
+
return port;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取占用指定端口的进程信息(仅用于提示)
|
|
56
|
+
* @param {number} port 端口号
|
|
57
|
+
* @returns {string} 提示信息
|
|
58
|
+
*/
|
|
59
|
+
function getPortOccupiedMessage(port) {
|
|
60
|
+
return `端口 ${port} 已被占用。请使用 \`lsof -i :${port}\` 或 \`netstat -an | grep ${port}\` 查看占用进程。`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
isPortAvailable,
|
|
65
|
+
findAvailablePort,
|
|
66
|
+
getPortOccupiedMessage
|
|
67
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-claude-code-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Local proxy that forwards API requests through the official Claude Code CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"open-claude-code-proxy": "
|
|
8
|
-
"claude-local-proxy": "
|
|
7
|
+
"open-claude-code-proxy": "cli.js",
|
|
8
|
+
"claude-local-proxy": "cli.js",
|
|
9
|
+
"claude-proxy": "claude-proxy"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"start": "node server.js",
|
package/server.js
CHANGED
|
File without changes
|