bhg-helper 1.0.6 → 1.0.7
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/cli/cli.js +235 -316
- package/package.json +1 -78
- package/dist/assets/index-C4x5glYN.js +0 -136
- package/dist/assets/index-CQrGCyBr.css +0 -1
- package/dist/favicon.svg +0 -4
- package/dist/index.html +0 -20
- package/index.html +0 -19
- package/nodemon.json +0 -10
- package/postcss.config.js +0 -10
- package/src/App.tsx +0 -33
- package/src/assets/react.svg +0 -1
- package/src/components/ConsolePanel.tsx +0 -44
- package/src/components/Empty.tsx +0 -8
- package/src/components/ErrorBoundary.tsx +0 -53
- package/src/components/Layout.tsx +0 -17
- package/src/components/Page.tsx +0 -130
- package/src/components/Sidebar.tsx +0 -53
- package/src/hooks/useTheme.ts +0 -29
- package/src/index.css +0 -1350
- package/src/lib/api.ts +0 -103
- package/src/lib/store.ts +0 -165
- package/src/lib/types.ts +0 -117
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
- package/src/pages/ConsolePage.tsx +0 -47
- package/src/pages/Dashboard.tsx +0 -101
- package/src/pages/Relay.tsx +0 -408
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.js +0 -13
- package/tsconfig.json +0 -40
- package/vite.config.ts +0 -28
package/cli/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { select, confirm, input } from '@inquirer/prompts'
|
|
8
|
-
import { execSync
|
|
8
|
+
import { execSync } from 'node:child_process'
|
|
9
9
|
import os from 'node:os'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import fs from 'node:fs'
|
|
@@ -24,44 +24,37 @@ const C = {
|
|
|
24
24
|
const theme = { prefix: `${C.cyan}?${C.r}` }
|
|
25
25
|
|
|
26
26
|
// ── 常量 ──────────────────────────────────────────────────────
|
|
27
|
-
const VERSION = '1.0.
|
|
27
|
+
const VERSION = '1.0.6'
|
|
28
28
|
const RELAY_PORT = 8787
|
|
29
29
|
const API_URL = 'http://127.0.0.1:3001'
|
|
30
30
|
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
const BHG_DIR = path.join(os.homedir(), '.bhg-helper')
|
|
32
|
+
const RELAY_CFG = path.join(BHG_DIR, 'relay.config.json')
|
|
33
|
+
const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json')
|
|
34
|
+
|
|
35
|
+
const DEFAULT_CONFIG = {
|
|
36
|
+
port: RELAY_PORT,
|
|
37
|
+
deepseekBaseUrl: 'https://api.deepseek.com',
|
|
38
|
+
modelMap: {
|
|
39
|
+
'deepseek-v4-pro': 'deepseek-v4-pro',
|
|
40
|
+
'deepseek-chat': 'deepseek-v4-pro',
|
|
41
|
+
},
|
|
42
|
+
spoofProvider: 'deepseek',
|
|
43
|
+
verbose: true,
|
|
44
|
+
}
|
|
41
45
|
|
|
42
|
-
// ── ANSI
|
|
46
|
+
// ── ANSI 渐变 Logo(蓝 → 紫 → 粉)────────────────────────────
|
|
43
47
|
function rgb(r, g, b) {
|
|
44
48
|
return `\x1b[38;2;${r};${g};${b}m`
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
function colorLine(row, totalRows = 10) {
|
|
49
|
-
const t = row / (totalRows - 1)
|
|
50
|
+
function logoColor(row, total = 10) {
|
|
51
|
+
const t = row / (total - 1)
|
|
50
52
|
if (t < 0.5) {
|
|
51
53
|
const p = t * 2
|
|
52
|
-
return rgb(
|
|
53
|
-
Math.round(80 + p * 70),
|
|
54
|
-
Math.round(180 - p * 40),
|
|
55
|
-
Math.round(255)
|
|
56
|
-
)
|
|
57
|
-
} else {
|
|
58
|
-
const p = (t - 0.5) * 2
|
|
59
|
-
return rgb(
|
|
60
|
-
Math.round(150 + p * 95),
|
|
61
|
-
Math.round(140 - p * 70),
|
|
62
|
-
Math.round(255)
|
|
63
|
-
)
|
|
54
|
+
return rgb(80 + p * 70, 180 - p * 40, 255)
|
|
64
55
|
}
|
|
56
|
+
const p = (t - 0.5) * 2
|
|
57
|
+
return rgb(150 + p * 95, 140 - p * 70, 255)
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
const BHG_LOGO = [
|
|
@@ -72,7 +65,6 @@ const BHG_LOGO = [
|
|
|
72
65
|
'██████╔╝ ██║ ██║ ███████║',
|
|
73
66
|
'╚═════╝ ╚═╝ ╚═╝ ╚══════╝',
|
|
74
67
|
]
|
|
75
|
-
|
|
76
68
|
const HELPER_LOGO = [
|
|
77
69
|
'██╗ ██╗ ███████╗ ██╗ ██████╗ ███████╗ ██████╗ ',
|
|
78
70
|
'██║ ██║ ██╔════╝ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗',
|
|
@@ -85,9 +77,7 @@ const HELPER_LOGO = [
|
|
|
85
77
|
function printLogo() {
|
|
86
78
|
const gap = ' '
|
|
87
79
|
for (let i = 0; i < BHG_LOGO.length; i++) {
|
|
88
|
-
console.log(
|
|
89
|
-
`${colorLine(i)}${C.bold}${BHG_LOGO[i]}${gap}${HELPER_LOGO[i]}${C.r}`
|
|
90
|
-
)
|
|
80
|
+
console.log(`${logoColor(i)}${C.bold}${BHG_LOGO[i]}${gap}${HELPER_LOGO[i]}${C.r}`)
|
|
91
81
|
}
|
|
92
82
|
console.log()
|
|
93
83
|
}
|
|
@@ -95,207 +85,170 @@ function printLogo() {
|
|
|
95
85
|
// ── 工具定义 ──────────────────────────────────────────────────
|
|
96
86
|
const TOOLS = {
|
|
97
87
|
claude: {
|
|
98
|
-
name: 'Claude Code',
|
|
99
|
-
cmd: 'claude',
|
|
100
|
-
desc: 'Anthropic 官方 CLI 编程助手',
|
|
88
|
+
name: 'Claude Code', cmd: 'claude', desc: 'Anthropic 官方 CLI 编程助手',
|
|
101
89
|
install: 'npm install -g @anthropic-ai/claude-code',
|
|
102
90
|
},
|
|
103
91
|
gemini: {
|
|
104
|
-
name: 'Gemini CLI',
|
|
105
|
-
cmd: 'gemini',
|
|
106
|
-
desc: 'Google 官方 CLI 编程助手',
|
|
92
|
+
name: 'Gemini CLI', cmd: 'gemini', desc: 'Google 官方 CLI 编程助手',
|
|
107
93
|
install: 'npm install -g @anthropic-ai/gemini-cli',
|
|
108
94
|
},
|
|
109
95
|
opencode: {
|
|
110
|
-
name: 'OpenCode',
|
|
111
|
-
cmd: 'opencode',
|
|
112
|
-
desc: '开源 AI 编程助手',
|
|
96
|
+
name: 'OpenCode', cmd: 'opencode', desc: '开源 AI 编程助手',
|
|
113
97
|
install: 'npm install -g opencode-ai',
|
|
114
|
-
configFile: (()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
98
|
+
configFile: path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', 'opencode.json'),
|
|
99
|
+
},
|
|
100
|
+
codex: {
|
|
101
|
+
name: 'OpenAI Codex CLI', cmd: 'codex', desc: 'OpenAI 官方 CLI 编程助手',
|
|
102
|
+
install: 'npm install -g @openai/codex',
|
|
118
103
|
},
|
|
119
104
|
cursor: {
|
|
120
|
-
name: 'Cursor',
|
|
121
|
-
cmd: null,
|
|
122
|
-
desc: 'AI 代码编辑器',
|
|
123
|
-
install: '从 https://cursor.com 下载',
|
|
105
|
+
name: 'Cursor', cmd: null, desc: 'AI 代码编辑器', install: '从 https://cursor.com 下载',
|
|
124
106
|
},
|
|
125
107
|
continue: {
|
|
126
|
-
name: 'Continue (VS Code)',
|
|
127
|
-
cmd: null,
|
|
128
|
-
desc: 'VS Code / JetBrains AI 扩展',
|
|
108
|
+
name: 'Continue (VS Code)', cmd: null, desc: 'VS Code / JetBrains AI 扩展',
|
|
129
109
|
install: 'VS Code 扩展搜索 "Continue"',
|
|
130
|
-
configFile: path.join(os.homedir(), '.continue', 'config.json'),
|
|
131
110
|
},
|
|
132
111
|
trae: {
|
|
133
|
-
name: 'Trae',
|
|
134
|
-
cmd: null,
|
|
135
|
-
desc: '字节跳动 AI 代码编辑器',
|
|
136
|
-
install: '从 https://trae.ai 下载',
|
|
137
|
-
},
|
|
138
|
-
codex: {
|
|
139
|
-
name: 'OpenAI Codex CLI',
|
|
140
|
-
cmd: 'codex',
|
|
141
|
-
desc: 'OpenAI 官方 CLI 编程助手',
|
|
142
|
-
install: 'npm install -g @openai/codex',
|
|
112
|
+
name: 'Trae', cmd: null, desc: '字节跳动 AI 代码编辑器', install: '从 https://trae.ai 下载',
|
|
143
113
|
},
|
|
144
114
|
}
|
|
145
115
|
|
|
146
|
-
|
|
116
|
+
/** 有命令行的 CLI 工具,用于头部展示 */
|
|
117
|
+
const CLI_TOOLS = Object.values(TOOLS).filter(t => t.cmd)
|
|
118
|
+
|
|
119
|
+
// ── 配置读写 ──────────────────────────────────────────────────
|
|
147
120
|
function loadConfig() {
|
|
148
121
|
try {
|
|
149
|
-
if (fs.existsSync(
|
|
150
|
-
return JSON.parse(fs.readFileSync(RELAY_CONFIG_FILE, 'utf-8'))
|
|
151
|
-
}
|
|
122
|
+
if (fs.existsSync(RELAY_CFG)) return JSON.parse(fs.readFileSync(RELAY_CFG, 'utf-8'))
|
|
152
123
|
} catch { /* */ }
|
|
153
124
|
return null
|
|
154
125
|
}
|
|
155
126
|
|
|
156
127
|
function saveConfig(cfg) {
|
|
157
|
-
fs.mkdirSync(
|
|
158
|
-
fs.writeFileSync(
|
|
128
|
+
fs.mkdirSync(BHG_DIR, { recursive: true })
|
|
129
|
+
fs.writeFileSync(RELAY_CFG, JSON.stringify(cfg, null, 2) + '\n', 'utf-8')
|
|
159
130
|
}
|
|
160
131
|
|
|
161
|
-
function
|
|
132
|
+
function hasApiKey() {
|
|
162
133
|
const cfg = loadConfig()
|
|
163
|
-
return !!(cfg
|
|
134
|
+
return !!(cfg?.deepseekApiKey?.length > 0)
|
|
164
135
|
}
|
|
165
136
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
if (!cfg) return 'deepseek-v4-pro'
|
|
169
|
-
return cfg.currentModel || 'deepseek-v4-pro'
|
|
137
|
+
function currentModel() {
|
|
138
|
+
return loadConfig()?.currentModel || 'deepseek-v4-pro'
|
|
170
139
|
}
|
|
171
140
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
execSync(`where ${cmd}`, { stdio: 'pipe' })
|
|
177
|
-
return true
|
|
178
|
-
} catch { return false }
|
|
141
|
+
function maskedKey() {
|
|
142
|
+
const k = loadConfig()?.deepseekApiKey || ''
|
|
143
|
+
return k.length > 8 ? k.slice(0, 4) + '****' + k.slice(-4) : k || '(empty)'
|
|
179
144
|
}
|
|
180
145
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
146
|
+
// ── 辅助函数 ──────────────────────────────────────────────────
|
|
147
|
+
function isInstalled(cmd) {
|
|
148
|
+
if (!cmd) return null
|
|
149
|
+
try { execSync(`where ${cmd}`, { stdio: 'pipe' }); return true }
|
|
150
|
+
catch { return false }
|
|
184
151
|
}
|
|
185
152
|
|
|
186
153
|
async function apiGet(pathname) {
|
|
187
154
|
try {
|
|
188
|
-
const
|
|
189
|
-
const timer = setTimeout(() =>
|
|
190
|
-
const res = await fetch(`${API_URL}${pathname}`, { signal:
|
|
155
|
+
const ac = new AbortController()
|
|
156
|
+
const timer = setTimeout(() => ac.abort(), 800)
|
|
157
|
+
const res = await fetch(`${API_URL}${pathname}`, { signal: ac.signal })
|
|
191
158
|
clearTimeout(timer)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} catch {
|
|
195
|
-
return null
|
|
196
|
-
}
|
|
159
|
+
return res.ok ? await res.json().catch(() => null) : null
|
|
160
|
+
} catch { return null }
|
|
197
161
|
}
|
|
198
162
|
|
|
199
|
-
async function
|
|
163
|
+
async function apiPost(pathname) {
|
|
200
164
|
try {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
165
|
+
const res = await fetch(`${API_URL}${pathname}`, { method: 'POST' })
|
|
166
|
+
return res.ok
|
|
167
|
+
} catch { return false }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function healthCheck() {
|
|
171
|
+
try {
|
|
172
|
+
const ac = new AbortController()
|
|
173
|
+
const timer = setTimeout(() => ac.abort(), 800)
|
|
174
|
+
const res = await fetch(`${API_URL}/api/health`, { signal: ac.signal })
|
|
204
175
|
clearTimeout(timer)
|
|
205
176
|
return res.ok
|
|
206
|
-
} catch {
|
|
207
|
-
return false
|
|
208
|
-
}
|
|
177
|
+
} catch { return false }
|
|
209
178
|
}
|
|
210
179
|
|
|
211
180
|
async function ensureBackend() {
|
|
212
|
-
if (await
|
|
181
|
+
if (await healthCheck()) return true
|
|
213
182
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
shell: true,
|
|
219
|
-
})
|
|
220
|
-
} else {
|
|
221
|
-
const child = spawn('npm', ['run', 'server:raw'], {
|
|
222
|
-
cwd: PROJECT_ROOT,
|
|
223
|
-
detached: true,
|
|
224
|
-
stdio: 'ignore',
|
|
225
|
-
shell: false,
|
|
226
|
-
})
|
|
227
|
-
child.unref()
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
return false
|
|
231
|
-
}
|
|
232
|
-
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
233
|
-
return await apiHealth()
|
|
183
|
+
execSync('start "" /min cmd /c "npm run server:raw"', { cwd: PROJECT_ROOT, stdio: 'ignore', shell: true })
|
|
184
|
+
} catch { return false }
|
|
185
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
186
|
+
return await healthCheck()
|
|
234
187
|
}
|
|
235
188
|
|
|
236
189
|
async function ensureRelay() {
|
|
237
|
-
|
|
238
|
-
return await apiPost('/api/relay/start')
|
|
190
|
+
return (await ensureBackend()) && await apiPost('/api/relay/start')
|
|
239
191
|
}
|
|
240
192
|
|
|
241
|
-
// ──
|
|
193
|
+
// ── 头部(ArkHelper 风格)──────────────────────────────────────
|
|
242
194
|
async function printHeader() {
|
|
243
195
|
console.clear()
|
|
244
196
|
printLogo()
|
|
245
|
-
|
|
246
197
|
console.log(` v${VERSION} · DeepSeek LLM bridge for your AI coding tools`)
|
|
247
198
|
console.log()
|
|
248
199
|
console.log(` ${C.bold}一键配置${C.r}`)
|
|
249
|
-
for (const t of
|
|
200
|
+
for (const t of CLI_TOOLS) {
|
|
250
201
|
console.log(` ${C.cyan}◆${C.r} ${t.name} ${C.dim}— ${t.desc}${C.r}`)
|
|
251
202
|
}
|
|
252
203
|
console.log()
|
|
253
204
|
|
|
254
|
-
const apiOk =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
205
|
+
const [apiOk, backendOk, relayJson] = await Promise.all([
|
|
206
|
+
Promise.resolve(hasApiKey()),
|
|
207
|
+
healthCheck(),
|
|
208
|
+
apiGet('/api/relay/status'),
|
|
209
|
+
])
|
|
210
|
+
const relayOk = !!(relayJson?.data?.running)
|
|
258
211
|
|
|
259
212
|
console.log(
|
|
260
|
-
` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓'
|
|
261
|
-
+ ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓'
|
|
262
|
-
+ ` ${C.dim}Relay:${C.r} ${relayOk ? C.green + '✓'
|
|
213
|
+
` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓' : C.red + '✗'}${C.r}`
|
|
214
|
+
+ ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓' : C.red + '✗'}${C.r}`
|
|
215
|
+
+ ` ${C.dim}Relay:${C.r} ${relayOk ? C.green + '✓' : C.red + '✗'}${C.r}`
|
|
263
216
|
)
|
|
264
|
-
|
|
265
217
|
console.log()
|
|
266
218
|
console.log(` ${C.dim}powered by BH6BHG${C.r}`)
|
|
267
219
|
console.log()
|
|
268
220
|
}
|
|
269
221
|
|
|
222
|
+
// ── 菜单分隔线 ────────────────────────────────────────────────
|
|
223
|
+
const SEP = { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true }
|
|
224
|
+
|
|
270
225
|
// ── 一级菜单 ──────────────────────────────────────────────────
|
|
271
226
|
async function mainMenu() {
|
|
272
|
-
|
|
227
|
+
return select({
|
|
273
228
|
message: '请选择操作',
|
|
274
229
|
choices: [
|
|
275
230
|
{ name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
|
|
276
231
|
{ name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
|
|
277
232
|
{ name: '语言配置', value: 'lang', description: '界面语言设置' },
|
|
278
|
-
|
|
233
|
+
SEP,
|
|
279
234
|
{ name: '退出', value: 'exit' },
|
|
280
235
|
],
|
|
281
236
|
theme,
|
|
282
237
|
})
|
|
283
|
-
return choice
|
|
284
238
|
}
|
|
285
239
|
|
|
286
|
-
// ──
|
|
240
|
+
// ── 模型选择 ──────────────────────────────────────────────────
|
|
287
241
|
async function modelMenu() {
|
|
288
242
|
const cfg = loadConfig()
|
|
289
|
-
const
|
|
243
|
+
const now = currentModel()
|
|
290
244
|
|
|
291
245
|
const choice = await select({
|
|
292
246
|
message: '模型选择',
|
|
293
247
|
choices: [
|
|
294
|
-
{ name: `切换模型 ${C.dim}
|
|
295
|
-
{ name: `输入 API
|
|
296
|
-
|
|
297
|
-
{ name: '返回', value: 'back' },
|
|
298
|
-
{ name: '退出', value: 'exit' },
|
|
248
|
+
{ name: `切换模型 ${C.dim}(current: ${now})${C.r}`, value: 'switch', description: '选择要使用的模型' },
|
|
249
|
+
{ name: `输入 API ${C.dim}(${hasApiKey() ? C.green + 'configured' : C.red + 'not set'}${C.r})${C.r}`, value: 'apikey', description: '设置 DeepSeek API Key' },
|
|
250
|
+
SEP,
|
|
251
|
+
{ name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
|
|
299
252
|
],
|
|
300
253
|
theme,
|
|
301
254
|
})
|
|
@@ -303,270 +256,236 @@ async function modelMenu() {
|
|
|
303
256
|
if (choice === 'exit') return 'exit'
|
|
304
257
|
if (choice === 'back') return 'back'
|
|
305
258
|
|
|
259
|
+
// ── 切换模型 ──
|
|
306
260
|
if (choice === 'switch') {
|
|
307
|
-
const models = []
|
|
308
|
-
|
|
309
|
-
const seen = new Set()
|
|
310
|
-
for (const [from] of Object.entries(cfg.modelMap)) {
|
|
311
|
-
if (!seen.has(from)) {
|
|
312
|
-
seen.add(from)
|
|
313
|
-
models.push(from)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
models.push('deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat')
|
|
318
|
-
}
|
|
261
|
+
const models = (cfg?.modelMap ? Object.keys(cfg.modelMap) : ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat'])
|
|
262
|
+
.filter((v, i, a) => a.indexOf(v) === i) // dedupe
|
|
319
263
|
|
|
320
|
-
const
|
|
264
|
+
const pick = await select({
|
|
321
265
|
message: '选择模型',
|
|
322
266
|
choices: [
|
|
323
267
|
...models.map(m => ({
|
|
324
|
-
name: m ===
|
|
268
|
+
name: m === now ? `${C.green}● ${m}${C.r}` : ` ${m}`,
|
|
325
269
|
value: m,
|
|
326
270
|
})),
|
|
327
|
-
|
|
271
|
+
SEP,
|
|
328
272
|
{ name: '返回', value: 'back' },
|
|
329
273
|
],
|
|
330
274
|
theme,
|
|
331
275
|
})
|
|
332
276
|
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
await confirm({ message: '按回车继续...', theme })
|
|
277
|
+
if (pick && pick !== 'back') {
|
|
278
|
+
saveConfig({ ...cfg, currentModel: pick })
|
|
279
|
+
console.log(`\n ${C.green}Model switched: ${pick}${C.r}\n`)
|
|
280
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
338
281
|
}
|
|
339
282
|
return 'back'
|
|
340
283
|
}
|
|
341
284
|
|
|
285
|
+
// ── 输入 API ──
|
|
342
286
|
if (choice === 'apikey') {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
? currentKey.slice(0, 4) + '****' + currentKey.slice(-4)
|
|
346
|
-
: currentKey || '(空)'
|
|
347
|
-
|
|
348
|
-
console.log(`\n 当前 API Key:${C.dim}${masked}${C.r}`)
|
|
349
|
-
console.log(` ${C.dim}前往 https://platform.deepseek.com 申请${C.r}\n`)
|
|
287
|
+
console.log(`\n Current API Key: ${C.dim}${maskedKey()}${C.r}`)
|
|
288
|
+
console.log(` ${C.dim}Apply at https://platform.deepseek.com${C.r}\n`)
|
|
350
289
|
|
|
351
290
|
const newKey = await input({
|
|
352
|
-
message: '
|
|
291
|
+
message: 'Enter DeepSeek API Key (sk-...)',
|
|
353
292
|
theme,
|
|
354
|
-
validate:
|
|
293
|
+
validate: v => v.length > 0 || 'API Key cannot be empty',
|
|
355
294
|
})
|
|
356
295
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
296
|
+
saveConfig({
|
|
297
|
+
...(cfg || {}),
|
|
298
|
+
deepseekApiKey: newKey,
|
|
299
|
+
...DEFAULT_CONFIG,
|
|
300
|
+
})
|
|
301
|
+
console.log(`\n ${C.green}API Key saved.${C.r}`)
|
|
360
302
|
|
|
361
|
-
console.log(` ${C.dim}
|
|
362
|
-
const
|
|
363
|
-
console.log(` ${
|
|
364
|
-
await confirm({ message: '
|
|
303
|
+
console.log(` ${C.dim}Starting relay...${C.r}`)
|
|
304
|
+
const ok = await ensureRelay()
|
|
305
|
+
console.log(` ${ok ? C.green + 'Relay started.' : C.yellow + 'Relay failed (try again later).'}${C.r}\n`)
|
|
306
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
365
307
|
return 'back'
|
|
366
308
|
}
|
|
367
309
|
|
|
368
310
|
return 'back'
|
|
369
311
|
}
|
|
370
312
|
|
|
371
|
-
// ──
|
|
313
|
+
// ── 工具配置 ──────────────────────────────────────────────────
|
|
372
314
|
async function toolsMenu() {
|
|
373
315
|
const choice = await select({
|
|
374
316
|
message: '工具配置 — 选择要配置的 AI 编程工具',
|
|
375
317
|
choices: [
|
|
376
|
-
{ name: `${C.bold}Claude Code${C.r}`, value: 'claude', description:
|
|
377
|
-
{ name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description:
|
|
378
|
-
{ name: 'OpenCode', value: 'opencode', description:
|
|
379
|
-
{ name: 'OpenAI Codex CLI', value: 'codex', description:
|
|
380
|
-
{ name: 'Cursor', value: 'cursor', description:
|
|
381
|
-
{ name: 'Continue (VS Code)', value: 'continue', description:
|
|
382
|
-
{ name: 'Trae', value: 'trae', description:
|
|
383
|
-
|
|
384
|
-
{ name: '返回', value: 'back' },
|
|
385
|
-
{ name: '退出', value: 'exit' },
|
|
318
|
+
{ name: `${C.bold}Claude Code${C.r}`, value: 'claude', description: TOOLS.claude.desc },
|
|
319
|
+
{ name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description: TOOLS.gemini.desc },
|
|
320
|
+
{ name: 'OpenCode', value: 'opencode', description: TOOLS.opencode.desc },
|
|
321
|
+
{ name: 'OpenAI Codex CLI', value: 'codex', description: TOOLS.codex.desc },
|
|
322
|
+
{ name: 'Cursor', value: 'cursor', description: TOOLS.cursor.desc },
|
|
323
|
+
{ name: 'Continue (VS Code)', value: 'continue', description: TOOLS.continue.desc },
|
|
324
|
+
{ name: 'Trae', value: 'trae', description: TOOLS.trae.desc },
|
|
325
|
+
SEP,
|
|
326
|
+
{ name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
|
|
386
327
|
],
|
|
387
328
|
theme,
|
|
388
329
|
})
|
|
389
330
|
|
|
390
331
|
if (choice === 'exit') return 'exit'
|
|
391
332
|
if (choice === 'back') return 'back'
|
|
392
|
-
|
|
393
|
-
return await toolDetailMenu(choice)
|
|
333
|
+
return toolAction(choice)
|
|
394
334
|
}
|
|
395
335
|
|
|
396
|
-
// ──
|
|
397
|
-
async function
|
|
398
|
-
const
|
|
336
|
+
// ── 单个工具操作 ──────────────────────────────────────────────
|
|
337
|
+
async function toolAction(key) {
|
|
338
|
+
const t = TOOLS[key]
|
|
399
339
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
console.log(
|
|
403
|
-
console.log(` ${C.
|
|
404
|
-
|
|
340
|
+
// GUI-only tools
|
|
341
|
+
if (!t.cmd) {
|
|
342
|
+
console.log(`\n ${C.bold}${t.name}${C.r} — ${t.desc}`)
|
|
343
|
+
console.log(` ${C.dim}Install: ${t.install}${C.r}`)
|
|
344
|
+
console.log(` ${C.yellow}CLI mode does not auto-configure ${t.name}.${C.r}\n`)
|
|
345
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
405
346
|
return 'back'
|
|
406
347
|
}
|
|
407
348
|
|
|
408
|
-
const installed =
|
|
409
|
-
|
|
410
|
-
const choices = []
|
|
411
|
-
if (!installed) {
|
|
412
|
-
choices.push({ name: `安装 ${tool.name}`, value: 'install', description: tool.install })
|
|
413
|
-
} else {
|
|
414
|
-
choices.push({ name: `${C.green}启动 ${tool.name}${C.r}`, value: 'launch', description: '启动工具' })
|
|
415
|
-
choices.push({ name: `${C.yellow}重装 ${tool.name}${C.r}`, value: 'reinstall', description: tool.install })
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
choices.push(
|
|
419
|
-
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
420
|
-
{ name: '返回', value: 'back' },
|
|
421
|
-
{ name: '退出', value: 'exit' },
|
|
422
|
-
)
|
|
423
|
-
|
|
349
|
+
const installed = isInstalled(t.cmd)
|
|
424
350
|
const action = await select({
|
|
425
|
-
message: `${
|
|
426
|
-
choices
|
|
351
|
+
message: `${t.name} — ${t.desc}${installed ? ` ${C.green}[installed]${C.r}` : ` ${C.red}[not installed]${C.r}`}`,
|
|
352
|
+
choices: [
|
|
353
|
+
...(installed
|
|
354
|
+
? [{ name: `${C.green}Launch ${t.name}${C.r}`, value: 'launch' }, { name: `${C.yellow}Reinstall${C.r}`, value: 'reinstall' }]
|
|
355
|
+
: [{ name: `Install ${t.name}`, value: 'install', description: t.install }]
|
|
356
|
+
),
|
|
357
|
+
SEP,
|
|
358
|
+
{ name: '返回', value: 'back' }, { name: '退出', value: 'exit' },
|
|
359
|
+
],
|
|
427
360
|
theme,
|
|
428
361
|
})
|
|
429
362
|
|
|
430
363
|
if (action === 'exit') return 'exit'
|
|
431
364
|
if (action === 'back') return 'back'
|
|
432
365
|
|
|
366
|
+
// 安装 / 重装
|
|
433
367
|
if (action === 'install' || action === 'reinstall') {
|
|
434
|
-
console.log(`\n
|
|
368
|
+
console.log(`\n Running: ${t.install}`)
|
|
435
369
|
try {
|
|
436
|
-
execSync(
|
|
437
|
-
console.log(`\n ${C.green}
|
|
370
|
+
execSync(t.install, { stdio: 'inherit', shell: true })
|
|
371
|
+
console.log(`\n ${C.green}${t.name} installed.${C.r}\n`)
|
|
438
372
|
} catch {
|
|
439
|
-
console.log(`\n ${C.red}
|
|
373
|
+
console.log(`\n ${C.red}Install failed. Run manually: ${t.install}${C.r}\n`)
|
|
440
374
|
}
|
|
441
|
-
await confirm({ message: '
|
|
375
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
442
376
|
return 'back'
|
|
443
377
|
}
|
|
444
378
|
|
|
379
|
+
// 启动
|
|
445
380
|
if (action === 'launch') {
|
|
446
|
-
if (!
|
|
447
|
-
console.log(`\n ${C.red}API
|
|
448
|
-
await confirm({ message: '
|
|
381
|
+
if (!hasApiKey()) {
|
|
382
|
+
console.log(`\n ${C.red}API not configured! Go to Model Settings -> Enter API first.${C.r}\n`)
|
|
383
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
449
384
|
return 'back'
|
|
450
385
|
}
|
|
451
386
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
await confirm({ message: '按回车继续...', theme })
|
|
387
|
+
if (!(await ensureRelay())) {
|
|
388
|
+
console.log(`\n ${C.red}Relay failed. Restart BHG-helper.${C.r}\n`)
|
|
389
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
456
390
|
return 'back'
|
|
457
391
|
}
|
|
458
392
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const existing = fs.existsSync(CLAUDE_SETTINGS_FILE)
|
|
464
|
-
? JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'))
|
|
465
|
-
: {}
|
|
466
|
-
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify({
|
|
467
|
-
...existing,
|
|
468
|
-
hasCompletedOnboarding: true,
|
|
469
|
-
env: {
|
|
470
|
-
...(existing.env || {}),
|
|
471
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
472
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${RELAY_PORT}`,
|
|
473
|
-
ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
|
|
474
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: currentModel,
|
|
475
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: currentModel,
|
|
476
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: currentModel,
|
|
477
|
-
ANTHROPIC_MODEL: currentModel,
|
|
478
|
-
},
|
|
479
|
-
}, null, 2) + '\n')
|
|
480
|
-
console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (toolKey === 'opencode') {
|
|
484
|
-
const file = TOOLS.opencode.configFile
|
|
485
|
-
const dir = path.dirname(file)
|
|
486
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
487
|
-
const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : {}
|
|
488
|
-
const currentModel = getCurrentModel()
|
|
489
|
-
fs.writeFileSync(file, JSON.stringify({
|
|
490
|
-
...existing,
|
|
491
|
-
model: { provider: 'openai', name: currentModel },
|
|
492
|
-
providers: [
|
|
493
|
-
...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
|
|
494
|
-
{ name: 'bhg-deepseek', type: 'openai', baseURL: `http://127.0.0.1:${RELAY_PORT}/v1`, apiKey: 'bhg-helper' },
|
|
495
|
-
],
|
|
496
|
-
}, null, 2) + '\n')
|
|
497
|
-
console.log(` ${C.green}✓ OpenCode 配置已写入${C.r}`)
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
console.log(`\n 正在启动 ${tool.cmd}...`)
|
|
501
|
-
try {
|
|
502
|
-
execSync(tool.cmd, { stdio: 'inherit', shell: true })
|
|
503
|
-
} catch { /* 退出 */ }
|
|
393
|
+
writeToolConfig(key)
|
|
394
|
+
console.log(`\n Launching ${t.cmd}...`)
|
|
395
|
+
try { execSync(t.cmd, { stdio: 'inherit', shell: true }) }
|
|
396
|
+
catch { /* exit */ }
|
|
504
397
|
return 'back'
|
|
505
398
|
}
|
|
506
399
|
|
|
507
400
|
return 'back'
|
|
508
401
|
}
|
|
509
402
|
|
|
403
|
+
// ── 写工具配置文件 ────────────────────────────────────────────
|
|
404
|
+
function writeToolConfig(key) {
|
|
405
|
+
const model = currentModel()
|
|
406
|
+
const relayUrl = `http://127.0.0.1:${RELAY_PORT}`
|
|
407
|
+
|
|
408
|
+
if (key === 'claude') {
|
|
409
|
+
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS), { recursive: true })
|
|
410
|
+
let existing = {}
|
|
411
|
+
try { existing = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf-8')) } catch { /* */ }
|
|
412
|
+
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify({
|
|
413
|
+
...existing,
|
|
414
|
+
hasCompletedOnboarding: true,
|
|
415
|
+
env: {
|
|
416
|
+
...(existing.env || {}),
|
|
417
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
418
|
+
ANTHROPIC_BASE_URL: relayUrl,
|
|
419
|
+
ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
|
|
420
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
421
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
|
422
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
|
423
|
+
ANTHROPIC_MODEL: model,
|
|
424
|
+
},
|
|
425
|
+
}, null, 2) + '\n')
|
|
426
|
+
console.log(` ${C.green}Claude Code config written.${C.r}`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (key === 'opencode') {
|
|
430
|
+
const file = TOOLS.opencode.configFile
|
|
431
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
432
|
+
let existing = {}
|
|
433
|
+
try { existing = JSON.parse(fs.readFileSync(file, 'utf-8')) } catch { /* */ }
|
|
434
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
435
|
+
...existing,
|
|
436
|
+
model: { provider: 'openai', name: model },
|
|
437
|
+
providers: [
|
|
438
|
+
...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
|
|
439
|
+
{ name: 'bhg-deepseek', type: 'openai', baseURL: `${relayUrl}/v1`, apiKey: 'bhg-helper' },
|
|
440
|
+
],
|
|
441
|
+
}, null, 2) + '\n')
|
|
442
|
+
console.log(` ${C.green}OpenCode config written.${C.r}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
510
446
|
// ── 语言配置 ──────────────────────────────────────────────────
|
|
511
447
|
async function langMenu() {
|
|
512
448
|
const choice = await select({
|
|
513
|
-
message: '语言配置',
|
|
449
|
+
message: '语言配置 / Language',
|
|
514
450
|
choices: [
|
|
515
|
-
{ name: '中文', value: 'zh' },
|
|
516
|
-
{ name: '
|
|
517
|
-
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
518
|
-
{ name: '返回', value: 'back' },
|
|
451
|
+
{ name: '中文', value: 'zh' }, { name: 'English', value: 'en' },
|
|
452
|
+
SEP, { name: '返回', value: 'back' },
|
|
519
453
|
],
|
|
520
454
|
theme,
|
|
521
455
|
})
|
|
522
|
-
|
|
523
|
-
if (choice === 'exit') return 'exit'
|
|
524
456
|
if (choice === 'back') return 'back'
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
await confirm({ message: '按回车继续...', theme })
|
|
457
|
+
console.log(`\n ${C.green}Language: ${choice === 'zh' ? '中文' : 'English'}${C.r}\n`)
|
|
458
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
528
459
|
return 'back'
|
|
529
460
|
}
|
|
530
461
|
|
|
531
462
|
// ── 主循环 ────────────────────────────────────────────────────
|
|
532
463
|
async function main() {
|
|
533
|
-
|
|
534
|
-
while (running) {
|
|
464
|
+
while (true) {
|
|
535
465
|
await printHeader()
|
|
536
466
|
const choice = await mainMenu()
|
|
537
467
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
break
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
case 'lang': {
|
|
557
|
-
const result = await langMenu()
|
|
558
|
-
if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
|
|
559
|
-
break
|
|
560
|
-
}
|
|
468
|
+
if (choice === 'exit') {
|
|
469
|
+
console.log(`\n ${C.dim}Bye.${C.r}\n`)
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const result = await (
|
|
474
|
+
choice === 'model' ? modelMenu()
|
|
475
|
+
: choice === 'tools' ? toolsMenu()
|
|
476
|
+
: choice === 'lang' ? langMenu()
|
|
477
|
+
: Promise.resolve('back')
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if (result === 'exit') {
|
|
481
|
+
console.log(`\n ${C.dim}Bye.${C.r}\n`)
|
|
482
|
+
break
|
|
561
483
|
}
|
|
562
484
|
}
|
|
563
485
|
}
|
|
564
486
|
|
|
565
487
|
main().catch((err) => {
|
|
566
|
-
if (err.name === 'ExitPromptError') {
|
|
567
|
-
console.log(`\n ${C.dim}再见。${C.r}\n`)
|
|
568
|
-
process.exit(0)
|
|
569
|
-
}
|
|
488
|
+
if (err.name === 'ExitPromptError') { console.log(`\n ${C.dim}Bye.${C.r}\n`); process.exit(0) }
|
|
570
489
|
console.error('Error:', err.message)
|
|
571
490
|
process.exit(1)
|
|
572
491
|
})
|