@simonyea/holysheep-cli 1.6.15 → 1.7.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/package.json +2 -2
- package/src/commands/doctor.js +33 -0
- package/src/index.js +14 -0
- package/src/tools/claude-bridge.js +331 -0
- package/src/tools/claude-code.js +52 -4
- package/src/utils/config.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai-china",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"homepage": "https://holysheep.ai",
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|
|
45
|
-
"url": "git+https://
|
|
45
|
+
"url": "git+https://gitee.com/simonyea/holysheep-cli.git"
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"bin": {
|
package/src/commands/doctor.js
CHANGED
|
@@ -58,6 +58,9 @@ async function doctor() {
|
|
|
58
58
|
if (tool.id === 'openclaw' && installed) {
|
|
59
59
|
printOpenClawDetails(tool, installState, nodeMajor)
|
|
60
60
|
}
|
|
61
|
+
if (tool.id === 'claude-code' && installed && configured) {
|
|
62
|
+
printClaudeBridgeDetails(tool)
|
|
63
|
+
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
console.log()
|
|
@@ -156,6 +159,36 @@ function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
|
156
159
|
})
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
function printClaudeBridgeDetails(tool) {
|
|
163
|
+
const port = typeof tool.getBridgePort === 'function' ? tool.getBridgePort() : null
|
|
164
|
+
if (!port) return
|
|
165
|
+
const bridgeConfig = typeof tool.getBridgeConfig === 'function' ? tool.getBridgeConfig() : {}
|
|
166
|
+
|
|
167
|
+
let healthy = false
|
|
168
|
+
try {
|
|
169
|
+
execSync(
|
|
170
|
+
process.platform === 'win32'
|
|
171
|
+
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/health -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
172
|
+
: `curl -sf http://127.0.0.1:${port}/health -o /dev/null --max-time 1`,
|
|
173
|
+
{ stdio: 'ignore', timeout: 3000 }
|
|
174
|
+
)
|
|
175
|
+
healthy = true
|
|
176
|
+
} catch {}
|
|
177
|
+
|
|
178
|
+
const icon = healthy ? chalk.green('↳') : chalk.yellow('↳')
|
|
179
|
+
const text = healthy
|
|
180
|
+
? chalk.green(`Claude Bridge 已运行:127.0.0.1:${port}`)
|
|
181
|
+
: chalk.yellow(`Claude Bridge 未运行:127.0.0.1:${port}(可运行 hs claude-bridge)`)
|
|
182
|
+
console.log(` ${icon} ${text}`)
|
|
183
|
+
|
|
184
|
+
const relayUrl = bridgeConfig.relayUrl || bridgeConfig.controlPlaneUrl || ''
|
|
185
|
+
if (relayUrl) {
|
|
186
|
+
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude Relay: ${relayUrl}`)}`)
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` ${chalk.gray('↳')} ${chalk.gray('Claude Relay: 未配置,当前仍直连 Anthropic 兼容入口')}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
159
192
|
function maskKey(key) {
|
|
160
193
|
if (!key || key.length < 8) return '****'
|
|
161
194
|
return key.slice(0, 6) + '...' + key.slice(-4)
|
package/src/index.js
CHANGED
|
@@ -167,6 +167,20 @@ program
|
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
// ── claude-bridge ────────────────────────────────────────────────────────────
|
|
171
|
+
program
|
|
172
|
+
.command('claude-bridge')
|
|
173
|
+
.description('启动 HolySheep 的 Claude Code 本地桥接服务')
|
|
174
|
+
.option('--port <port>', '指定桥接服务端口')
|
|
175
|
+
.option('--host <host>', '指定桥接服务监听地址')
|
|
176
|
+
.action((opts) => {
|
|
177
|
+
const { startBridge } = require('./tools/claude-bridge')
|
|
178
|
+
startBridge({
|
|
179
|
+
port: opts.port ? Number(opts.port) : null,
|
|
180
|
+
host: opts.host || '127.0.0.1',
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
170
184
|
// 默认:无命令时显示帮助 + 提示 setup
|
|
171
185
|
program
|
|
172
186
|
.action(() => {
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const http = require('http')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
const crypto = require('crypto')
|
|
9
|
+
const { spawn, execSync } = require('child_process')
|
|
10
|
+
const fetch = global.fetch || require('node-fetch')
|
|
11
|
+
|
|
12
|
+
const HOLYSHEEP_DIR = path.join(os.homedir(), '.holysheep')
|
|
13
|
+
const CLAUDE_BRIDGE_CONFIG_FILE = path.join(HOLYSHEEP_DIR, 'claude-bridge.json')
|
|
14
|
+
const DEFAULT_BRIDGE_PORT = 14555
|
|
15
|
+
const isWin = process.platform === 'win32'
|
|
16
|
+
const leaseCache = new Map()
|
|
17
|
+
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
if (!fs.existsSync(HOLYSHEEP_DIR)) fs.mkdirSync(HOLYSHEEP_DIR, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = { port: null, host: '127.0.0.1', config: CLAUDE_BRIDGE_CONFIG_FILE }
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const value = argv[i]
|
|
26
|
+
if (value === '--port') args.port = Number(argv[++i])
|
|
27
|
+
else if (value === '--host') args.host = argv[++i]
|
|
28
|
+
else if (value === '--config') args.config = argv[++i]
|
|
29
|
+
}
|
|
30
|
+
return args
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBridgeConfig(configPath = CLAUDE_BRIDGE_CONFIG_FILE) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
36
|
+
} catch {
|
|
37
|
+
return {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeBridgeConfig(data, configPath = CLAUDE_BRIDGE_CONFIG_FILE) {
|
|
42
|
+
ensureDir()
|
|
43
|
+
fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf8')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getConfiguredBridgePort(config = readBridgeConfig()) {
|
|
47
|
+
const port = Number(config.port)
|
|
48
|
+
return Number.isInteger(port) && port > 0 ? port : DEFAULT_BRIDGE_PORT
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getBridgeBaseUrl(port = getConfiguredBridgePort()) {
|
|
52
|
+
return `http://127.0.0.1:${port}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sendJson(res, statusCode, payload) {
|
|
56
|
+
res.writeHead(statusCode, {
|
|
57
|
+
'content-type': 'application/json; charset=utf-8',
|
|
58
|
+
'cache-control': 'no-store',
|
|
59
|
+
})
|
|
60
|
+
res.end(JSON.stringify(payload))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readRawBody(req) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const chunks = []
|
|
66
|
+
let size = 0
|
|
67
|
+
req.on('data', (chunk) => {
|
|
68
|
+
size += chunk.length
|
|
69
|
+
if (size > 20 * 1024 * 1024) {
|
|
70
|
+
reject(new Error('Request body too large'))
|
|
71
|
+
req.destroy()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
chunks.push(chunk)
|
|
75
|
+
})
|
|
76
|
+
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
77
|
+
req.on('error', reject)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripBearer(value) {
|
|
82
|
+
const raw = String(value || '').trim()
|
|
83
|
+
if (!raw) return ''
|
|
84
|
+
if (/^Bearer\s+/i.test(raw)) return raw.replace(/^Bearer\s+/i, '').trim()
|
|
85
|
+
return raw
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildBodyHash(body) {
|
|
89
|
+
return crypto.createHash('sha256').update(body || Buffer.alloc(0)).digest('hex')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildBridgeSignature(req, config, body, timestamp) {
|
|
93
|
+
const secret = String(config.bridgeSecret || '').trim()
|
|
94
|
+
if (!secret) return ''
|
|
95
|
+
|
|
96
|
+
const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`)
|
|
97
|
+
const canonical = [
|
|
98
|
+
String(req.method || 'GET').toUpperCase(),
|
|
99
|
+
url.pathname + (url.search || ''),
|
|
100
|
+
String(timestamp),
|
|
101
|
+
buildBodyHash(body),
|
|
102
|
+
String(config.bridgeId || '')
|
|
103
|
+
].join('\n')
|
|
104
|
+
|
|
105
|
+
return crypto.createHmac('sha256', secret).update(canonical).digest('hex')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getBridgeUpstream(config) {
|
|
109
|
+
return String(config.relayUrl || config.controlPlaneUrl || config.baseUrlAnthropic || '').replace(/\/+$/, '')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseJsonBody(body) {
|
|
113
|
+
if (!body || !body.length) return null
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(body.toString('utf8'))
|
|
116
|
+
} catch {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractSessionId(parsedBody, config) {
|
|
122
|
+
const headerSessionId =
|
|
123
|
+
parsedBody?.metadata?.session_id ||
|
|
124
|
+
parsedBody?.session_id ||
|
|
125
|
+
parsedBody?.metadata?.conversation_id ||
|
|
126
|
+
null
|
|
127
|
+
if (headerSessionId) return String(headerSessionId)
|
|
128
|
+
|
|
129
|
+
const metadataUserId = parsedBody?.metadata?.user_id
|
|
130
|
+
if (typeof metadataUserId === 'string' && metadataUserId.trim()) {
|
|
131
|
+
return metadataUserId.trim()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `${config.bridgeId || 'local-bridge'}:default`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isRelayMode(config) {
|
|
138
|
+
const upstream = getBridgeUpstream(config)
|
|
139
|
+
return /\/claude-relay(?:\/|$)/.test(upstream)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function requestSessionLease(config, sessionId, parsedBody) {
|
|
143
|
+
const controlPlaneUrl = getBridgeUpstream(config)
|
|
144
|
+
const cached = leaseCache.get(sessionId)
|
|
145
|
+
if (cached && cached.expiresAt && new Date(cached.expiresAt).getTime() - Date.now() > 30_000) {
|
|
146
|
+
return cached
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const response = await fetch(`${controlPlaneUrl}/session/open`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
sessionId,
|
|
156
|
+
bridgeId: config.bridgeId || 'local-bridge',
|
|
157
|
+
deviceId: config.deviceId || '',
|
|
158
|
+
model: parsedBody?.model || null,
|
|
159
|
+
installSource: config.installSource || 'holysheep-cli',
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const payload = await response.json().catch(() => null)
|
|
164
|
+
if (!response.ok || !payload?.success || !payload?.data?.nodeBaseUrl || !payload?.data?.ticket) {
|
|
165
|
+
const message =
|
|
166
|
+
payload?.error?.message ||
|
|
167
|
+
payload?.message ||
|
|
168
|
+
`Failed to open Claude relay session (HTTP ${response.status})`
|
|
169
|
+
throw new Error(message)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
leaseCache.set(sessionId, payload.data)
|
|
173
|
+
return payload.data
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildUpstreamHeaders(req, config, body) {
|
|
177
|
+
const headers = {}
|
|
178
|
+
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
179
|
+
const normalized = key.toLowerCase()
|
|
180
|
+
if (normalized === 'host') continue
|
|
181
|
+
if (normalized === 'content-length') continue
|
|
182
|
+
headers[normalized] = value
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const token = stripBearer(headers.authorization) || headers['x-api-key'] || config.apiKey || ''
|
|
186
|
+
delete headers.authorization
|
|
187
|
+
if (token) headers['x-api-key'] = token
|
|
188
|
+
if (!headers['anthropic-version']) headers['anthropic-version'] = '2023-06-01'
|
|
189
|
+
headers['user-agent'] = 'holysheep-claude-bridge/1.0'
|
|
190
|
+
headers['x-hs-bridge-id'] = config.bridgeId || 'local-bridge'
|
|
191
|
+
headers['x-hs-install-source'] = config.installSource || 'holysheep-cli'
|
|
192
|
+
if (config.deviceId) headers['x-hs-device-id'] = config.deviceId
|
|
193
|
+
if (config.userToken) headers['x-hs-user-token'] = config.userToken
|
|
194
|
+
if (config.nodeId) headers['x-hs-node-id'] = config.nodeId
|
|
195
|
+
if (config.sessionMode) headers['x-hs-session-mode'] = config.sessionMode
|
|
196
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
197
|
+
headers['x-hs-bridge-timestamp'] = String(timestamp)
|
|
198
|
+
const signature = buildBridgeSignature(req, config, body, timestamp)
|
|
199
|
+
if (signature) headers['x-hs-bridge-signature'] = signature
|
|
200
|
+
if (body && body.length > 0) headers['content-length'] = String(body.length)
|
|
201
|
+
|
|
202
|
+
return headers
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createBridgeServer(configPath = CLAUDE_BRIDGE_CONFIG_FILE) {
|
|
206
|
+
return http.createServer(async (req, res) => {
|
|
207
|
+
if (req.method === 'OPTIONS') {
|
|
208
|
+
res.writeHead(204, {
|
|
209
|
+
'access-control-allow-origin': '*',
|
|
210
|
+
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
|
211
|
+
'access-control-allow-headers': 'content-type,authorization,x-api-key,anthropic-version',
|
|
212
|
+
})
|
|
213
|
+
return res.end()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const config = readBridgeConfig(configPath)
|
|
218
|
+
const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`)
|
|
219
|
+
const port = getConfiguredBridgePort(config)
|
|
220
|
+
|
|
221
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
222
|
+
return sendJson(res, 200, {
|
|
223
|
+
ok: true,
|
|
224
|
+
port,
|
|
225
|
+
upstream: getBridgeUpstream(config),
|
|
226
|
+
relayUrl: config.relayUrl || '',
|
|
227
|
+
mode: config.relayUrl ? 'relay' : 'direct',
|
|
228
|
+
bridgeId: config.bridgeId || null,
|
|
229
|
+
nodeId: config.nodeId || null,
|
|
230
|
+
installSource: config.installSource || 'holysheep-cli',
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let upstreamBase = getBridgeUpstream(config)
|
|
235
|
+
if (!upstreamBase) {
|
|
236
|
+
return sendJson(res, 500, { error: { message: 'Bridge upstream is not configured' } })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const body = await readRawBody(req)
|
|
240
|
+
const parsedBody = parseJsonBody(body)
|
|
241
|
+
if (isRelayMode(config) && req.method !== 'GET' && url.pathname !== '/health') {
|
|
242
|
+
const sessionId = extractSessionId(parsedBody, config)
|
|
243
|
+
const lease = await requestSessionLease(config, sessionId, parsedBody)
|
|
244
|
+
upstreamBase = String(lease.nodeBaseUrl || '').replace(/\/+$/, '')
|
|
245
|
+
req.headers['x-hs-session-id'] = sessionId
|
|
246
|
+
req.headers['x-hs-bridge-ticket'] = lease.ticket
|
|
247
|
+
req.headers['x-hs-node-id'] = lease.nodeId || ''
|
|
248
|
+
}
|
|
249
|
+
const upstreamUrl = `${upstreamBase}${url.pathname}${url.search || ''}`
|
|
250
|
+
const upstream = await fetch(upstreamUrl, {
|
|
251
|
+
method: req.method,
|
|
252
|
+
headers: buildUpstreamHeaders(req, config, body),
|
|
253
|
+
body: body.length > 0 ? body : undefined,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
res.writeHead(upstream.status, {
|
|
257
|
+
'content-type': upstream.headers.get('content-type') || 'application/json; charset=utf-8',
|
|
258
|
+
'cache-control': upstream.headers.get('cache-control') || 'no-store',
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (upstream.body && typeof upstream.body.pipe === 'function') {
|
|
262
|
+
upstream.body.pipe(res)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const text = await upstream.text()
|
|
267
|
+
res.end(text)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return sendJson(res, 500, { error: { message: error.message || 'Claude bridge error' } })
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function waitForBridge(port) {
|
|
275
|
+
for (let i = 0; i < 12; i++) {
|
|
276
|
+
const t0 = Date.now()
|
|
277
|
+
while (Date.now() - t0 < 500) {}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
execSync(
|
|
281
|
+
isWin
|
|
282
|
+
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/health -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
283
|
+
: `curl -sf http://127.0.0.1:${port}/health -o /dev/null --max-time 1`,
|
|
284
|
+
{ stdio: 'ignore', timeout: 3000 }
|
|
285
|
+
)
|
|
286
|
+
return true
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function startBridge(args = parseArgs(process.argv.slice(2))) {
|
|
294
|
+
const config = readBridgeConfig(args.config)
|
|
295
|
+
const port = args.port || getConfiguredBridgePort(config)
|
|
296
|
+
const host = args.host || '127.0.0.1'
|
|
297
|
+
const server = createBridgeServer(args.config)
|
|
298
|
+
|
|
299
|
+
server.listen(port, host, () => {
|
|
300
|
+
process.stdout.write(`HolySheep Claude bridge listening on http://${host}:${port}\n`)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return server
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function spawnBridge(port = getConfiguredBridgePort()) {
|
|
307
|
+
if (waitForBridge(port)) return true
|
|
308
|
+
|
|
309
|
+
const scriptPath = path.join(__dirname, '..', 'index.js')
|
|
310
|
+
const child = spawn(process.execPath, [scriptPath, 'claude-bridge', '--port', String(port)], {
|
|
311
|
+
detached: true,
|
|
312
|
+
stdio: 'ignore',
|
|
313
|
+
})
|
|
314
|
+
child.unref()
|
|
315
|
+
|
|
316
|
+
return waitForBridge(port)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
CLAUDE_BRIDGE_CONFIG_FILE,
|
|
321
|
+
DEFAULT_BRIDGE_PORT,
|
|
322
|
+
createBridgeServer,
|
|
323
|
+
getBridgeBaseUrl,
|
|
324
|
+
getConfiguredBridgePort,
|
|
325
|
+
parseArgs,
|
|
326
|
+
readBridgeConfig,
|
|
327
|
+
spawnBridge,
|
|
328
|
+
startBridge,
|
|
329
|
+
waitForBridge,
|
|
330
|
+
writeBridgeConfig,
|
|
331
|
+
}
|
package/src/tools/claude-code.js
CHANGED
|
@@ -11,6 +11,19 @@
|
|
|
11
11
|
const fs = require('fs')
|
|
12
12
|
const path = require('path')
|
|
13
13
|
const os = require('os')
|
|
14
|
+
const crypto = require('crypto')
|
|
15
|
+
const {
|
|
16
|
+
BASE_URL_ANTHROPIC,
|
|
17
|
+
BASE_URL_CLAUDE_RELAY,
|
|
18
|
+
} = require('../utils/config')
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_BRIDGE_PORT,
|
|
21
|
+
getBridgeBaseUrl,
|
|
22
|
+
getConfiguredBridgePort,
|
|
23
|
+
readBridgeConfig,
|
|
24
|
+
spawnBridge,
|
|
25
|
+
writeBridgeConfig,
|
|
26
|
+
} = require('./claude-bridge')
|
|
14
27
|
|
|
15
28
|
const SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json')
|
|
16
29
|
|
|
@@ -38,15 +51,43 @@ module.exports = {
|
|
|
38
51
|
},
|
|
39
52
|
isConfigured() {
|
|
40
53
|
const s = readSettings()
|
|
41
|
-
|
|
54
|
+
const bridge = readBridgeConfig()
|
|
55
|
+
const bridgePort = getConfiguredBridgePort(bridge)
|
|
56
|
+
return Boolean(
|
|
57
|
+
s.env?.ANTHROPIC_AUTH_TOKEN &&
|
|
58
|
+
s.env?.ANTHROPIC_BASE_URL === getBridgeBaseUrl(bridgePort) &&
|
|
59
|
+
bridge.apiKey &&
|
|
60
|
+
bridge.bridgeSecret
|
|
61
|
+
)
|
|
42
62
|
},
|
|
43
63
|
configure(apiKey, baseUrl) {
|
|
64
|
+
const bridge = readBridgeConfig()
|
|
65
|
+
const bridgePort = getConfiguredBridgePort(bridge)
|
|
66
|
+
const relayUrl = bridge.relayUrl || BASE_URL_CLAUDE_RELAY || ''
|
|
67
|
+
writeBridgeConfig({
|
|
68
|
+
port: bridgePort || DEFAULT_BRIDGE_PORT,
|
|
69
|
+
apiKey,
|
|
70
|
+
baseUrlAnthropic: baseUrl || BASE_URL_ANTHROPIC,
|
|
71
|
+
controlPlaneUrl: relayUrl || baseUrl || BASE_URL_ANTHROPIC,
|
|
72
|
+
relayUrl,
|
|
73
|
+
bridgeId: bridge.bridgeId || crypto.randomUUID(),
|
|
74
|
+
deviceId: bridge.deviceId || crypto.randomUUID(),
|
|
75
|
+
bridgeSecret: bridge.bridgeSecret || crypto.randomBytes(32).toString('hex'),
|
|
76
|
+
nodeId: bridge.nodeId || '',
|
|
77
|
+
sessionMode: bridge.sessionMode || 'pinned-node',
|
|
78
|
+
installSource: 'holysheep-cli',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!spawnBridge(bridgePort)) {
|
|
82
|
+
throw new Error('HolySheep Claude Bridge 启动失败')
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
const settings = readSettings()
|
|
45
86
|
if (!settings.env) settings.env = {}
|
|
46
|
-
// Claude Code 用 ANTHROPIC_AUTH_TOKEN(最高优先级),兼容 ANTHROPIC_API_KEY
|
|
47
87
|
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey
|
|
48
|
-
settings.env.ANTHROPIC_BASE_URL =
|
|
88
|
+
settings.env.ANTHROPIC_BASE_URL = getBridgeBaseUrl(bridgePort)
|
|
49
89
|
settings.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
|
|
90
|
+
settings.env.HOLYSHEEP_CLAUDE_BRIDGE = '1'
|
|
50
91
|
// 清理旧的同义字段
|
|
51
92
|
delete settings.env.ANTHROPIC_API_KEY
|
|
52
93
|
writeSettings(settings)
|
|
@@ -59,12 +100,19 @@ module.exports = {
|
|
|
59
100
|
delete settings.env.ANTHROPIC_API_KEY
|
|
60
101
|
delete settings.env.ANTHROPIC_BASE_URL
|
|
61
102
|
delete settings.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
|
|
103
|
+
delete settings.env.HOLYSHEEP_CLAUDE_BRIDGE
|
|
62
104
|
}
|
|
63
105
|
writeSettings(settings)
|
|
64
106
|
},
|
|
65
107
|
getConfigPath() { return SETTINGS_FILE },
|
|
66
|
-
hint: '
|
|
108
|
+
hint: '通过 HolySheep 本地 Bridge 接入,支持热切换,无需重启终端',
|
|
67
109
|
launchCmd: 'claude',
|
|
68
110
|
installCmd: 'npm install -g @anthropic-ai/claude-code',
|
|
69
111
|
docsUrl: 'https://docs.anthropic.com/claude-code',
|
|
112
|
+
getBridgePort() {
|
|
113
|
+
return getConfiguredBridgePort()
|
|
114
|
+
},
|
|
115
|
+
getBridgeConfig() {
|
|
116
|
+
return readBridgeConfig()
|
|
117
|
+
},
|
|
70
118
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
|
11
11
|
|
|
12
12
|
const BASE_URL_ANTHROPIC = 'https://api.holysheep.ai' // 不带 /v1 (Anthropic SDK)
|
|
13
13
|
const BASE_URL_OPENAI = 'https://api.holysheep.ai/v1' // 带 /v1 (OpenAI 兼容)
|
|
14
|
+
const BASE_URL_CLAUDE_RELAY = process.env.HOLYSHEEP_CLAUDE_RELAY_URL || 'https://api.holysheep.ai/claude-relay'
|
|
14
15
|
const SHOP_URL = 'https://holysheep.ai'
|
|
15
16
|
|
|
16
17
|
function ensureDir() {
|
|
@@ -45,6 +46,7 @@ module.exports = {
|
|
|
45
46
|
CONFIG_FILE,
|
|
46
47
|
BASE_URL_ANTHROPIC,
|
|
47
48
|
BASE_URL_OPENAI,
|
|
49
|
+
BASE_URL_CLAUDE_RELAY,
|
|
48
50
|
SHOP_URL,
|
|
49
51
|
loadConfig,
|
|
50
52
|
saveConfig,
|