@simonyea/holysheep-cli 2.1.38 → 2.1.41

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.
Files changed (45) hide show
  1. package/dist/configure-worker.js +4491 -0
  2. package/dist/index.js +9591 -0
  3. package/dist/process-proxy-inject.js +117 -0
  4. package/package.json +19 -6
  5. package/.gitea/workflows/sanity.yml +0 -125
  6. package/scripts/check-tarball-size.js +0 -44
  7. package/src/commands/balance.js +0 -57
  8. package/src/commands/claude-proxy.js +0 -248
  9. package/src/commands/claude.js +0 -135
  10. package/src/commands/doctor.js +0 -282
  11. package/src/commands/login.js +0 -211
  12. package/src/commands/openclaw.js +0 -258
  13. package/src/commands/reset.js +0 -53
  14. package/src/commands/setup.js +0 -493
  15. package/src/commands/upgrade.js +0 -168
  16. package/src/commands/webui.js +0 -622
  17. package/src/index.js +0 -226
  18. package/src/tools/aider.js +0 -78
  19. package/src/tools/antigravity.js +0 -42
  20. package/src/tools/claude-code.js +0 -228
  21. package/src/tools/claude-process-proxy.js +0 -1030
  22. package/src/tools/codex.js +0 -254
  23. package/src/tools/continue.js +0 -146
  24. package/src/tools/cursor.js +0 -71
  25. package/src/tools/droid.js +0 -281
  26. package/src/tools/env-config.js +0 -185
  27. package/src/tools/gemini-cli.js +0 -82
  28. package/src/tools/hermes.js +0 -354
  29. package/src/tools/index.js +0 -13
  30. package/src/tools/openclaw-bridge.js +0 -987
  31. package/src/tools/openclaw.js +0 -925
  32. package/src/tools/opencode.js +0 -227
  33. package/src/tools/process-proxy-inject.js +0 -142
  34. package/src/utils/config.js +0 -54
  35. package/src/utils/shell.js +0 -342
  36. package/src/utils/which.js +0 -63
  37. package/src/webui/aionui-runtime-fetcher.js +0 -429
  38. package/src/webui/aionui-runtime.js +0 -139
  39. package/src/webui/aionui-wrapper.js +0 -734
  40. package/src/webui/configure-worker.js +0 -67
  41. package/src/webui/server.js +0 -1566
  42. package/src/webui/workspace-runtime.js +0 -288
  43. package/src/webui/workspace-store.js +0 -325
  44. /package/{src/webui → dist}/index.html +0 -0
  45. /package/{src/tools → dist}/pty-hermes-wrapper.py +0 -0
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/tools/process-proxy-inject.js
6
+ var net = require("net");
7
+ var { URL } = require("url");
8
+ var raw = process.env.HS_PROXY_URL;
9
+ if (!raw) return;
10
+ var parsed;
11
+ try {
12
+ parsed = new URL(raw);
13
+ } catch {
14
+ return;
15
+ }
16
+ var PROXY_HOST = parsed.hostname;
17
+ var PROXY_PORT = Number(parsed.port) || 80;
18
+ var SKIP = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "0.0.0.0", PROXY_HOST]);
19
+ var BLOCK = /* @__PURE__ */ new Set(["api.anthropic.com"]);
20
+ function shouldProxy(host, port) {
21
+ if (!port || !host || SKIP.has(host)) return false;
22
+ if (BLOCK.has(host)) return "block";
23
+ return true;
24
+ }
25
+ __name(shouldProxy, "shouldProxy");
26
+ function rewriteHost(host) {
27
+ if (host === "api.anthropic.com") return "api.holysheep.ai";
28
+ return host;
29
+ }
30
+ __name(rewriteHost, "rewriteHost");
31
+ function setupTunnel(sock, host, port, origEmit) {
32
+ const targetHost = rewriteHost(host);
33
+ sock.write(`CONNECT ${targetHost}:${port} HTTP/1.1\r
34
+ Host: ${targetHost}:${port}\r
35
+ \r
36
+ `);
37
+ let buf = Buffer.alloc(0);
38
+ sock.on("data", /* @__PURE__ */ __name(function onData(chunk) {
39
+ buf = Buffer.concat([buf, chunk]);
40
+ const i = buf.indexOf("\r\n\r\n");
41
+ if (i === -1) return;
42
+ sock.removeListener("data", onData);
43
+ if (!buf.slice(0, i).toString().includes(" 200 ")) {
44
+ sock.emit = origEmit;
45
+ sock.destroy(new Error(`CONNECT ${host}:${port} failed: ${buf.slice(0, buf.indexOf("\r\n")).toString()}`));
46
+ return;
47
+ }
48
+ const rest = buf.slice(i + 4);
49
+ if (rest.length) sock.unshift(rest);
50
+ sock.emit = origEmit;
51
+ origEmit("connect");
52
+ }, "onData"));
53
+ }
54
+ __name(setupTunnel, "setupTunnel");
55
+ function patchEmitAndConnect(sock, host, port, connectFn) {
56
+ let tunnelReady = false;
57
+ const origEmit = sock.emit.bind(sock);
58
+ sock.emit = function(type) {
59
+ if (type === "connect" && !tunnelReady) {
60
+ tunnelReady = true;
61
+ setupTunnel(sock, host, port, origEmit);
62
+ return false;
63
+ }
64
+ return origEmit.apply(null, arguments);
65
+ };
66
+ return connectFn();
67
+ }
68
+ __name(patchEmitAndConnect, "patchEmitAndConnect");
69
+ var _origSocketConnect = net.Socket.prototype.connect;
70
+ net.Socket.prototype.connect = function(options) {
71
+ const isObj = options !== null && typeof options === "object";
72
+ const host = isObj ? String(options.host || options.hostname || "") : String(options || "");
73
+ const port = isObj ? Number(options.port || 0) : Number(arguments[1] || 0);
74
+ const action = shouldProxy(host, port);
75
+ if (!action) return _origSocketConnect.apply(this, arguments);
76
+ if (action === "block") {
77
+ const sock2 = this;
78
+ process.nextTick(() => sock2.destroy(new Error(`ECONNREFUSED: blocked direct connection to ${host}`)));
79
+ return sock2;
80
+ }
81
+ const sock = this;
82
+ const proxyOpts = isObj ? { ...options, host: PROXY_HOST, port: PROXY_PORT } : { host: PROXY_HOST, port: PROXY_PORT };
83
+ return patchEmitAndConnect(
84
+ sock,
85
+ host,
86
+ port,
87
+ () => _origSocketConnect.call(sock, proxyOpts)
88
+ );
89
+ };
90
+ var _origCreate = net.createConnection;
91
+ function proxied(options, cb) {
92
+ const isObj = options !== null && typeof options === "object";
93
+ const host = isObj ? String(options.host || options.hostname || "localhost") : String(options || "localhost");
94
+ const port = isObj ? Number(options.port || 0) : Number(arguments[1] || 0);
95
+ const action = shouldProxy(host, port);
96
+ if (!action) return _origCreate.apply(this, arguments);
97
+ if (action === "block") {
98
+ const sock2 = new net.Socket();
99
+ process.nextTick(() => sock2.destroy(new Error(`ECONNREFUSED: blocked direct connection to ${host}`)));
100
+ return sock2;
101
+ }
102
+ const sock = _origCreate({ host: PROXY_HOST, port: PROXY_PORT });
103
+ if (typeof cb === "function") sock.once("connect", cb);
104
+ let tunnelReady = false;
105
+ const origEmit = sock.emit.bind(sock);
106
+ sock.emit = function(type) {
107
+ if (type === "connect" && !tunnelReady) {
108
+ tunnelReady = true;
109
+ setupTunnel(sock, host, port, origEmit);
110
+ return false;
111
+ }
112
+ return origEmit.apply(null, arguments);
113
+ };
114
+ return sock;
115
+ }
116
+ __name(proxied, "proxied");
117
+ net.createConnection = net.connect = proxied;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.1.38",
3
+ "version": "2.1.41",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China \u2014 \u00a51=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "scripts": {
6
- "test": "node tests/droid.test.js && node tests/workspace-store.test.js && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js && node tests/opencode-auth-purge.test.js && node tests/shell-winpath.test.js && node tests/openclaw-atomic-write.test.js",
7
- "prepublishOnly": "node scripts/check-tarball-size.js"
6
+ "build": "node scripts/build.mjs",
7
+ "test": "node tests/droid.test.js && node tests/workspace-store.test.js && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js && node tests/opencode-auth-purge.test.js && node tests/shell-winpath.test.js && node tests/openclaw-atomic-write.test.js && node tests/opencode-default-model.test.js && node tests/paths-bundled.test.js",
8
+ "prepublishOnly": "npm run build && npm test && node scripts/check-tarball-size.js"
8
9
  },
