bhg-helper 1.0.0

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 (53) hide show
  1. package/README.md +78 -0
  2. package/api/app.ts +53 -0
  3. package/api/index.ts +9 -0
  4. package/api/lib/logger.ts +65 -0
  5. package/api/lib/paths.ts +27 -0
  6. package/api/lib/providers.ts +66 -0
  7. package/api/lib/repository.ts +153 -0
  8. package/api/lib/types.ts +43 -0
  9. package/api/relay/config.ts +76 -0
  10. package/api/relay/protocol.ts +393 -0
  11. package/api/relay/server.ts +283 -0
  12. package/api/routes/backups.ts +73 -0
  13. package/api/routes/config.ts +197 -0
  14. package/api/routes/install.ts +158 -0
  15. package/api/routes/logs.ts +20 -0
  16. package/api/routes/providers.ts +13 -0
  17. package/api/routes/relay.ts +106 -0
  18. package/api/server.ts +40 -0
  19. package/cli/cli.js +454 -0
  20. package/dist/assets/index-BjvGHrGe.js +156 -0
  21. package/dist/assets/index-CQrGCyBr.css +1 -0
  22. package/dist/favicon.svg +4 -0
  23. package/dist/index.html +20 -0
  24. package/index.html +19 -0
  25. package/nodemon.json +10 -0
  26. package/package.json +82 -0
  27. package/postcss.config.js +10 -0
  28. package/scripts/install.bat +32 -0
  29. package/scripts/start.bat +46 -0
  30. package/scripts/start.ps1 +45 -0
  31. package/src/App.tsx +73 -0
  32. package/src/assets/react.svg +1 -0
  33. package/src/components/ConsolePanel.tsx +44 -0
  34. package/src/components/Empty.tsx +8 -0
  35. package/src/components/ErrorBoundary.tsx +54 -0
  36. package/src/components/Layout.tsx +17 -0
  37. package/src/components/Page.tsx +130 -0
  38. package/src/components/Sidebar.tsx +56 -0
  39. package/src/hooks/useTheme.ts +29 -0
  40. package/src/index.css +1350 -0
  41. package/src/lib/api.ts +120 -0
  42. package/src/lib/store.ts +166 -0
  43. package/src/lib/types.ts +117 -0
  44. package/src/lib/utils.ts +6 -0
  45. package/src/main.tsx +10 -0
  46. package/src/pages/ConsolePage.tsx +48 -0
  47. package/src/pages/Dashboard.tsx +101 -0
  48. package/src/pages/Install.tsx +195 -0
  49. package/src/pages/Relay.tsx +409 -0
  50. package/src/vite-env.d.ts +1 -0
  51. package/tailwind.config.js +13 -0
  52. package/tsconfig.json +40 -0
  53. package/vite.config.ts +28 -0
