@simonyea/holysheep-cli 2.1.40 → 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 +20 -7
  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 -176
  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 -1572
  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
@@ -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
@@ -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
@@ -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 }