9
10
  "keywords": [
10
11
  "openai-china",
@@ -50,11 +51,20 @@
50
51
  },
51
52
  "license": "MIT",
52
53
  "bin": {
53
- "hs": "src/index.js",
54
- "holysheep": "src/index.js"
54
+ "hs": "dist/index.js",
55
+ "holysheep": "dist/index.js"
55
56
  },
56
57
  "type": "commonjs",
57
- "main": "src/index.js",
58
+ "main": "dist/index.js",
59
+ "files": [
60
+ "dist/index.js",
61
+ "dist/configure-worker.js",
62
+ "dist/process-proxy-inject.js",
63
+ "dist/index.html",
64
+ "dist/pty-hermes-wrapper.py",
65
+ "README.md",
66
+ "LICENSE"
67
+ ],
58
68
  "engines": {
59
69
  "node": ">=16.0.0"
60
70
  },
@@ -64,5 +74,8 @@
64
74
  "inquirer": "^8.2.6",
65
75
  "node-fetch": "^2.7.0",
66
76
  "ora": "^5.4.1"
77
+ },
78
+ "devDependencies": {
79
+ "esbuild": "^0.28.0"
67
80
  }
68
81
  }
@@ -1,125 +0,0 @@
1
- name: CI sanity
2
-
3
- # [HolySheep fork] Minimal Gitea-side CI for holysheep-cli.
4
- # Mirrors the sanity check pattern from holysheep-webui.
5
-
6
- on:
7
- push:
8
- branches: [main]
9
- pull_request:
10
- branches: [main]
11
-
12
- jobs:
13
- sanity:
14
- name: Sanity check (version + key files + invariants)
15
- runs-on: ubuntu-latest
16
- steps:
17
- - name: Checkout (host deploy-cache mode)
18
- run: |
19
- set -e
20
- REPO_DIR="/opt/act_runner/deploy-cache/holysheep-cli"
21
- BRANCH="${{ github.ref_name }}"
22
- BRANCH="${BRANCH:-${GITHUB_REF_NAME:-main}}"
23
- echo "branch: $BRANCH"
24
- if [ -d "$REPO_DIR/.git" ]; then
25
- cd "$REPO_DIR"
26
- git fetch origin "$BRANCH"
27
- git reset --hard "origin/$BRANCH"
28
- else
29
- mkdir -p "$REPO_DIR"
30
- git clone --depth 1 -b "$BRANCH" \
31
- http://simon:123123aa@localhost:3000/simon/holysheep-cli.git "$REPO_DIR"
32
- fi
33
- cd "$REPO_DIR"
34
- echo "head: $(git rev-parse --short HEAD)"
35
- echo "msg: $(git log -1 --pretty=%s)"
36
-
37
- - name: Version format check
38
- run: |
39
- set -e
40
- cd /opt/act_runner/deploy-cache/holysheep-cli
41
- VER=$(node -e "console.log(require('./package.json').version)")
42
- echo "version: $VER"
43
- if ! echo "$VER" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
44
- echo "::error::package.json version '$VER' does not match semver"
45
- exit 1
46
- fi
47
- echo " ok: version $VER"
48
-
49
- - name: Key source files present
50
- run: |
51
- set -e
52
- cd /opt/act_runner/deploy-cache/holysheep-cli
53
- for f in \
54
- src/webui/aionui-wrapper.js \
55
- src/webui/aionui-runtime-fetcher.js \
56
- src/tools/claude-process-proxy.js \
57
- src/tools/pty-hermes-wrapper.py \
58
- src/tools/droid.js \
59
- src/tools/hermes.js; do
60
- if [ ! -f "$f" ]; then
61
- echo "::error::required file missing: $f"
62
- exit 1
63
- fi
64
- echo " ok: $f"
65
- done
66
-
67
- - name: Runtime fetcher URL/SHA/VERSION consistency
68
- run: |
69
- set -e
70
- cd /opt/act_runner/deploy-cache/holysheep-cli
71
- # The URL value is on the line AFTER 'const DEFAULT_RUNTIME_URL ='
72
- # Use node to read the values reliably
73
- URL_TAG=$(node -e "
74
- const fs = require('fs');
75
- const content = fs.readFileSync('src/webui/aionui-runtime-fetcher.js', 'utf8');
76
- const urlMatch = content.match(/DEFAULT_RUNTIME_URL\s*=\s*[\r\n\s]*'[^']*holysheep-(hs\d+)[^']*'/);
77
- const verMatch = content.match(/DEFAULT_RUNTIME_VERSION\s*=\s*'[^']*holysheep-(hs\d+)[^']*'/);
78
- if (!urlMatch || !verMatch) { process.stderr.write('PARSE_FAIL'); process.exit(1); }
79
- if (urlMatch[1] !== verMatch[1]) {
80
- process.stderr.write('MISMATCH: URL=' + urlMatch[1] + ' VER=' + verMatch[1]);
81
- process.exit(1);
82
- }
83
- console.log(urlMatch[1]);
84
- " 2>&1)
85
- echo "URL/VERSION tag: $URL_TAG"
86
- if [ -z "$URL_TAG" ] || echo "$URL_TAG" | grep -q "FAIL\|MISMATCH"; then
87
- echo "::error::DEFAULT_RUNTIME_URL and DEFAULT_RUNTIME_VERSION hs-tags mismatch: $URL_TAG"
88
- exit 1
89
- fi
90
- echo " ok: URL and VERSION both reference $URL_TAG"
91
-
92
- - name: Invariant grep (brand + proxy)
93
- run: |
94
- set -e
95
- cd /opt/act_runner/deploy-cache/holysheep-cli
96
-
97
- # Brand: wrapper must NOT use old [aionui-wrapper] prefix
98
- if grep -q "\[aionui-wrapper\]" src/webui/aionui-wrapper.js; then
99
- echo "::error::aionui-wrapper.js still contains [aionui-wrapper] log prefix (should be [holysheep-web])"
100
- exit 1
101
- fi
102
- echo " ok: no [aionui-wrapper] prefix"
103
-
104
- # mode field must be holysheep-webui
105
- if ! grep -q "holysheep-webui" src/webui/aionui-wrapper.js; then
106
- echo "::error::aionui-wrapper.js missing mode=holysheep-webui"
107
- exit 1
108
- fi
109
- echo " ok: holysheep-webui mode present"
110
-
111
- # claude-process-proxy: sanitizeClaudeClientHeaders must exist
112
- if ! grep -q "sanitizeClaudeClientHeaders" src/tools/claude-process-proxy.js; then
113
- echo "::error::claude-process-proxy.js missing sanitizeClaudeClientHeaders (UA-rewrite regression)"
114
- exit 1
115
- fi
116
- echo " ok: sanitizeClaudeClientHeaders present"
117
-
118
- # pty-hermes-wrapper: ICANON must be disabled
119
- if ! grep -q "ICANON" src/tools/pty-hermes-wrapper.py; then
120
- echo "::error::pty-hermes-wrapper.py missing ICANON disable (PTY truncation regression)"
121
- exit 1
122
- fi
123
- echo " ok: ICANON disabled in pty-hermes-wrapper.py"
124
-
125
- echo "all invariants passed"
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * prepublish guard: refuse to publish if the tarball grows beyond the
4
- * expected ~100KB slim size. This protects against accidentally packing
5
- * `src/webui/vendor/` (the 154MB AionUi runtime) again, which broke
6
- * 2.0.2 (Taobao mirror skipped it, Windows got ETARGET).
7
- *
8
- * If this script fails, inspect `.npmignore` — vendor/ must stay excluded.
9
- */
10
- 'use strict'
11
-
12
- const { execSync } = require('child_process')
13
-
14
- const MAX_BYTES = 5 * 1024 * 1024 // 5MB hard ceiling
15
- const SOFT_FILES = 100 // flag if > 100 files
16
-
17
- try {
18
- const raw = execSync('npm pack --dry-run --json', { stdio: ['ignore', 'pipe', 'pipe'] }).toString()
19
- const info = JSON.parse(raw)[0]
20
- const { size, unpackedSize, entryCount, filename } = info
21
-
22
- const sizeKB = (size / 1024).toFixed(1)
23
- const unpackedMB = (unpackedSize / 1024 / 1024).toFixed(2)
24
-
25
- if (size > MAX_BYTES) {
26
- console.error(`\n❌ prepublish guard FAILED`)
27
- console.error(` ${filename}`)
28
- console.error(` packed: ${sizeKB} kB (limit 5 MB)`)
29
- console.error(` unpacked: ${unpackedMB} MB`)
30
- console.error(` files: ${entryCount}`)
31
- console.error(`\n vendor/ 可能又被打进包了。检查 .npmignore 是否排除 src/webui/vendor/`)
32
- process.exit(1)
33
- }
34
-
35
- if (entryCount > SOFT_FILES) {
36
- console.warn(`\n⚠️ prepublish guard: ${entryCount} files is unusually high (soft limit ${SOFT_FILES})`)
37
- console.warn(` 仍允许发布,但请确认没有误打包大目录`)
38
- }
39
-
40
- console.log(`✅ prepublish guard OK: ${sizeKB} kB, ${entryCount} files, ${unpackedMB} MB unpacked`)
41
- } catch (err) {
42
- console.error(`prepublish guard could not run: ${err.message}`)
43
- process.exit(1)
44
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * hs balance — 查看账户余额和今日用量
3
- */
4
- const chalk = require('chalk')
5
- const ora = require('ora')
6
- const { getApiKey, SHOP_URL } = require('../utils/config')
7
-
8
- async function balance() {
9
- const apiKey = getApiKey()
10
- if (!apiKey) {
11
- console.log(chalk.red('\n未找到 API Key,请先运行: hs setup\n'))
12
- return
13
- }
14
-
15
- const spinner = ora('获取账户信息...').start()
16
- try {
17
- const fetch = require('node-fetch')
18
- // Use the canonical www host directly. `${SHOP_URL}` returns a 301 redirect,
19
- // and node-fetch drops the Authorization header on cross-origin redirects,
20
- // which surfaced as "HTTP 404" on every `hs balance` call (2.1.13 bug).
21
- const res = await fetch('https://www.holysheep.ai/api/stats/overview', {
22
- headers: { 'Authorization': `Bearer ${apiKey}` },
23
- timeout: 8000,
24
- })
25
-
26
- if (res.status === 401) {
27
- spinner.fail('API Key 无效或已过期,请重新登录')
28
- return
29
- }
30
-
31
- if (!res.ok) {
32
- spinner.fail(`请求失败 (HTTP ${res.status})`)
33
- return
34
- }
35
-
36
- const data = await res.json()
37
- spinner.stop()
38
-
39
- console.log()
40
- console.log(chalk.bold('💰 账户余额'))
41
- console.log(chalk.gray('━'.repeat(40)))
42
- console.log()
43
- console.log(` ${chalk.cyan('余额')} $${chalk.bold(Number(data.balance || 0).toFixed(4))}`)
44
- console.log(` ${chalk.cyan('今日消费')} $${Number(data.todayCost || 0).toFixed(4)}`)
45
- console.log(` ${chalk.cyan('本月消费')} $${Number(data.monthCost || 0).toFixed(4)}`)
46
- console.log(` ${chalk.cyan('累计调用')} ${(data.totalCalls || 0).toLocaleString()} 次`)
47
- console.log()
48
- console.log(chalk.gray(`充值: ${SHOP_URL}/app/recharge`))
49
- console.log()
50
-
51
- } catch (e) {
52
- spinner.fail(`获取失败: ${e.message}`)
53
- console.log(chalk.gray(`\n请前往 ${SHOP_URL} 查看账户信息`))
54
- }
55
- }
56
-
57
- module.exports = balance
@@ -1,248 +0,0 @@
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
- // Windows: Invoke-WebRequest 遇到 500 会抛异常,改用 TCP 端口检查
63
- execSync(
64
- isWin
65
- ? `powershell -NonInteractive -Command "if(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue){exit 0}else{exit 1}"`
66
- : `curl -so /dev/null --max-time 1 http://127.0.0.1:${port}/`,
67
- { stdio: 'ignore', timeout: 5000, windowsHide: true }
68
- )
69
- return true
70
- } catch {
71
- return false
72
- }
73
- }
74
-
75
- function writeBaseUrlToSettings(port) {
76
- const settings = claudeCodeTool.readSettings()
77
- if (!settings.env) settings.env = {}
78
- settings.env.ANTHROPIC_BASE_URL = getLocalProxyUrl(port)
79
- claudeCodeTool.writeSettings(settings)
80
- }
81
-
82
- function clearBaseUrlFromSettings() {
83
- const settings = claudeCodeTool.readSettings()
84
- if (settings.env?.ANTHROPIC_BASE_URL?.includes('127.0.0.1')) {
85
- delete settings.env.ANTHROPIC_BASE_URL
86
- claudeCodeTool.writeSettings(settings)
87
- }
88
- }
89
-
90
- // ── 子命令 ──────────────────────────────────────────────────────────────────
91
-
92
- async function handleStop() {
93
- const info = readPid()
94
- if (!info) {
95
- console.log(chalk.yellow('没有正在运行的代理'))
96
- return
97
- }
98
-
99
- if (isProcessAlive(info.pid)) {
100
- try {
101
- process.kill(info.pid, 'SIGTERM')
102
- console.log(chalk.green(`已停止代理 (PID ${info.pid})`))
103
- } catch (e) {
104
- console.log(chalk.red(`停止失败: ${e.message}`))
105
- if (isWin) {
106
- try { execSync(`taskkill /F /PID ${info.pid}`, { stdio: 'ignore' }) } catch {}
107
- }
108
- }
109
- } else {
110
- console.log(chalk.gray('代理进程已不存在'))
111
- }
112
-
113
- clearBaseUrlFromSettings()
114
- clearPid()
115
- }
116
-
117
- function handleStatus() {
118
- const info = readPid()
119
- if (!info) {
120
- console.log(chalk.yellow('代理未启动'))
121
- return
122
- }
123
-
124
- const alive = isProcessAlive(info.pid)
125
- const healthy = alive && isProxyHealthy(info.port)
126
-
127
- if (healthy) {
128
- console.log(chalk.green(`代理运行中`))
129
- console.log(chalk.gray(` PID: ${info.pid}`))
130
- console.log(chalk.gray(` 端口: ${info.port}`))
131
- console.log(chalk.gray(` 地址: ${getLocalProxyUrl(info.port)}`))
132
- console.log(chalk.gray(` 启动: ${info.startedAt}`))
133
- } else if (alive) {
134
- console.log(chalk.yellow(`代理进程存在 (PID ${info.pid}) 但未响应`))
135
- } else {
136
- console.log(chalk.yellow('代理进程已退出'))
137
- clearPid()
138
- clearBaseUrlFromSettings()
139
- }
140
- }
141
-
142
- async function handleDaemon() {
143
- // 检查是否已运行
144
- const info = readPid()
145
- if (info && isProcessAlive(info.pid) && isProxyHealthy(info.port)) {
146
- console.log(chalk.green(`代理已在运行 (PID ${info.pid}, 端口 ${info.port})`))
147
- return
148
- }
149
-
150
- const scriptPath = path.join(__dirname, '..', 'index.js')
151
- const spawnCmd = isWin ? 'node' : process.execPath
152
- const child = spawn(spawnCmd, [scriptPath, 'claude-proxy'], {
153
- detached: true,
154
- stdio: 'ignore',
155
- windowsHide: true,
156
- })
157
- child.unref()
158
-
159
- // 等代理就绪
160
- const port = getProcessProxyPort()
161
- for (let i = 0; i < 15; i++) {
162
- await new Promise(r => setTimeout(r, 500))
163
- if (isProxyHealthy(port)) {
164
- console.log(chalk.green(`代理已在后台启动`))
165
- console.log(chalk.gray(` PID: ${child.pid}`))
166
- console.log(chalk.gray(` 端口: ${port}`))
167
- console.log(chalk.gray(` 地址: ${getLocalProxyUrl(port)}`))
168
- console.log(chalk.cyan('\n VS Code Claude 扩展现在可以使用了'))
169
- return
170
- }
171
- }
172
-
173
- console.log(chalk.yellow('代理启动中,请稍等...'))
174
- console.log(chalk.gray(` PID: ${child.pid}`))
175
- }
176
-
177
- async function handleForeground() {
178
- const config = readConfig()
179
- const apiKey = config.apiKey || getApiKey()
180
- if (!apiKey) {
181
- console.log(chalk.red('缺少 API Key,请先运行 hs setup'))
182
- process.exit(1)
183
- }
184
-
185
- // 检查是否已运行
186
- const existing = readPid()
187
- if (existing && isProcessAlive(existing.pid) && isProxyHealthy(existing.port)) {
188
- console.log(chalk.yellow(`代理已在运行 (PID ${existing.pid}, 端口 ${existing.port}),先停止: hs claude-proxy --stop`))
189
- process.exit(1)
190
- }
191
-
192
- const ensureClaudeProxyConfig = require('./claude').ensureClaudeProxyConfig || (() => {})
193
- try { ensureClaudeProxyConfig(apiKey) } catch {}
194
-
195
- console.log(chalk.gray('启动 Claude 代理...'))
196
-
197
- const { server, port, sessionId } = await startProcessProxy({})
198
-
199
- writePid(process.pid, port, sessionId)
200
- writeBaseUrlToSettings(port)
201
-
202
- console.log(chalk.green(`\n✓ Claude 代理已启动`))
203
- console.log(chalk.gray(` 端口: ${port}`))
204
- console.log(chalk.gray(` 地址: ${getLocalProxyUrl(port)}`))
205
- console.log(chalk.gray(` session: ${sessionId}`))
206
- console.log(chalk.cyan('\n VS Code Claude 扩展现在可以使用了'))
207
- console.log(chalk.gray(' 按 Ctrl+C 停止\n'))
208
-
209
- const cleanup = async () => {
210
- console.log(chalk.gray('\n正在停止...'))
211
- clearBaseUrlFromSettings()
212
- clearPid()
213
- server.close()
214
- await closeSession(undefined, sessionId)
215
- process.exit(0)
216
- }
217
-
218
- process.on('SIGINT', cleanup)
219
- process.on('SIGTERM', cleanup)
220
- }
221
-
222
- // ── 入口 ──────────────────────────────────────────────────────────────────
223
-
224
- async function claudeProxy(args = []) {
225
- if (args.includes('--stop')) {
226
- return handleStop()
227
- }
228
- if (args.includes('--status')) {
229
- return handleStatus()
230
- }
231
- if (args.includes('--daemon') || args.includes('-d')) {
232
- return handleDaemon()
233
- }
234
- return handleForeground()
235
- }
236
-
237
- // 导出 ensureClaudeProxyConfig 检测函数供 daemon 使用
238
- claudeProxy.ensureClaudeProxyConfig = function (apiKey) {
239
- const claudeCodeTool = require('../tools/claude-code')
240
- const proxy = require('../tools/claude-process-proxy')
241
- const config = proxy.readConfig()
242
- if (!config.apiKey || !config.bridgeSecret) {
243
- const bridgeConfig = claudeCodeTool.buildBridgeConfig(apiKey, undefined, config)
244
- proxy.writeConfig(bridgeConfig)
245
- }
246
- }
247
-
248
- module.exports = claudeProxy