bhg-helper 1.0.5 → 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 +236 -306
- 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,196 +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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
shell: false,
|
|
219
|
-
})
|
|
220
|
-
child.unref()
|
|
221
|
-
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
222
|
-
return await apiHealth()
|
|
181
|
+
if (await healthCheck()) return true
|
|
182
|
+
try {
|
|
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()
|
|
223
187
|
}
|
|
224
188
|
|
|
225
189
|
async function ensureRelay() {
|
|
226
|
-
|
|
227
|
-
return await apiPost('/api/relay/start')
|
|
190
|
+
return (await ensureBackend()) && await apiPost('/api/relay/start')
|
|
228
191
|
}
|
|
229
192
|
|
|
230
|
-
// ──
|
|
193
|
+
// ── 头部(ArkHelper 风格)──────────────────────────────────────
|
|
231
194
|
async function printHeader() {
|
|
232
195
|
console.clear()
|
|
233
196
|
printLogo()
|
|
234
|
-
|
|
235
197
|
console.log(` v${VERSION} · DeepSeek LLM bridge for your AI coding tools`)
|
|
236
198
|
console.log()
|
|
237
199
|
console.log(` ${C.bold}一键配置${C.r}`)
|
|
238
|
-
for (const t of
|
|
200
|
+
for (const t of CLI_TOOLS) {
|
|
239
201
|
console.log(` ${C.cyan}◆${C.r} ${t.name} ${C.dim}— ${t.desc}${C.r}`)
|
|
240
202
|
}
|
|
241
203
|
console.log()
|
|
242
204
|
|
|
243
|
-
const apiOk =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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)
|
|
247
211
|
|
|
248
212
|
console.log(
|
|
249
|
-
` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓'
|
|
250
|
-
+ ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓'
|
|
251
|
-
+ ` ${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}`
|
|
252
216
|
)
|
|
253
|
-
|
|
254
217
|
console.log()
|
|
255
218
|
console.log(` ${C.dim}powered by BH6BHG${C.r}`)
|
|
256
219
|
console.log()
|
|
257
220
|
}
|
|
258
221
|
|
|
222
|
+
// ── 菜单分隔线 ────────────────────────────────────────────────
|
|
223
|
+
const SEP = { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true }
|
|
224
|
+
|
|
259
225
|
// ── 一级菜单 ──────────────────────────────────────────────────
|
|
260
226
|
async function mainMenu() {
|
|
261
|
-
|
|
227
|
+
return select({
|
|
262
228
|
message: '请选择操作',
|
|
263
229
|
choices: [
|
|
264
230
|
{ name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
|
|
265
231
|
{ name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
|
|
266
232
|
{ name: '语言配置', value: 'lang', description: '界面语言设置' },
|
|
267
|
-
|
|
233
|
+
SEP,
|
|
268
234
|
{ name: '退出', value: 'exit' },
|
|
269
235
|
],
|
|
270
236
|
theme,
|
|
271
237
|
})
|
|
272
|
-
return choice
|
|
273
238
|
}
|
|
274
239
|
|
|
275
|
-
// ──
|
|
240
|
+
// ── 模型选择 ──────────────────────────────────────────────────
|
|
276
241
|
async function modelMenu() {
|
|
277
242
|
const cfg = loadConfig()
|
|
278
|
-
const
|
|
243
|
+
const now = currentModel()
|
|
279
244
|
|
|
280
245
|
const choice = await select({
|
|
281
246
|
message: '模型选择',
|
|
282
247
|
choices: [
|
|
283
|
-
{ name: `切换模型 ${C.dim}
|
|
284
|
-
{ name: `输入 API
|
|
285
|
-
|
|
286
|
-
{ name: '返回', value: 'back' },
|
|
287
|
-
{ 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' },
|
|
288
252
|
],
|
|
289
253
|
theme,
|
|
290
254
|
})
|
|
@@ -292,270 +256,236 @@ async function modelMenu() {
|
|
|
292
256
|
if (choice === 'exit') return 'exit'
|
|
293
257
|
if (choice === 'back') return 'back'
|
|
294
258
|
|
|
259
|
+
// ── 切换模型 ──
|
|
295
260
|
if (choice === 'switch') {
|
|
296
|
-
const models = []
|
|
297
|
-
|
|
298
|
-
const seen = new Set()
|
|
299
|
-
for (const [from] of Object.entries(cfg.modelMap)) {
|
|
300
|
-
if (!seen.has(from)) {
|
|
301
|
-
seen.add(from)
|
|
302
|
-
models.push(from)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
models.push('deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat')
|
|
307
|
-
}
|
|
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
|
|
308
263
|
|
|
309
|
-
const
|
|
264
|
+
const pick = await select({
|
|
310
265
|
message: '选择模型',
|
|
311
266
|
choices: [
|
|
312
267
|
...models.map(m => ({
|
|
313
|
-
name: m ===
|
|
268
|
+
name: m === now ? `${C.green}● ${m}${C.r}` : ` ${m}`,
|
|
314
269
|
value: m,
|
|
315
270
|
})),
|
|
316
|
-
|
|
271
|
+
SEP,
|
|
317
272
|
{ name: '返回', value: 'back' },
|
|
318
273
|
],
|
|
319
274
|
theme,
|
|
320
275
|
})
|
|
321
276
|
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
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 })
|
|
327
281
|
}
|
|
328
282
|
return 'back'
|
|
329
283
|
}
|
|
330
284
|
|
|
285
|
+
// ── 输入 API ──
|
|
331
286
|
if (choice === 'apikey') {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
? currentKey.slice(0, 4) + '****' + currentKey.slice(-4)
|
|
335
|
-
: currentKey || '(空)'
|
|
336
|
-
|
|
337
|
-
console.log(`\n 当前 API Key:${C.dim}${masked}${C.r}`)
|
|
338
|
-
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`)
|
|
339
289
|
|
|
340
290
|
const newKey = await input({
|
|
341
|
-
message: '
|
|
291
|
+
message: 'Enter DeepSeek API Key (sk-...)',
|
|
342
292
|
theme,
|
|
343
|
-
validate:
|
|
293
|
+
validate: v => v.length > 0 || 'API Key cannot be empty',
|
|
344
294
|
})
|
|
345
295
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
296
|
+
saveConfig({
|
|
297
|
+
...(cfg || {}),
|
|
298
|
+
deepseekApiKey: newKey,
|
|
299
|
+
...DEFAULT_CONFIG,
|
|
300
|
+
})
|
|
301
|
+
console.log(`\n ${C.green}API Key saved.${C.r}`)
|
|
349
302
|
|
|
350
|
-
console.log(` ${C.dim}
|
|
351
|
-
const
|
|
352
|
-
console.log(` ${
|
|
353
|
-
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 })
|
|
354
307
|
return 'back'
|
|
355
308
|
}
|
|
356
309
|
|
|
357
310
|
return 'back'
|
|
358
311
|
}
|
|
359
312
|
|
|
360
|
-
// ──
|
|
313
|
+
// ── 工具配置 ──────────────────────────────────────────────────
|
|
361
314
|
async function toolsMenu() {
|
|
362
315
|
const choice = await select({
|
|
363
316
|
message: '工具配置 — 选择要配置的 AI 编程工具',
|
|
364
317
|
choices: [
|
|
365
|
-
{ name: `${C.bold}Claude Code${C.r}`, value: 'claude', description:
|
|
366
|
-
{ name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description:
|
|
367
|
-
{ name: 'OpenCode', value: 'opencode', description:
|
|
368
|
-
{ name: 'OpenAI Codex CLI', value: 'codex', description:
|
|
369
|
-
{ name: 'Cursor', value: 'cursor', description:
|
|
370
|
-
{ name: 'Continue (VS Code)', value: 'continue', description:
|
|
371
|
-
{ name: 'Trae', value: 'trae', description:
|
|
372
|
-
|
|
373
|
-
{ name: '返回', value: 'back' },
|
|
374
|
-
{ 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' },
|
|
375
327
|
],
|
|
376
328
|
theme,
|
|
377
329
|
})
|
|
378
330
|
|
|
379
331
|
if (choice === 'exit') return 'exit'
|
|
380
332
|
if (choice === 'back') return 'back'
|
|
381
|
-
|
|
382
|
-
return await toolDetailMenu(choice)
|
|
333
|
+
return toolAction(choice)
|
|
383
334
|
}
|
|
384
335
|
|
|
385
|
-
// ──
|
|
386
|
-
async function
|
|
387
|
-
const
|
|
336
|
+
// ── 单个工具操作 ──────────────────────────────────────────────
|
|
337
|
+
async function toolAction(key) {
|
|
338
|
+
const t = TOOLS[key]
|
|
388
339
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
console.log(
|
|
392
|
-
console.log(` ${C.
|
|
393
|
-
|
|
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 })
|
|
394
346
|
return 'back'
|
|
395
347
|
}
|
|
396
348
|
|
|
397
|
-
const installed =
|
|
398
|
-
|
|
399
|
-
const choices = []
|
|
400
|
-
if (!installed) {
|
|
401
|
-
choices.push({ name: `安装 ${tool.name}`, value: 'install', description: tool.install })
|
|
402
|
-
} else {
|
|
403
|
-
choices.push({ name: `${C.green}启动 ${tool.name}${C.r}`, value: 'launch', description: '启动工具' })
|
|
404
|
-
choices.push({ name: `${C.yellow}重装 ${tool.name}${C.r}`, value: 'reinstall', description: tool.install })
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
choices.push(
|
|
408
|
-
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
409
|
-
{ name: '返回', value: 'back' },
|
|
410
|
-
{ name: '退出', value: 'exit' },
|
|
411
|
-
)
|
|
412
|
-
|
|
349
|
+
const installed = isInstalled(t.cmd)
|
|
413
350
|
const action = await select({
|
|
414
|
-
message: `${
|
|
415
|
-
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
|
+
],
|
|
416
360
|
theme,
|
|
417
361
|
})
|
|
418
362
|
|
|
419
363
|
if (action === 'exit') return 'exit'
|
|
420
364
|
if (action === 'back') return 'back'
|
|
421
365
|
|
|
366
|
+
// 安装 / 重装
|
|
422
367
|
if (action === 'install' || action === 'reinstall') {
|
|
423
|
-
console.log(`\n
|
|
368
|
+
console.log(`\n Running: ${t.install}`)
|
|
424
369
|
try {
|
|
425
|
-
execSync(
|
|
426
|
-
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`)
|
|
427
372
|
} catch {
|
|
428
|
-
console.log(`\n ${C.red}
|
|
373
|
+
console.log(`\n ${C.red}Install failed. Run manually: ${t.install}${C.r}\n`)
|
|
429
374
|
}
|
|
430
|
-
await confirm({ message: '
|
|
375
|
+
await confirm({ message: 'Press Enter to continue...', theme })
|
|
431
376
|
return 'back'
|
|
432
377
|
}
|
|
433
378
|
|
|
379
|
+
// 启动
|
|
434
380
|
if (action === 'launch') {
|
|
435
|
-
if (!
|
|
436
|
-
console.log(`\n ${C.red}API
|
|
437
|
-
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 })
|
|
438
384
|
return 'back'
|
|
439
385
|
}
|
|
440
386
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
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 })
|
|
445
390
|
return 'back'
|
|
446
391
|
}
|
|
447
392
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const existing = fs.existsSync(CLAUDE_SETTINGS_FILE)
|
|
453
|
-
? JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'))
|
|
454
|
-
: {}
|
|
455
|
-
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify({
|
|
456
|
-
...existing,
|
|
457
|
-
hasCompletedOnboarding: true,
|
|
458
|
-
env: {
|
|
459
|
-
...(existing.env || {}),
|
|
460
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
461
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${RELAY_PORT}`,
|
|
462
|
-
ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
|
|
463
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: currentModel,
|
|
464
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: currentModel,
|
|
465
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: currentModel,
|
|
466
|
-
ANTHROPIC_MODEL: currentModel,
|
|
467
|
-
},
|
|
468
|
-
}, null, 2) + '\n')
|
|
469
|
-
console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (toolKey === 'opencode') {
|
|
473
|
-
const file = TOOLS.opencode.configFile
|
|
474
|
-
const dir = path.dirname(file)
|
|
475
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
476
|
-
const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : {}
|
|
477
|
-
const currentModel = getCurrentModel()
|
|
478
|
-
fs.writeFileSync(file, JSON.stringify({
|
|
479
|
-
...existing,
|
|
480
|
-
model: { provider: 'openai', name: currentModel },
|
|
481
|
-
providers: [
|
|
482
|
-
...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
|
|
483
|
-
{ name: 'bhg-deepseek', type: 'openai', baseURL: `http://127.0.0.1:${RELAY_PORT}/v1`, apiKey: 'bhg-helper' },
|
|
484
|
-
],
|
|
485
|
-
}, null, 2) + '\n')
|
|
486
|
-
console.log(` ${C.green}✓ OpenCode 配置已写入${C.r}`)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
console.log(`\n 正在启动 ${tool.cmd}...`)
|
|
490
|
-
try {
|
|
491
|
-
execSync(tool.cmd, { stdio: 'inherit', shell: true })
|
|
492
|
-
} catch { /* 退出 */ }
|
|
393
|
+
writeToolConfig(key)
|
|
394
|
+
console.log(`\n Launching ${t.cmd}...`)
|
|
395
|
+
try { execSync(t.cmd, { stdio: 'inherit', shell: true }) }
|
|
396
|
+
catch { /* exit */ }
|
|
493
397
|
return 'back'
|
|
494
398
|
}
|
|
495
399
|
|
|
496
400
|
return 'back'
|
|
497
401
|
}
|
|
498
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
|
+
|
|
499
446
|
// ── 语言配置 ──────────────────────────────────────────────────
|
|
500
447
|
async function langMenu() {
|
|
501
448
|
const choice = await select({
|
|
502
|
-
message: '语言配置',
|
|
449
|
+
message: '语言配置 / Language',
|
|
503
450
|
choices: [
|
|
504
|
-
{ name: '中文', value: 'zh' },
|
|
505
|
-
{ name: '
|
|
506
|
-
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
507
|
-
{ name: '返回', value: 'back' },
|
|
451
|
+
{ name: '中文', value: 'zh' }, { name: 'English', value: 'en' },
|
|
452
|
+
SEP, { name: '返回', value: 'back' },
|
|
508
453
|
],
|
|
509
454
|
theme,
|
|
510
455
|
})
|
|
511
|
-
|
|
512
|
-
if (choice === 'exit') return 'exit'
|
|
513
456
|
if (choice === 'back') return 'back'
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
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 })
|
|
517
459
|
return 'back'
|
|
518
460
|
}
|
|
519
461
|
|
|
520
462
|
// ── 主循环 ────────────────────────────────────────────────────
|
|
521
463
|
async function main() {
|
|
522
|
-
|
|
523
|
-
while (running) {
|
|
464
|
+
while (true) {
|
|
524
465
|
await printHeader()
|
|
525
466
|
const choice = await mainMenu()
|
|
526
467
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
break
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
case 'lang': {
|
|
546
|
-
const result = await langMenu()
|
|
547
|
-
if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
|
|
548
|
-
break
|
|
549
|
-
}
|
|
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
|
|
550
483
|
}
|
|
551
484
|
}
|
|
552
485
|
}
|
|
553
486
|
|
|
554
487
|
main().catch((err) => {
|
|
555
|
-
if (err.name === 'ExitPromptError') {
|
|
556
|
-
console.log(`\n ${C.dim}再见。${C.r}\n`)
|
|
557
|
-
process.exit(0)
|
|
558
|
-
}
|
|
488
|
+
if (err.name === 'ExitPromptError') { console.log(`\n ${C.dim}Bye.${C.r}\n`); process.exit(0) }
|
|
559
489
|
console.error('Error:', err.message)
|
|
560
490
|
process.exit(1)
|
|
561
491
|
})
|