@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
package/src/commands/claude.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const { spawn } = require('child_process')
|
|
5
|
-
const {
|
|
6
|
-
BASE_URL_ANTHROPIC,
|
|
7
|
-
BASE_URL_CLAUDE_RELAY,
|
|
8
|
-
getApiKey,
|
|
9
|
-
} = require('../utils/config')
|
|
10
|
-
const {
|
|
11
|
-
closeSession,
|
|
12
|
-
getLocalProxyUrl,
|
|
13
|
-
startProcessProxy,
|
|
14
|
-
readConfig,
|
|
15
|
-
writeConfig,
|
|
16
|
-
} = require('../tools/claude-process-proxy')
|
|
17
|
-
const claudeCodeTool = require('../tools/claude-code')
|
|
18
|
-
|
|
19
|
-
const INJECT_PATH = path.resolve(__dirname, '../tools/process-proxy-inject.js')
|
|
20
|
-
|
|
21
|
-
function appendNodeRequire(existingValue, requirePath) {
|
|
22
|
-
const nextFlag = `--require ${requirePath}`
|
|
23
|
-
if (!existingValue) return nextFlag
|
|
24
|
-
return existingValue.includes(nextFlag) ? existingValue : `${existingValue} ${nextFlag}`.trim()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function mergeNoProxy(existingValue, extraHosts = []) {
|
|
28
|
-
const merged = new Set()
|
|
29
|
-
for (const chunk of String(existingValue || '').split(',')) {
|
|
30
|
-
const value = chunk.trim()
|
|
31
|
-
if (value) merged.add(value)
|
|
32
|
-
}
|
|
33
|
-
for (const host of extraHosts) {
|
|
34
|
-
if (host) merged.add(host)
|
|
35
|
-
}
|
|
36
|
-
return Array.from(merged).join(',')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function ensureClaudeProxyConfig(apiKey) {
|
|
40
|
-
const config = readConfig()
|
|
41
|
-
const next = claudeCodeTool.buildBridgeConfig(apiKey, BASE_URL_ANTHROPIC, {
|
|
42
|
-
...config,
|
|
43
|
-
relayUrl: config.relayUrl || BASE_URL_CLAUDE_RELAY,
|
|
44
|
-
})
|
|
45
|
-
const changed = JSON.stringify(next) !== JSON.stringify(config)
|
|
46
|
-
|
|
47
|
-
if (changed) {
|
|
48
|
-
writeConfig(next)
|
|
49
|
-
return next
|
|
50
|
-
}
|
|
51
|
-
return config
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function runClaude(args = []) {
|
|
55
|
-
const config = readConfig()
|
|
56
|
-
const apiKey = config.apiKey || getApiKey()
|
|
57
|
-
if (!apiKey) {
|
|
58
|
-
throw new Error('Missing API Key. Run hs setup first.')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
ensureClaudeProxyConfig(apiKey)
|
|
62
|
-
|
|
63
|
-
// 清理 settings.json 中的 ANTHROPIC_BASE_URL(旧版遗留)
|
|
64
|
-
// BASE_URL 只通过 env 变量注入,settings.json 里不能有,否则 Claude Code 双路径发请求
|
|
65
|
-
const settings = claudeCodeTool.readSettings()
|
|
66
|
-
if (settings.env?.ANTHROPIC_BASE_URL) {
|
|
67
|
-
delete settings.env.ANTHROPIC_BASE_URL
|
|
68
|
-
claudeCodeTool.writeSettings(settings)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const runtime = typeof claudeCodeTool.detectClaudeRuntime === 'function'
|
|
72
|
-
? claudeCodeTool.detectClaudeRuntime()
|
|
73
|
-
: { kind: 'unknown', launchMode: 'env-proxy' }
|
|
74
|
-
const { server, port, sessionId } = await startProcessProxy({})
|
|
75
|
-
const proxyUrl = getLocalProxyUrl(port)
|
|
76
|
-
const launchMode = runtime.launchMode === 'node-inject'
|
|
77
|
-
? 'local-api + connect-fallback + node-inject'
|
|
78
|
-
: 'whole-process-proxy + local-api'
|
|
79
|
-
|
|
80
|
-
const env = {
|
|
81
|
-
...process.env,
|
|
82
|
-
ANTHROPIC_API_KEY: undefined,
|
|
83
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
84
|
-
ANTHROPIC_BASE_URL: proxyUrl,
|
|
85
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
86
|
-
HOLYSHEEP_CLAUDE_PROCESS_PROXY: '1',
|
|
87
|
-
HOLYSHEEP_CLAUDE_SESSION_ID: sessionId,
|
|
88
|
-
HS_PROXY_URL: proxyUrl,
|
|
89
|
-
HOLYSHEEP_CLAUDE_RUNTIME_KIND: runtime.kind || 'unknown',
|
|
90
|
-
HOLYSHEEP_CLAUDE_LAUNCH_MODE: launchMode,
|
|
91
|
-
HTTP_PROXY: proxyUrl,
|
|
92
|
-
HTTPS_PROXY: proxyUrl,
|
|
93
|
-
ALL_PROXY: proxyUrl,
|
|
94
|
-
NO_PROXY: mergeNoProxy(process.env.NO_PROXY, ['127.0.0.1', 'localhost']),
|
|
95
|
-
ANTHROPIC_MAX_RETRIES: '0',
|
|
96
|
-
MAX_RETRIES: '0',
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 不再写 settings.json — 只用 env 变量,避免 Claude Code 从两个来源
|
|
100
|
-
// 读到同一个 proxy URL 导致重复请求 + 避免退出时覆盖 WebUI configure 的值
|
|
101
|
-
|
|
102
|
-
if (runtime.launchMode === 'node-inject') {
|
|
103
|
-
env.NODE_OPTIONS = appendNodeRequire(process.env.NODE_OPTIONS, INJECT_PATH)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const child = spawn('claude', args, {
|
|
107
|
-
stdio: 'inherit',
|
|
108
|
-
env,
|
|
109
|
-
shell: process.platform === 'win32',
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const cleanup = async () => {
|
|
113
|
-
try {
|
|
114
|
-
server.close()
|
|
115
|
-
} catch {}
|
|
116
|
-
await closeSession(undefined, sessionId)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
process.on('SIGINT', () => child.kill('SIGINT'))
|
|
120
|
-
process.on('SIGTERM', () => child.kill('SIGTERM'))
|
|
121
|
-
|
|
122
|
-
return await new Promise((resolve, reject) => {
|
|
123
|
-
child.once('error', async (error) => {
|
|
124
|
-
await cleanup()
|
|
125
|
-
reject(error)
|
|
126
|
-
})
|
|
127
|
-
child.once('exit', async (code, signal) => {
|
|
128
|
-
await cleanup()
|
|
129
|
-
if (signal) process.kill(process.pid, signal)
|
|
130
|
-
resolve(code || 0)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
module.exports = runClaude
|
package/src/commands/doctor.js
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hs doctor — 检查所有工具的配置状态
|
|
3
|
-
*/
|
|
4
|
-
const chalk = require('chalk')
|
|
5
|
-
const { execSync } = require('child_process')
|
|
6
|
-
const { getApiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, BASE_URL_CLAUDE_RELAY } = require('../utils/config')
|
|
7
|
-
const TOOLS = require('../tools')
|
|
8
|
-
|
|
9
|
-
async function doctor() {
|
|
10
|
-
console.log()
|
|
11
|
-
console.log(chalk.bold('🔍 HolySheep Doctor — 环境检查'))
|
|
12
|
-
console.log(chalk.gray('━'.repeat(50)))
|
|
13
|
-
console.log()
|
|
14
|
-
|
|
15
|
-
const nodeMajor = parseInt(process.version.slice(1), 10)
|
|
16
|
-
|
|
17
|
-
// Node.js 版本
|
|
18
|
-
const nodeVer = process.version
|
|
19
|
-
const nodeOk = nodeMajor >= 16
|
|
20
|
-
printCheck(nodeOk, `Node.js ${nodeVer}`, nodeOk ? '' : '需要 >= 16')
|
|
21
|
-
|
|
22
|
-
// API Key
|
|
23
|
-
const apiKey = getApiKey()
|
|
24
|
-
printCheck(!!apiKey, 'API Key', apiKey ? maskKey(apiKey) : `未设置 — 运行 ${chalk.cyan('hs setup')} 配置`)
|
|
25
|
-
|
|
26
|
-
// 环境变量
|
|
27
|
-
const envAnthropicKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
28
|
-
const envOpenAIKey = process.env.OPENAI_API_KEY
|
|
29
|
-
const envAnthropicUrl = process.env.ANTHROPIC_BASE_URL
|
|
30
|
-
const envOpenAIUrl = process.env.OPENAI_BASE_URL
|
|
31
|
-
|
|
32
|
-
console.log()
|
|
33
|
-
console.log(chalk.bold('环境变量:'))
|
|
34
|
-
printCheck(!!envAnthropicKey, 'ANTHROPIC_API_KEY / AUTH_TOKEN', envAnthropicKey ? maskKey(envAnthropicKey) : '未设置')
|
|
35
|
-
printCheck(!!envAnthropicUrl, 'ANTHROPIC_BASE_URL', envAnthropicUrl || '未设置')
|
|
36
|
-
printCheck(!!envOpenAIKey, 'OPENAI_API_KEY', envOpenAIKey ? maskKey(envOpenAIKey) : '未设置')
|
|
37
|
-
printCheck(!!envOpenAIUrl, 'OPENAI_BASE_URL', envOpenAIUrl || '未设置')
|
|
38
|
-
|
|
39
|
-
// 各工具检查
|
|
40
|
-
console.log()
|
|
41
|
-
console.log(chalk.bold('工具状态:'))
|
|
42
|
-
|
|
43
|
-
for (const tool of TOOLS) {
|
|
44
|
-
const installState = getInstallState(tool)
|
|
45
|
-
const installed = installState.installed
|
|
46
|
-
const configured = installed ? tool.isConfigured() : null
|
|
47
|
-
const version = installState.version
|
|
48
|
-
const suffix = installState.detail ? chalk.gray(` (${installState.detail})`) : ''
|
|
49
|
-
|
|
50
|
-
if (!installed) {
|
|
51
|
-
console.log(` ${chalk.gray('○')} ${chalk.gray(tool.name.padEnd(20))} ${chalk.gray('未安装')} ${chalk.gray(`— ${tool.installCmd}`)}`)
|
|
52
|
-
} else if (configured) {
|
|
53
|
-
console.log(` ${chalk.green('✓')} ${chalk.green(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')}${suffix} ${chalk.green('已配置 HolySheep')}`)
|
|
54
|
-
} else {
|
|
55
|
-
console.log(` ${chalk.yellow('!')} ${chalk.yellow(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')}${suffix} ${chalk.yellow('未配置')} ${chalk.gray('— 运行 hs setup')}`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (tool.id === 'openclaw' && installed) {
|
|
59
|
-
printOpenClawDetails(tool, installState, nodeMajor)
|
|
60
|
-
}
|
|
61
|
-
if (tool.id === 'claude-code' && installed && configured) {
|
|
62
|
-
printClaudeProcessProxyDetails(tool)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log()
|
|
67
|
-
|
|
68
|
-
// 连通性测试(可选)
|
|
69
|
-
if (apiKey) {
|
|
70
|
-
process.stdout.write(chalk.gray('测试 API 连通性... '))
|
|
71
|
-
try {
|
|
72
|
-
const fetch = require('node-fetch')
|
|
73
|
-
const res = await fetch(`${BASE_URL_ANTHROPIC}/v1/models`, {
|
|
74
|
-
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
75
|
-
timeout: 8000,
|
|
76
|
-
})
|
|
77
|
-
if (res.ok) {
|
|
78
|
-
const data = await res.json()
|
|
79
|
-
const count = data.data?.length || '?'
|
|
80
|
-
console.log(chalk.green(`✓ 连通 (${count} 个模型可用)`))
|
|
81
|
-
} else {
|
|
82
|
-
console.log(chalk.red(`✗ 失败 (HTTP ${res.status})`))
|
|
83
|
-
}
|
|
84
|
-
} catch (e) {
|
|
85
|
-
console.log(chalk.red(`✗ 连接失败: ${e.message}`))
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function printCheck(ok, label, detail = '') {
|
|
93
|
-
const icon = ok ? chalk.green('✓') : chalk.red('✗')
|
|
94
|
-
const lbl = ok ? chalk.green(label.padEnd(35)) : chalk.red(label.padEnd(35))
|
|
95
|
-
const det = detail ? chalk.gray(detail) : ''
|
|
96
|
-
console.log(` ${icon} ${lbl} ${det}`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
100
|
-
const details = []
|
|
101
|
-
const gatewayPort = typeof tool.getGatewayPort === 'function' ? tool.getGatewayPort() : 18789
|
|
102
|
-
const bridgePort = typeof tool.getBridgePort === 'function' ? tool.getBridgePort() : null
|
|
103
|
-
const primaryModel = typeof tool.getPrimaryModel === 'function' ? tool.getPrimaryModel() : ''
|
|
104
|
-
const primaryModelRoute = typeof tool.getPrimaryModelRoute === 'function' ? tool.getPrimaryModelRoute() : ''
|
|
105
|
-
const listeners = typeof tool.getPortListeners === 'function' ? tool.getPortListeners(gatewayPort) : []
|
|
106
|
-
const foreignListeners = listeners.filter((item) => !String(item.command || '').toLowerCase().includes('openclaw'))
|
|
107
|
-
const launchAgent = typeof tool.getLaunchAgentDiagnosis === 'function' ? tool.getLaunchAgentDiagnosis() : null
|
|
108
|
-
const launchHint = Array.isArray(tool.launchSteps) && tool.launchSteps.length > 1
|
|
109
|
-
? tool.launchSteps[1].cmd
|
|
110
|
-
: tool.launchCmd || 'npx openclaw gateway --port <port>'
|
|
111
|
-
|
|
112
|
-
if (installState.detail === 'npx fallback') {
|
|
113
|
-
details.push({
|
|
114
|
-
level: 'info',
|
|
115
|
-
text: '未检测到全局 openclaw,当前将通过 npx 运行',
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
details.push(
|
|
120
|
-
nodeMajor >= 20
|
|
121
|
-
? { level: 'ok', text: `OpenClaw Node 版本要求满足(当前 ${process.version})` }
|
|
122
|
-
: { level: 'warn', text: `OpenClaw 建议 Node.js >= 20(当前 ${process.version})` }
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if (primaryModel) {
|
|
126
|
-
details.push({
|
|
127
|
-
level: 'info',
|
|
128
|
-
text: `当前默认模型:${primaryModel}`,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (primaryModelRoute) {
|
|
133
|
-
details.push({
|
|
134
|
-
level: 'info',
|
|
135
|
-
text: `当前默认模型路由:${primaryModelRoute}`,
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (bridgePort) {
|
|
140
|
-
details.push({
|
|
141
|
-
level: 'info',
|
|
142
|
-
text: `Bridge 端口:${bridgePort}`,
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (bridgePort && bridgePort === gatewayPort) {
|
|
147
|
-
details.push({
|
|
148
|
-
level: 'warn',
|
|
149
|
-
text: `Bridge 端口和 Gateway 端口都被配置成了 ${gatewayPort};这会让 OpenClaw provider 请求打回 Gateway 自己并触发 404`,
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (launchAgent?.unstable) {
|
|
154
|
-
details.push({
|
|
155
|
-
level: 'warn',
|
|
156
|
-
text: `检测到失效的 LaunchAgent,引用了临时 npx 缓存路径:${launchAgent.unstableArg}`,
|
|
157
|
-
})
|
|
158
|
-
} else if (launchAgent?.exists) {
|
|
159
|
-
details.push({
|
|
160
|
-
level: 'ok',
|
|
161
|
-
text: `LaunchAgent 配置存在:${launchAgent.path}`,
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (foreignListeners.length) {
|
|
166
|
-
const occupiedBy = foreignListeners
|
|
167
|
-
.slice(0, 2)
|
|
168
|
-
.map((item) => `${item.command}(${item.pid})`)
|
|
169
|
-
.join(', ')
|
|
170
|
-
details.push({
|
|
171
|
-
level: 'warn',
|
|
172
|
-
text: `Gateway 端口 ${gatewayPort} 被其他进程占用:${occupiedBy}`,
|
|
173
|
-
})
|
|
174
|
-
} else if (listeners.length) {
|
|
175
|
-
details.push({
|
|
176
|
-
level: 'ok',
|
|
177
|
-
text: `Gateway 端口 ${gatewayPort} 当前由 OpenClaw 占用`,
|
|
178
|
-
})
|
|
179
|
-
} else {
|
|
180
|
-
details.push({
|
|
181
|
-
level: launchAgent?.unstable ? 'warn' : 'info',
|
|
182
|
-
text: launchAgent?.unstable
|
|
183
|
-
? `Gateway 端口 ${gatewayPort} 当前未监听;这通常会导致浏览器只剩空白/黑屏窗口`
|
|
184
|
-
: `Gateway 端口 ${gatewayPort} 当前空闲;如刚完成配置,可运行 ${launchHint}`,
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
details.forEach((detail) => {
|
|
189
|
-
const icon = detail.level === 'ok'
|
|
190
|
-
? chalk.green('↳')
|
|
191
|
-
: detail.level === 'warn'
|
|
192
|
-
? chalk.yellow('↳')
|
|
193
|
-
: chalk.gray('↳')
|
|
194
|
-
const text = detail.level === 'ok'
|
|
195
|
-
? chalk.green(detail.text)
|
|
196
|
-
: detail.level === 'warn'
|
|
197
|
-
? chalk.yellow(detail.text)
|
|
198
|
-
: chalk.gray(detail.text)
|
|
199
|
-
console.log(` ${icon} ${text}`)
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function printClaudeProcessProxyDetails(tool) {
|
|
204
|
-
const proxyConfig = typeof tool.getProcessProxyConfig === 'function' ? tool.getProcessProxyConfig() : {}
|
|
205
|
-
const runtime = typeof tool.detectClaudeRuntime === 'function'
|
|
206
|
-
? tool.detectClaudeRuntime()
|
|
207
|
-
: { display: '未知', kind: 'unknown', launchMode: 'unknown', path: null }
|
|
208
|
-
const relayUrl = proxyConfig.controlPlaneUrl || proxyConfig.relayUrl || BASE_URL_CLAUDE_RELAY
|
|
209
|
-
const mode = proxyConfig.proxyMode || 'unknown'
|
|
210
|
-
const hasBridgeSecret = Boolean(proxyConfig.bridgeSecret)
|
|
211
|
-
const hasBridgeIds = Boolean(proxyConfig.bridgeId && proxyConfig.deviceId)
|
|
212
|
-
const hasProcessPort = Boolean(proxyConfig.processProxyPort)
|
|
213
|
-
const launchPath = runtime.launchMode === 'node-inject'
|
|
214
|
-
? 'local-api + connect-fallback + node-inject'
|
|
215
|
-
: 'local-api + connect-fallback'
|
|
216
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 启动方式:hs claude`)}`)
|
|
217
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 代理模式:${mode}`)}`)
|
|
218
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 可执行类型:${runtime.display}`)}`)
|
|
219
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 启动代理路径:${launchPath}`)}`)
|
|
220
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude Relay: ${relayUrl || '未配置'}`)}`)
|
|
221
|
-
console.log(` ${hasBridgeSecret ? chalk.green('↳') : chalk.yellow('↳')} ${hasBridgeSecret ? chalk.green('Bridge secret 已配置') : chalk.yellow('Bridge secret 缺失')}`)
|
|
222
|
-
console.log(` ${hasBridgeIds ? chalk.green('↳') : chalk.yellow('↳')} ${hasBridgeIds ? chalk.green('Bridge ID / Device ID 已配置') : chalk.yellow('Bridge ID / Device ID 缺失')}`)
|
|
223
|
-
console.log(` ${hasProcessPort ? chalk.green('↳') : chalk.yellow('↳')} ${hasProcessPort ? chalk.green(`Claude process proxy 端口:${proxyConfig.processProxyPort}`) : chalk.yellow('Claude process proxy 端口缺失')}`)
|
|
224
|
-
if (hasProcessPort) {
|
|
225
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 本地 API 入口:http://127.0.0.1:${proxyConfig.processProxyPort}`)}`)
|
|
226
|
-
}
|
|
227
|
-
if (runtime.path) {
|
|
228
|
-
console.log(` ${chalk.gray('↳')} ${chalk.gray(`Claude 路径:${runtime.path}`)}`)
|
|
229
|
-
}
|
|
230
|
-
const nodeMajor = parseInt(process.version.slice(1), 10)
|
|
231
|
-
if (nodeMajor > 22) {
|
|
232
|
-
console.log(` ${chalk.yellow('↳')} ${chalk.yellow(`当前 hs 运行在 ${process.version};Claude 代理更建议使用 Node 20/22 LTS`)}`)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function maskKey(key) {
|
|
237
|
-
if (!key || key.length < 8) return '****'
|
|
238
|
-
return key.slice(0, 6) + '...' + key.slice(-4)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getInstallState(tool) {
|
|
242
|
-
if (tool.id === 'openclaw' && typeof tool.detectRuntime === 'function') {
|
|
243
|
-
const runtime = tool.detectRuntime()
|
|
244
|
-
return {
|
|
245
|
-
installed: runtime.available,
|
|
246
|
-
version: runtime.version,
|
|
247
|
-
detail: runtime.via === 'npx' ? 'npx fallback' : '',
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const installed = tool.checkInstalled()
|
|
252
|
-
return {
|
|
253
|
-
installed,
|
|
254
|
-
version: installed ? getVersion(tool) : null,
|
|
255
|
-
detail: '',
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getVersion(tool) {
|
|
260
|
-
if (typeof tool.getVersion === 'function') {
|
|
261
|
-
return tool.getVersion()
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const cmds = {
|
|
265
|
-
'claude-code': 'claude --version',
|
|
266
|
-
'codex': 'codex --version',
|
|
267
|
-
'droid': 'droid --version',
|
|
268
|
-
'gemini-cli': 'gemini --version',
|
|
269
|
-
'opencode': 'opencode --version',
|
|
270
|
-
'openclaw': 'openclaw --version',
|
|
271
|
-
'aider': 'aider --version',
|
|
272
|
-
}
|
|
273
|
-
const cmd = cmds[tool.id]
|
|
274
|
-
if (!cmd) return null
|
|
275
|
-
try {
|
|
276
|
-
return execSync(cmd, { stdio: 'pipe' }).toString().trim().split('\n')[0].slice(0, 30)
|
|
277
|
-
} catch {
|
|
278
|
-
return null
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
module.exports = doctor
|
package/src/commands/login.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hs login — 登录并保存 API Key
|
|
3
|
-
* hs logout — 清除本地 API Key
|
|
4
|
-
* hs whoami — 显示当前登录状态
|
|
5
|
-
*/
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
const inquirer = require('inquirer')
|
|
9
|
-
const chalk = require('chalk')
|
|
10
|
-
const ora = require('ora')
|
|
11
|
-
const fetch = require('node-fetch')
|
|
12
|
-
const { execSync } = require('child_process')
|
|
13
|
-
const { loadConfig, saveConfig, getApiKey, BASE_URL_OPENAI, SHOP_URL, CONFIG_FILE } = require('../utils/config')
|
|
14
|
-
const fs = require('fs')
|
|
15
|
-
|
|
16
|
-
const MODELS_URL = `${BASE_URL_OPENAI}/models`
|
|
17
|
-
|
|
18
|
-
function maskKey(key) {
|
|
19
|
-
if (!key || key.length < 8) return '****'
|
|
20
|
-
return key.slice(0, 8) + '...' + key.slice(-4)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 调用 /v1/models 验证 API Key 是否有效
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
async function validateApiKey(apiKey) {
|
|
28
|
-
const res = await fetch(MODELS_URL, {
|
|
29
|
-
method: 'GET',
|
|
30
|
-
headers: {
|
|
31
|
-
Authorization: `Bearer ${apiKey}`,
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
},
|
|
34
|
-
timeout: 10000,
|
|
35
|
-
})
|
|
36
|
-
return res.status === 200
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── login ────────────────────────────────────────────────────────────────────
|
|
40
|
-
async function login() {
|
|
41
|
-
console.log()
|
|
42
|
-
console.log(chalk.bold('🐑 HolySheep — 登录'))
|
|
43
|
-
console.log(chalk.gray('━'.repeat(50)))
|
|
44
|
-
console.log()
|
|
45
|
-
|
|
46
|
-
// 检查是否已登录
|
|
47
|
-
const existing = getApiKey()
|
|
48
|
-
if (existing) {
|
|
49
|
-
console.log(`${chalk.green('✓')} 已登录,当前 API Key: ${chalk.cyan(maskKey(existing))}`)
|
|
50
|
-
const { relogin } = await inquirer.prompt([{
|
|
51
|
-
type: 'confirm',
|
|
52
|
-
name: 'relogin',
|
|
53
|
-
message: '是否使用新的 API Key 重新登录?',
|
|
54
|
-
default: false,
|
|
55
|
-
}])
|
|
56
|
-
if (!relogin) {
|
|
57
|
-
console.log(chalk.gray('保持当前登录,退出。'))
|
|
58
|
-
console.log()
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
console.log()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 提示用户获取 key 的方式
|
|
65
|
-
console.log(chalk.cyan('获取 API Key 的方式:'))
|
|
66
|
-
console.log(` ${chalk.bold('a)')} 输入已有的 API Key (cr_xxx)`)
|
|
67
|
-
console.log(` ${chalk.bold('b)')} 打开浏览器前往 ${chalk.cyan(SHOP_URL)} 注册`)
|
|
68
|
-
console.log()
|
|
69
|
-
|
|
70
|
-
const { choice } = await inquirer.prompt([{
|
|
71
|
-
type: 'list',
|
|
72
|
-
name: 'choice',
|
|
73
|
-
message: '请选择:',
|
|
74
|
-
choices: [
|
|
75
|
-
{ name: '输入已有的 API Key', value: 'input' },
|
|
76
|
-
{ name: `打开浏览器注册 (${SHOP_URL})`, value: 'browser' },
|
|
77
|
-
],
|
|
78
|
-
}])
|
|
79
|
-
|
|
80
|
-
if (choice === 'browser') {
|
|
81
|
-
console.log(chalk.gray(`\n正在打开浏览器: ${SHOP_URL}`))
|
|
82
|
-
try {
|
|
83
|
-
const platform = process.platform
|
|
84
|
-
if (platform === 'darwin') execSync(`open "${SHOP_URL}"`)
|
|
85
|
-
else if (platform === 'win32') execSync(`start "" "${SHOP_URL}"`)
|
|
86
|
-
else execSync(`xdg-open "${SHOP_URL}"`)
|
|
87
|
-
} catch {
|
|
88
|
-
console.log(chalk.yellow(`无法自动打开浏览器,请手动访问: ${SHOP_URL}`))
|
|
89
|
-
}
|
|
90
|
-
console.log()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 输入 API Key
|
|
94
|
-
const { apiKey } = await inquirer.prompt([{
|
|
95
|
-
type: 'password',
|
|
96
|
-
name: 'apiKey',
|
|
97
|
-
message: '请输入 API Key (cr_xxx):',
|
|
98
|
-
validate: v => {
|
|
99
|
-
if (!v || !v.trim()) return '请输入 API Key'
|
|
100
|
-
if (!v.trim().startsWith('cr_')) return '请输入以 cr_ 开头的 API Key'
|
|
101
|
-
return true
|
|
102
|
-
},
|
|
103
|
-
}])
|
|
104
|
-
|
|
105
|
-
const key = apiKey.trim()
|
|
106
|
-
|
|
107
|
-
// 验证 API Key
|
|
108
|
-
const spinner = ora('正在验证 API Key...').start()
|
|
109
|
-
try {
|
|
110
|
-
const valid = await validateApiKey(key)
|
|
111
|
-
if (!valid) {
|
|
112
|
-
spinner.fail(chalk.red('API Key 无效,请检查后重试'))
|
|
113
|
-
console.log(chalk.gray(` 前往获取有效 Key: ${SHOP_URL}`))
|
|
114
|
-
console.log()
|
|
115
|
-
process.exit(1)
|
|
116
|
-
}
|
|
117
|
-
} catch (e) {
|
|
118
|
-
spinner.fail(`验证失败: ${e.message}`)
|
|
119
|
-
console.log(chalk.yellow(' 网络异常,请检查网络连接后重试'))
|
|
120
|
-
console.log()
|
|
121
|
-
process.exit(1)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 保存
|
|
125
|
-
saveConfig({ apiKey: key, savedAt: new Date().toISOString() })
|
|
126
|
-
spinner.succeed(chalk.green('API Key 验证成功并已保存!'))
|
|
127
|
-
|
|
128
|
-
console.log()
|
|
129
|
-
console.log(` Key: ${chalk.cyan(maskKey(key))}`)
|
|
130
|
-
console.log(` 配置文件: ${chalk.gray(CONFIG_FILE)}`)
|
|
131
|
-
console.log()
|
|
132
|
-
console.log(chalk.bold('接下来:'))
|
|
133
|
-
console.log(` 运行 ${chalk.cyan('hs setup')} 一键配置 AI 工具`)
|
|
134
|
-
console.log(` 运行 ${chalk.cyan('hs whoami')} 查看登录状态`)
|
|
135
|
-
console.log()
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── logout ───────────────────────────────────────────────────────────────────
|
|
139
|
-
async function logout() {
|
|
140
|
-
console.log()
|
|
141
|
-
const existing = getApiKey()
|
|
142
|
-
if (!existing) {
|
|
143
|
-
console.log(chalk.yellow('当前未登录(无本地 API Key)'))
|
|
144
|
-
console.log()
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { confirm } = await inquirer.prompt([{
|
|
149
|
-
type: 'confirm',
|
|
150
|
-
name: 'confirm',
|
|
151
|
-
message: `确认退出登录?将删除本地 API Key (${maskKey(existing)})`,
|
|
152
|
-
default: false,
|
|
153
|
-
}])
|
|
154
|
-
|
|
155
|
-
if (!confirm) {
|
|
156
|
-
console.log(chalk.gray('取消,保持当前登录。'))
|
|
157
|
-
console.log()
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const config = loadConfig()
|
|
163
|
-
delete config.apiKey
|
|
164
|
-
delete config.savedAt
|
|
165
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
166
|
-
console.log(chalk.green('✓ 已退出登录,本地 API Key 已清除'))
|
|
167
|
-
} catch (e) {
|
|
168
|
-
console.log(chalk.red(`退出失败: ${e.message}`))
|
|
169
|
-
}
|
|
170
|
-
console.log()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ── whoami ───────────────────────────────────────────────────────────────────
|
|
174
|
-
async function whoami() {
|
|
175
|
-
console.log()
|
|
176
|
-
console.log(chalk.bold('🐑 HolySheep — 登录状态'))
|
|
177
|
-
console.log(chalk.gray('━'.repeat(50)))
|
|
178
|
-
console.log()
|
|
179
|
-
|
|
180
|
-
const apiKey = getApiKey()
|
|
181
|
-
if (!apiKey) {
|
|
182
|
-
console.log(chalk.yellow('未登录 — 本地无 API Key'))
|
|
183
|
-
console.log(chalk.gray(`运行 ${chalk.cyan('hs login')} 登录`))
|
|
184
|
-
console.log()
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
console.log(`状态: ${chalk.green('● 已登录')}`)
|
|
189
|
-
console.log(`Key: ${chalk.cyan(maskKey(apiKey))}`)
|
|
190
|
-
|
|
191
|
-
const config = loadConfig()
|
|
192
|
-
if (config.savedAt) {
|
|
193
|
-
console.log(`保存时间: ${chalk.gray(new Date(config.savedAt).toLocaleString())}`)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 验证 key 是否仍然有效
|
|
197
|
-
const spinner = ora('验证 Key 有效性...').start()
|
|
198
|
-
try {
|
|
199
|
-
const valid = await validateApiKey(apiKey)
|
|
200
|
-
if (valid) {
|
|
201
|
-
spinner.succeed(chalk.green('Key 有效'))
|
|
202
|
-
} else {
|
|
203
|
-
spinner.fail(chalk.red('Key 已失效,请重新登录 (hs login)'))
|
|
204
|
-
}
|
|
205
|
-
} catch (e) {
|
|
206
|
-
spinner.warn(`无法验证(网络异常): ${e.message}`)
|
|
207
|
-
}
|
|
208
|
-
console.log()
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
module.exports = { login, logout, whoami }
|