@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.
- package/dist/configure-worker.js +4491 -0
- package/dist/index.js +9591 -0
- package/dist/process-proxy-inject.js +117 -0
- package/package.json +19 -6
- package/.gitea/workflows/sanity.yml +0 -125
- package/scripts/check-tarball-size.js +0 -44
- package/src/commands/balance.js +0 -57
- package/src/commands/claude-proxy.js +0 -248
- package/src/commands/claude.js +0 -135
- package/src/commands/doctor.js +0 -282
- package/src/commands/login.js +0 -211
- package/src/commands/openclaw.js +0 -258
- package/src/commands/reset.js +0 -53
- package/src/commands/setup.js +0 -493
- package/src/commands/upgrade.js +0 -168
- package/src/commands/webui.js +0 -622
- package/src/index.js +0 -226
- package/src/tools/aider.js +0 -78
- package/src/tools/antigravity.js +0 -42
- package/src/tools/claude-code.js +0 -228
- package/src/tools/claude-process-proxy.js +0 -1030
- package/src/tools/codex.js +0 -254
- package/src/tools/continue.js +0 -146
- package/src/tools/cursor.js +0 -71
- package/src/tools/droid.js +0 -281
- package/src/tools/env-config.js +0 -185
- package/src/tools/gemini-cli.js +0 -82
- package/src/tools/hermes.js +0 -354
- package/src/tools/index.js +0 -13
- package/src/tools/openclaw-bridge.js +0 -987
- package/src/tools/openclaw.js +0 -925
- package/src/tools/opencode.js +0 -227
- package/src/tools/process-proxy-inject.js +0 -142
- package/src/utils/config.js +0 -54
- package/src/utils/shell.js +0 -342
- package/src/utils/which.js +0 -63
- package/src/webui/aionui-runtime-fetcher.js +0 -429
- package/src/webui/aionui-runtime.js +0 -139
- package/src/webui/aionui-wrapper.js +0 -734
- package/src/webui/configure-worker.js +0 -67
- package/src/webui/server.js +0 -1566
- package/src/webui/workspace-runtime.js +0 -288
- package/src/webui/workspace-store.js +0 -325
- /package/{src/webui → dist}/index.html +0 -0
- /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.
|
|
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
|
-
"
|
|
7
|
-
"
|
|
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": "
|
|
54
|
-
"holysheep": "
|
|
54
|
+
"hs": "dist/index.js",
|
|
55
|
+
"holysheep": "dist/index.js"
|
|
55
56
|
},
|
|
56
57
|
"type": "commonjs",
|
|
57
|
-
"main": "
|
|
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
|
-
}
|
package/src/commands/balance.js
DELETED
|
@@ -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
|