@simonyea/holysheep-cli 1.7.105 → 1.7.107

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.105",
3
+ "version": "1.7.107",
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",
@@ -0,0 +1,247 @@
1
+ /**
2
+ * hs claude-proxy — 独立后台代理,让 VS Code Claude 扩展也能用 HolySheep
3
+ *
4
+ * 用法:
5
+ * hs claude-proxy 前台启动代理
6
+ * hs claude-proxy --daemon 后台启动
7
+ * hs claude-proxy --stop 停止后台代理
8
+ * hs claude-proxy --status 查看代理状态
9
+ */
10
+ 'use strict'
11
+
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+ const os = require('os')
15
+ const { spawn, execSync } = require('child_process')
16
+ const chalk = require('chalk')
17
+
18
+ const {
19
+ startProcessProxy,
20
+ closeSession,
21
+ getProcessProxyPort,
22
+ getLocalProxyUrl,
23
+ readConfig,
24
+ } = require('../tools/claude-process-proxy')
25
+
26
+ const claudeCodeTool = require('../tools/claude-code')
27
+ const { getApiKey } = require('../utils/config')
28
+
29
+ const PID_FILE = path.join(os.homedir(), '.holysheep', 'claude-proxy.pid')
30
+ const isWin = process.platform === 'win32'
31
+
32
+ function readPid() {
33
+ try {
34
+ const content = fs.readFileSync(PID_FILE, 'utf8').trim()
35
+ return JSON.parse(content)
36
+ } catch {
37
+ return null
38
+ }
39
+ }
40
+
41
+ function writePid(pid, port, sessionId) {
42
+ const dir = path.dirname(PID_FILE)
43
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
44
+ fs.writeFileSync(PID_FILE, JSON.stringify({ pid, port, sessionId, startedAt: new Date().toISOString() }), 'utf8')
45
+ }
46
+
47
+ function clearPid() {
48
+ try { fs.unlinkSync(PID_FILE) } catch {}
49
+ }
50
+
51
+ function isProcessAlive(pid) {
52
+ try {
53
+ process.kill(pid, 0)
54
+ return true
55
+ } catch {
56
+ return false
57
+ }
58
+ }
59
+
60
+ function isProxyHealthy(port) {
61
+ try {
62
+ execSync(
63
+ isWin
64
+ ? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
65
+ : `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
66
+ { stdio: 'ignore', timeout: 3000, windowsHide: true }
67
+ )
68
+ return true
69
+ } catch {
70
+ return false
71
+ }
72
+ }
73
+
74
+ function writeBaseUrlToSettings(port) {
75
+ const settings = claudeCodeTool.readSettings()
76
+ if (!settings.env) settings.env = {}
77
+ settings.env.ANTHROPIC_BASE_URL = getLocalProxyUrl(port)
78
+ claudeCodeTool.writeSettings(settings)
79
+ }
80
+
81
+ function clearBaseUrlFromSettings() {
82
+ const settings = claudeCodeTool.readSettings()
83
+ if (settings.env?.ANTHROPIC_BASE_URL?.includes('127.0.0.1')) {
84
+ delete settings.env.ANTHROPIC_BASE_URL
85
+ claudeCodeTool.writeSettings(settings)
86
+ }
87
+ }
88
+
89
+ // ── 子命令 ──────────────────────────────────────────────────────────────────
90
+
91
+ async function handleStop() {
92
+ const info = readPid()
93
+ if (!info) {
94
+ console.log(chalk.yellow('没有正在运行的代理'))
95
+ return
96
+ }
97
+
98
+ if (isProcessAlive(info.pid)) {
99
+ try {
100
+ process.kill(info.pid, 'SIGTERM')
101
+ console.log(chalk.green(`已停止代理 (PID ${info.pid})`))
102
+ } catch (e) {
103
+ console.log(chalk.red(`停止失败: ${e.message}`))
104
+ if (isWin) {
105
+ try { execSync(`taskkill /F /PID ${info.pid}`, { stdio: 'ignore' }) } catch {}
106
+ }
107
+ }
108
+ } else {
109
+ console.log(chalk.gray('代理进程已不存在'))
110
+ }
111
+
112
+ clearBaseUrlFromSettings()
113
+ clearPid()
114
+ }
115
+
116
+ function handleStatus() {
117
+ const info = readPid()
118
+ if (!info) {
119
+ console.log(chalk.yellow('代理未启动'))
120
+ return
121
+ }
122
+
123
+ const alive = isProcessAlive(info.pid)
124
+ const healthy = alive && isProxyHealthy(info.port)
125
+
126
+ if (healthy) {
127
+ console.log(chalk.green(`代理运行中`))
128
+ console.log(chalk.gray(` PID: ${info.pid}`))
129
+ console.log(chalk.gray(` 端口: ${info.port}`))
130
+ console.log(chalk.gray(` 地址: ${getLocalProxyUrl(info.port)}`))
131
+ console.log(chalk.gray(` 启动: ${info.startedAt}`))
132
+ } else if (alive) {
133
+ console.log(chalk.yellow(`代理进程存在 (PID ${info.pid}) 但未响应`))
134
+ } else {
135
+ console.log(chalk.yellow('代理进程已退出'))
136
+ clearPid()
137
+ clearBaseUrlFromSettings()
138
+ }
139
+ }
140
+
141
+ async function handleDaemon() {
142
+ // 检查是否已运行
143
+ const info = readPid()
144
+ if (info && isProcessAlive(info.pid) && isProxyHealthy(info.port)) {
145
+ console.log(chalk.green(`代理已在运行 (PID ${info.pid}, 端口 ${info.port})`))
146
+ return
147
+ }
148
+
149
+ const scriptPath = path.join(__dirname, '..', 'index.js')
150
+ const spawnCmd = isWin ? 'node' : process.execPath
151
+ const child = spawn(spawnCmd, [scriptPath, 'claude-proxy'], {
152
+ detached: true,
153
+ stdio: 'ignore',
154
+ windowsHide: true,
155
+ })
156
+ child.unref()
157
+
158
+ // 等代理就绪
159
+ const port = getProcessProxyPort()
160
+ for (let i = 0; i < 15; i++) {
161
+ await new Promise(r => setTimeout(r, 500))
162
+ if (isProxyHealthy(port)) {
163
+ console.log(chalk.green(`代理已在后台启动`))
164
+ console.log(chalk.gray(` PID: ${child.pid}`))
165
+ console.log(chalk.gray(` 端口: ${port}`))
166
+ console.log(chalk.gray(` 地址: ${getLocalProxyUrl(port)}`))
167
+ console.log(chalk.cyan('\n VS Code Claude 扩展现在可以使用了'))
168
+ return
169
+ }
170
+ }
171
+
172
+ console.log(chalk.yellow('代理启动中,请稍等...'))
173
+ console.log(chalk.gray(` PID: ${child.pid}`))
174
+ }
175
+
176
+ async function handleForeground() {
177
+ const config = readConfig()
178
+ const apiKey = config.apiKey || getApiKey()
179
+ if (!apiKey) {
180
+ console.log(chalk.red('缺少 API Key,请先运行 hs setup'))
181
+ process.exit(1)
182
+ }
183
+
184
+ // 检查是否已运行
185
+ const existing = readPid()
186
+ if (existing && isProcessAlive(existing.pid) && isProxyHealthy(existing.port)) {
187
+ console.log(chalk.yellow(`代理已在运行 (PID ${existing.pid}, 端口 ${existing.port}),先停止: hs claude-proxy --stop`))
188
+ process.exit(1)
189
+ }
190
+
191
+ const ensureClaudeProxyConfig = require('./claude').ensureClaudeProxyConfig || (() => {})
192
+ try { ensureClaudeProxyConfig(apiKey) } catch {}
193
+
194
+ console.log(chalk.gray('启动 Claude 代理...'))
195
+
196
+ const { server, port, sessionId } = await startProcessProxy({})
197
+
198
+ writePid(process.pid, port, sessionId)
199
+ writeBaseUrlToSettings(port)
200
+
201
+ console.log(chalk.green(`\n✓ Claude 代理已启动`))
202
+ console.log(chalk.gray(` 端口: ${port}`))
203
+ console.log(chalk.gray(` 地址: ${getLocalProxyUrl(port)}`))
204
+ console.log(chalk.gray(` session: ${sessionId}`))
205
+ console.log(chalk.cyan('\n VS Code Claude 扩展现在可以使用了'))
206
+ console.log(chalk.gray(' 按 Ctrl+C 停止\n'))
207
+
208
+ const cleanup = async () => {
209
+ console.log(chalk.gray('\n正在停止...'))
210
+ clearBaseUrlFromSettings()
211
+ clearPid()
212
+ server.close()
213
+ await closeSession(undefined, sessionId)
214
+ process.exit(0)
215
+ }
216
+
217
+ process.on('SIGINT', cleanup)
218
+ process.on('SIGTERM', cleanup)
219
+ }
220
+
221
+ // ── 入口 ──────────────────────────────────────────────────────────────────
222
+
223
+ async function claudeProxy(args = []) {
224
+ if (args.includes('--stop')) {
225
+ return handleStop()
226
+ }
227
+ if (args.includes('--status')) {
228
+ return handleStatus()
229
+ }
230
+ if (args.includes('--daemon') || args.includes('-d')) {
231
+ return handleDaemon()
232
+ }
233
+ return handleForeground()
234
+ }
235
+
236
+ // 导出 ensureClaudeProxyConfig 检测函数供 daemon 使用
237
+ claudeProxy.ensureClaudeProxyConfig = function (apiKey) {
238
+ const claudeCodeTool = require('../tools/claude-code')
239
+ const proxy = require('../tools/claude-process-proxy')
240
+ const config = proxy.readConfig()
241
+ if (!config.apiKey || !config.bridgeSecret) {
242
+ const bridgeConfig = claudeCodeTool.buildBridgeConfig(apiKey, undefined, config)
243
+ proxy.writeConfig(bridgeConfig)
244
+ }
245
+ }
246
+
247
+ module.exports = claudeProxy
package/src/index.js CHANGED
@@ -194,6 +194,16 @@ program
194
194
  process.exit(code)
195
195
  })
196
196
 
197
+ // ── claude-proxy ────────────────────────────────────────────────────────────
198
+ program
199
+ .command('claude-proxy [args...]')
200
+ .allowUnknownOption(true)
201
+ .description('启动 Claude 代理服务(让 VS Code Claude 扩展也能用 HolySheep)')
202
+ .action(async (args = []) => {
203
+ const claudeProxy = require('./commands/claude-proxy')
204
+ await claudeProxy(args)
205
+ })
206
+
197
207
  program.parse(process.argv)
198
208
 
199
209
  // 默认:无子命令时显示帮助 + 提示 setup
@@ -346,6 +346,31 @@ async function loadTools() {
346
346
  function renderToolCard(tool) {
347
347
  let dotClass, statusBadges, actions, hintLine
348
348
 
349
+ // Claude Proxy 特殊卡片
350
+ if (tool.id === 'claude-proxy') {
351
+ if (tool.configured) {
352
+ dotClass = 'dot-ok'
353
+ statusBadges = `<span class="badge badge-ok">运行中</span>`
354
+ actions = `<button class="btn btn-danger btn-sm" onclick="doClaudeProxyStop()">停止代理</button>`
355
+ hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
356
+ } else {
357
+ dotClass = 'dot-warn'
358
+ statusBadges = `<span class="badge badge-warn">未启动</span>`
359
+ actions = `<button class="btn btn-primary btn-sm" onclick="doClaudeProxyStart()">启动代理</button>`
360
+ hintLine = ''
361
+ }
362
+ const hintText = tool.hint ? `<div class="tool-hint">${esc(tool.hint)}</div>` : ''
363
+ return `<div class="tool-card" id="tool-${tool.id}">
364
+ <div class="tool-dot ${dotClass}"></div>
365
+ <div class="tool-body">
366
+ <div class="tool-name">${esc(tool.name)}</div>
367
+ <div class="tool-meta">${statusBadges} ${hintLine}</div>
368
+ ${hintText}
369
+ </div>
370
+ <div class="tool-actions">${actions}</div>
371
+ </div>`
372
+ }
373
+
349
374
  if (!tool.installed) {
350
375
  dotClass = 'dot-gray'
351
376
  statusBadges = `<span class="badge badge-gray">${t('notInstalled')}</span>`
@@ -424,6 +449,20 @@ async function doConfigureTool(id, name) {
424
449
  loadTools()
425
450
  }
426
451
 
452
+ async function doClaudeProxyStart() {
453
+ try {
454
+ await api('claude-proxy/start', { method: 'POST' })
455
+ loadTools()
456
+ } catch (e) { alert('启动失败: ' + e.message) }
457
+ }
458
+
459
+ async function doClaudeProxyStop() {
460
+ try {
461
+ await api('claude-proxy/stop', { method: 'POST' })
462
+ loadTools()
463
+ } catch (e) { alert('停止失败: ' + e.message) }
464
+ }
465
+
427
466
  async function doLaunchTool(id) {
428
467
  try {
429
468
  await api('tool/launch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ toolId: id }) })
@@ -285,6 +285,17 @@ async function handleDoctor(_req, res) {
285
285
  })
286
286
  }
287
287
 
288
+ function isClaudeProxyRunning() {
289
+ try {
290
+ const pidFile = path.join(require('os').homedir(), '.holysheep', 'claude-proxy.pid')
291
+ const info = JSON.parse(fs.readFileSync(pidFile, 'utf8'))
292
+ process.kill(info.pid, 0) // check alive
293
+ return { running: true, port: info.port, pid: info.pid }
294
+ } catch {
295
+ return { running: false }
296
+ }
297
+ }
298
+
288
299
  async function handleTools(_req, res) {
289
300
  const tools = await Promise.all(TOOLS.map(async t => {
290
301
  const installed = t.checkInstalled()
@@ -301,6 +312,23 @@ async function handleTools(_req, res) {
301
312
  canUpgrade: !!UPGRADABLE_TOOLS.find(u => u.id === t.id),
302
313
  }
303
314
  }))
315
+
316
+ // 追加 claude-proxy 虚拟工具
317
+ const proxyState = isClaudeProxyRunning()
318
+ tools.push({
319
+ id: 'claude-proxy',
320
+ name: 'Claude 代理(VS Code)',
321
+ installed: true,
322
+ configured: proxyState.running,
323
+ version: proxyState.running ? `端口 ${proxyState.port}` : null,
324
+ installCmd: null,
325
+ hint: '启动后 VS Code Claude 扩展即可通过 HolySheep 使用',
326
+ launchCmd: 'hs claude-proxy',
327
+ canAutoInstall: false,
328
+ canUpgrade: false,
329
+ isProxy: true,
330
+ })
331
+
304
332
  json(res, tools)
305
333
  }
306
334
 
@@ -695,6 +723,35 @@ async function handleToolLaunch(req, res) {
695
723
  json(res, { ok: true, type: 'terminal', cmd })
696
724
  }
697
725
 
726
+ // ── Claude Proxy start/stop ──────────────────────────────────────────────────
727
+
728
+ async function handleClaudeProxyStart(_req, res) {
729
+ const state = isClaudeProxyRunning()
730
+ if (state.running) {
731
+ return json(res, { ok: true, message: '代理已在运行', port: state.port, pid: state.pid })
732
+ }
733
+ try {
734
+ const claudeProxy = require('../commands/claude-proxy')
735
+ await claudeProxy(['--daemon'])
736
+ // 等一下确认
737
+ await new Promise(r => setTimeout(r, 2000))
738
+ const newState = isClaudeProxyRunning()
739
+ json(res, { ok: newState.running, port: newState.port, pid: newState.pid })
740
+ } catch (e) {
741
+ json(res, { ok: false, error: e.message }, 500)
742
+ }
743
+ }
744
+
745
+ async function handleClaudeProxyStop(_req, res) {
746
+ try {
747
+ const claudeProxy = require('../commands/claude-proxy')
748
+ await claudeProxy(['--stop'])
749
+ json(res, { ok: true })
750
+ } catch (e) {
751
+ json(res, { ok: false, error: e.message }, 500)
752
+ }
753
+ }
754
+
698
755
  // ── Environment variables ────────────────────────────────────────────────────
699
756
 
700
757
  const HS_ENV_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL']
@@ -782,6 +839,8 @@ async function handleRequest(req, res) {
782
839
  if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
783
840
  if (route === '/api/tool/upgrade' && req.method === 'POST') return await handleToolUpgrade(req, res)
784
841
  if (route === '/api/tool/launch' && req.method === 'POST') return await handleToolLaunch(req, res)
842
+ if (route === '/api/claude-proxy/start' && req.method === 'POST') return await handleClaudeProxyStart(req, res)
843
+ if (route === '/api/claude-proxy/stop' && req.method === 'POST') return await handleClaudeProxyStop(req, res)
785
844
  if (route === '/api/env' && req.method === 'GET') return handleEnv(req, res)
786
845
  if (route === '/api/env/clean' && req.method === 'POST') return handleEnvClean(req, res)
787
846
  if (route === '/api/restart' && req.method === 'POST') {