package/cli/cli.js ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BHG-helper CLI — 命令行交互菜单
4
+ * 一级菜单:模型选择 / 工具配置 / 语言配置 / BHG-helper UI / 退出
5
+ */
6
+
7
+ import { select, confirm, input } from '@inquirer/prompts'
8
+ import { execSync } from 'node:child_process'
9
+ import os from 'node:os'
10
+ import path from 'node:path'
11
+ import fs from 'node:fs'
12
+
13
+ // ── 色彩 ──────────────────────────────────────────────────────
14
+ const C = { r: '\x1b[0m', bold: '\x1b[1m', cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', red: '\x1b[31m' }
15
+ const theme = { prefix: `${C.cyan}?${C.r}` }
16
+
17
+ // ── 常量 ──────────────────────────────────────────────────────
18
+ const RELAY_PORT = 8787
19
+ const UI_URL = 'http://localhost:5173'
20
+ const BHG_HELPER_DIR = path.join(os.homedir(), '.bhg-helper')
21
+ const RELAY_CONFIG_FILE = path.join(BHG_HELPER_DIR, 'relay.config.json')
22
+ const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json')
23
+
24
+ // ── 工具定义 ──────────────────────────────────────────────────
25
+ const TOOLS = {
26
+ claude: {
27
+ name: 'Claude Code',
28
+ cmd: 'claude',
29
+ desc: 'Anthropic 官方 CLI 编程助手',
30
+ install: 'npm install -g @anthropic-ai/claude-code',
31
+ },
32
+ gemini: {
33
+ name: 'Gemini CLI',
34
+ cmd: 'gemini',
35
+ desc: 'Google 官方 CLI 编程助手',
36
+ install: 'npm install -g @anthropic-ai/gemini-cli',
37
+ },
38
+ opencode: {
39
+ name: 'OpenCode',
40
+ cmd: 'opencode',
41
+ desc: '开源 AI 编程助手',
42
+ install: 'npm install -g opencode-ai',
43
+ configFile: (() => {
44
+ if (process.platform === 'win32') return path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', 'opencode.json')
45
+ return path.join(os.homedir(), '.config', 'opencode', 'opencode.json')
46
+ })(),
47
+ },
48
+ cursor: {
49
+ name: 'Cursor',
50
+ cmd: null,
51
+ desc: 'AI 代码编辑器',
52
+ install: '从 https://cursor.com 下载',
53
+ },
54
+ continue: {
55
+ name: 'Continue (VS Code)',
56
+ cmd: null,
57
+ desc: 'VS Code / JetBrains AI 扩展',
58
+ install: 'VS Code 扩展搜索 "Continue"',
59
+ configFile: path.join(os.homedir(), '.continue', 'config.json'),
60
+ },
61
+ trae: {
62
+ name: 'Trae',
63
+ cmd: null,
64
+ desc: '字节跳动 AI 代码编辑器',
65
+ install: '从 https://trae.ai 下载',
66
+ },
67
+ codex: {
68
+ name: 'OpenAI Codex CLI',
69
+ cmd: 'codex',
70
+ desc: 'OpenAI 官方 CLI 编程助手',
71
+ install: 'npm install -g @openai/codex',
72
+ },
73
+ }
74
+
75
+ // ── 读取 / 写入 relay config ─────────────────────────────────
76
+ function loadConfig() {
77
+ try {
78
+ if (fs.existsSync(RELAY_CONFIG_FILE)) {
79
+ return JSON.parse(fs.readFileSync(RELAY_CONFIG_FILE, 'utf-8'))
80
+ }
81
+ } catch { /* */ }
82
+ return null
83
+ }
84
+
85
+ function saveConfig(cfg) {
86
+ fs.mkdirSync(BHG_HELPER_DIR, { recursive: true })
87
+ fs.writeFileSync(RELAY_CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf-8')
88
+ }
89
+
90
+ function hasApiConfig() {
91
+ const cfg = loadConfig()
92
+ return !!(cfg && cfg.deepseekApiKey && cfg.deepseekApiKey.length > 0)
93
+ }
94
+
95
+ function getCurrentModel() {
96
+ const cfg = loadConfig()
97
+ if (!cfg) return 'deepseek-v4-pro'
98
+ // 优先读取当前激活的模型
99
+ return cfg.currentModel || 'deepseek-v4-pro'
100
+ }
101
+
102
+ // ── 辅助 ──────────────────────────────────────────────────────
103
+ function checkInstalled(cmd) {
104
+ if (!cmd) return null
105
+ try {
106
+ execSync(`where ${cmd}`, { stdio: 'pipe' })
107
+ return true
108
+ } catch { return false }
109
+ }
110
+
111
+ function openBrowser(url) {
112
+ try {
113
+ execSync(`start "" "${url}"`, { shell: true })
114
+ } catch { /* */ }
115
+ }
116
+
117
+ // ── 打印头部 ──────────────────────────────────────────────────
118
+ function printHeader() {
119
+ console.clear()
120
+ console.log(`${C.bold}${C.cyan} ◆ BHG-helper${C.r}`)
121
+ const apiOk = hasApiConfig()
122
+ const model = getCurrentModel()
123
+ console.log(` ${C.dim}API${C.r} ${apiOk ? `${C.green}已配置${C.r}` : `${C.red}未配置${C.r}`}`)
124
+ console.log(` ${C.dim}模型${C.r} ${C.cyan}${model}${C.r}`)
125
+ console.log(` ${C.dim}UI${C.r} ${UI_URL}\n`)
126
+ }
127
+
128
+ // ── 一级菜单 ──────────────────────────────────────────────────
129
+ async function mainMenu() {
130
+ const choice = await select({
131
+ message: '请选择操作',
132
+ choices: [
133
+ { name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
134
+ { name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
135
+ { name: '语言配置', value: 'lang', description: '界面语言设置' },
136
+ { name: `${C.yellow}BHG-helper 网页界面${C.r}`, value: 'ui', description: '打开网页控制台' },
137
+ { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
138
+ { name: '退出', value: 'exit' },
139
+ ],
140
+ theme,
141
+ })
142
+ return choice
143
+ }
144
+
145
+ // ── 模型选择子菜单 ───────────────────────────────────────────
146
+ async function modelMenu() {
147
+ const cfg = loadConfig()
148
+ const currentModel = getCurrentModel()
149
+
150
+ const choice = await select({
151
+ message: '模型选择',
152
+ choices: [
153
+ { name: `切换模型 ${C.dim}(当前:${currentModel})${C.r}`, value: 'switch', description: '选择要使用的模型' },
154
+ { name: `输入 API ${C.dim}(${hasApiConfig() ? C.green + '已配置' + C.r : C.red + '未配置' + C.r})${C.r}`, value: 'apikey', description: '设置 DeepSeek API Key' },
155
+ { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
156
+ { name: '返回', value: 'back' },
157
+ { name: '退出', value: 'exit' },
158
+ ],
159
+ theme,
160
+ })
161
+
162
+ if (choice === 'exit') return 'exit'
163
+ if (choice === 'back') return 'back'
164
+
165
+ if (choice === 'switch') {
166
+ // 从 relay config 读取模型列表
167
+ const models = []
168
+ if (cfg && cfg.modelMap) {
169
+ const seen = new Set()
170
+ for (const [from, to] of Object.entries(cfg.modelMap)) {
171
+ if (!seen.has(to)) {
172
+ seen.add(to)
173
+ models.push({ from, to })
174
+ }
175
+ }
176
+ } else {
177
+ // 默认列表
178
+ models.push(
179
+ { from: 'deepseek-v4-pro', to: 'deepseek-v4-pro' },
180
+ { from: 'deepseek-v4-flash', to: 'deepseek-v4-flash' },
181
+ { from: 'deepseek-chat', to: 'deepseek-v4-pro' },
182
+ { from: 'deepseek-reasoner', to: 'deepseek-v4-pro' },
183
+ )
184
+ }
185
+
186
+ const modelChoice = await select({
187
+ message: '选择模型',
188
+ choices: [
189
+ ...models.map(m => ({
190
+ name: m.from === currentModel
191
+ ? `${C.green}● ${m.from}${C.r} ${C.dim}→ ${m.to}${C.r}`
192
+ : ` ${m.from} ${C.dim}→ ${m.to}${C.r}`,
193
+ value: m.from,
194
+ })),
195
+ { name: `${C.dim}──────────${C.r}`, value: '---2', disabled: true },
196
+ { name: '返回', value: 'back' },
197
+ ],
198
+ theme,
199
+ })
200
+
201
+ if (modelChoice && modelChoice !== 'back') {
202
+ const next = { ...cfg, currentModel: modelChoice }
203
+ saveConfig(next)
204
+ console.log(`\n ${C.green}✓ 已切换模型:${modelChoice}${C.r}\n`)
205
+ await confirm({ message: '按回车继续...', theme })
206
+ }
207
+ return 'back'
208
+ }
209
+
210
+ if (choice === 'apikey') {
211
+ const currentKey = cfg?.deepseekApiKey || ''
212
+ const masked = currentKey.length > 8
213
+ ? currentKey.slice(0, 4) + '****' + currentKey.slice(-4)
214
+ : currentKey || '(空)'
215
+
216
+ console.log(`\n 当前 API Key:${C.dim}${masked}${C.r}`)
217
+ console.log(` ${C.dim}前往 https://platform.deepseek.com 申请${C.r}\n`)
218
+
219
+ const newKey = await input({
220
+ message: '输入 DeepSeek API Key(sk-...)',
221
+ theme,
222
+ validate: (v) => v.length > 0 ? true : 'API Key 不能为空',
223
+ })
224
+
225
+ const next = { ...(cfg || {}), deepseekApiKey: newKey, port: cfg?.port || RELAY_PORT, deepseekBaseUrl: cfg?.deepseekBaseUrl || 'https://api.deepseek.com', modelMap: cfg?.modelMap || { 'deepseek-v4-pro': 'deepseek-v4-pro', 'deepseek-chat': 'deepseek-v4-pro' }, spoofProvider: cfg?.spoofProvider || 'deepseek', verbose: cfg?.verbose ?? true }
226
+ saveConfig(next)
227
+ console.log(`\n ${C.green}✓ API Key 已保存${C.r}\n`)
228
+ await confirm({ message: '按回车继续...', theme })
229
+ return 'back'
230
+ }
231
+
232
+ return 'back'
233
+ }
234
+
235
+ // ── 工具配置子菜单 ───────────────────────────────────────────
236
+ async function toolsMenu() {
237
+ const choice = await select({
238
+ message: '工具配置 — 选择要配置的 AI 编程工具',
239
+ choices: [
240
+ { name: `${C.bold}Claude Code${C.r}`, value: 'claude', description: 'Anthropic 官方 CLI 编程助手' },
241
+ { name: `${C.bold}Gemini CLI${C.r}`, value: 'gemini', description: 'Google 官方 CLI 编程助手' },
242
+ { name: 'OpenCode', value: 'opencode', description: '开源 AI 编程助手' },
243
+ { name: 'OpenAI Codex CLI', value: 'codex', description: 'OpenAI 官方 CLI 编程助手' },
244
+ { name: 'Cursor', value: 'cursor', description: 'AI 代码编辑器' },
245
+ { name: 'Continue (VS Code)', value: 'continue', description: 'VS Code / JetBrains 扩展' },
246
+ { name: 'Trae', value: 'trae', description: '字节跳动 AI 编辑器' },
247
+ { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
248
+ { name: '返回', value: 'back' },
249
+ { name: '退出', value: 'exit' },
250
+ ],
251
+ theme,
252
+ })
253
+
254
+ if (choice === 'exit') return 'exit'
255
+ if (choice === 'back') return 'back'
256
+
257
+ if (choice === 'ui') {
258
+ openBrowser(UI_URL)
259
+ console.log(`\n ${C.green}✓ 浏览器已打开 → ${UI_URL}${C.r}\n`)
260
+ await confirm({ message: '按回车继续...', theme })
261
+ return 'back'
262
+ }
263
+
264
+ return await toolDetailMenu(choice)
265
+ }
266
+
267
+ // ── 单个工具详情 ──────────────────────────────────────────────
268
+ async function toolDetailMenu(toolKey) {
269
+ const tool = TOOLS[toolKey]
270
+
271
+ // Cursor / Continue / Trae — 无 CLI,只引导去 UI
272
+ if (!tool.cmd) {
273
+ console.log(`\n ${C.bold}${tool.name}${C.r} — ${tool.desc}`)
274
+ console.log(` ${C.dim}安装方式:${tool.install}${C.r}`)
275
+ console.log(` ${C.yellow}${tool.name} 需要在 BHG-helper 网页界面配置${C.r}\n`)
276
+
277
+ const openUi = await confirm({ message: '打开 BHG-helper 网页界面?', default: true, theme })
278
+ if (openUi) {
279
+ openBrowser(UI_URL)
280
+ console.log(` ${C.green}✓ 浏览器已打开${C.r}`)
281
+ }
282
+ console.log()
283
+ await confirm({ message: '按回车继续...', theme })
284
+ return 'back'
285
+ }
286
+
287
+ // 有 CLI 的工具
288
+ const installed = checkInstalled(tool.cmd)
289
+
290
+ const choices = []
291
+ if (!installed) {
292
+ choices.push({ name: `安装 ${tool.name}`, value: 'install', description: tool.install })
293
+ } else {
294
+ choices.push({ name: `${C.green}启动 ${tool.name}${C.r}`, value: 'launch', description: '启动工具' })
295
+ choices.push({ name: `${C.yellow}重装 ${tool.name}${C.r}`, value: 'reinstall', description: tool.install })
296
+ }
297
+
298
+ choices.push(
299
+ { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
300
+ { name: '返回', value: 'back' },
301
+ { name: '退出', value: 'exit' },
302
+ )
303
+
304
+ const action = await select({
305
+ message: `${tool.name} — ${tool.desc}${installed ? ` ${C.green}(已安装)${C.r}` : ` ${C.red}(未安装)${C.r}`}`,
306
+ choices,
307
+ theme,
308
+ })
309
+
310
+ if (action === 'exit') return 'exit'
311
+ if (action === 'back') return 'back'
312
+
313
+ if (action === 'install' || action === 'reinstall') {
314
+ console.log(`\n 正在执行:${tool.install}`)
315
+ try {
316
+ execSync(tool.install, { stdio: 'inherit', shell: true })
317
+ console.log(`\n ${C.green}✓ ${tool.name} 安装完成${C.r}\n`)
318
+ } catch {
319
+ console.log(`\n ${C.red}安装失败,请手动执行:${tool.install}${C.r}\n`)
320
+ }
321
+ await confirm({ message: '按回车继续...', theme })
322
+ return 'back'
323
+ }
324
+
325
+ if (action === 'launch') {
326
+ // 启动前检查 API 配置
327
+ if (!hasApiConfig()) {
328
+ console.log(`\n ${C.red}API 未配置!请先在「模型选择」→「输入 API」中配置。${C.r}\n`)
329
+ await confirm({ message: '按回车继续...', theme })
330
+ return 'back'
331
+ }
332
+
333
+ // 写入 ~/.claude/settings.json(Claude Code 专用)
334
+ if (toolKey === 'claude') {
335
+ const currentModel = getCurrentModel()
336
+ const dir = path.dirname(CLAUDE_SETTINGS_FILE)
337
+ fs.mkdirSync(dir, { recursive: true })
338
+ const existing = fs.existsSync(CLAUDE_SETTINGS_FILE)
339
+ ? JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'))
340
+ : {}
341
+ fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify({
342
+ ...existing,
343
+ hasCompletedOnboarding: true,
344
+ env: {
345
+ ...(existing.env || {}),
346
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
347
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${RELAY_PORT}`,
348
+ ANTHROPIC_AUTH_TOKEN: 'bhg-helper',
349
+ ANTHROPIC_DEFAULT_OPUS_MODEL: currentModel,
350
+ ANTHROPIC_DEFAULT_SONNET_MODEL: currentModel,
351
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: currentModel,
352
+ ANTHROPIC_MODEL: currentModel,
353
+ },
354
+ }, null, 2) + '\n')
355
+ console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
356
+ }
357
+
358
+ // 写入 opencode 配置
359
+ if (toolKey === 'opencode') {
360
+ const file = TOOLS.opencode.configFile
361
+ const dir = path.dirname(file)
362
+ fs.mkdirSync(dir, { recursive: true })
363
+ const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf-8')) : {}
364
+ const currentModel = getCurrentModel()
365
+ fs.writeFileSync(file, JSON.stringify({
366
+ ...existing,
367
+ model: { provider: 'openai', name: currentModel },
368
+ providers: [
369
+ ...(existing.providers || []).filter(p => p.name !== 'bhg-deepseek'),
370
+ { name: 'bhg-deepseek', type: 'openai', baseURL: `http://127.0.0.1:${RELAY_PORT}/v1`, apiKey: 'bhg-helper' },
371
+ ],
372
+ }, null, 2) + '\n')
373
+ console.log(` ${C.green}✓ OpenCode 配置已写入${C.r}`)
374
+ }
375
+
376
+ console.log(`\n 正在启动 ${tool.cmd}...`)
377
+ try {
378
+ execSync(tool.cmd, { stdio: 'inherit', shell: true })
379
+ } catch { /* 退出 */ }
380
+ return 'back'
381
+ }
382
+
383
+ return 'back'
384
+ }
385
+
386
+ // ── 语言配置 ──────────────────────────────────────────────────
387
+ async function langMenu() {
388
+ const choice = await select({
389
+ message: '语言配置',
390
+ choices: [
391
+ { name: '中文', value: 'zh' },
392
+ { name: 'English', value: 'en' },
393
+ { name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
394
+ { name: '返回', value: 'back' },
395
+ ],
396
+ theme,
397
+ })
398
+
399
+ if (choice === 'exit') return 'exit'
400
+ if (choice === 'back') return 'back'
401
+
402
+ console.log(`\n ${C.green}✓ 语言已设为:${choice === 'zh' ? '中文' : 'English'}${C.r}\n`)
403
+ await confirm({ message: '按回车继续...', theme })
404
+ return 'back'
405
+ }
406
+
407
+ // ── 主循环 ────────────────────────────────────────────────────
408
+ async function main() {
409
+ let running = true
410
+ while (running) {
411
+ printHeader()
412
+ const choice = await mainMenu()
413
+
414
+ switch (choice) {
415
+ case 'exit':
416
+ console.log(`\n ${C.dim}再见。${C.r}\n`)
417
+ running = false
418
+ break
419
+
420
+ case 'model': {
421
+ const result = await modelMenu()
422
+ if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
423
+ break
424
+ }
425
+
426
+ case 'tools': {
427
+ const result = await toolsMenu()
428
+ if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
429
+ break
430
+ }
431
+
432
+ case 'lang': {
433
+ const result = await langMenu()
434
+ if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
435
+ break
436
+ }
437
+
438
+ case 'ui':
439
+ openBrowser(UI_URL)
440
+ console.log(`\n ${C.green}✓ 浏览器已打开 → ${UI_URL}${C.r}\n`)
441
+ await confirm({ message: '按回车继续...', theme })
442
+ break
443
+ }
444
+ }
445
+ }
446
+
447
+ main().catch((err) => {
448
+ if (err.name === 'ExitPromptError') {
449
+ console.log(`\n ${C.dim}再见。${C.r}\n`)
450
+ process.exit(0)
451
+ }
452
+ console.error('Error:', err.message)
453
+ process.exit(1)
454
+ })