bhg-helper 1.0.2 → 1.0.4
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/api/app.ts +1 -0
- package/api/relay/protocol.ts +0 -4
- package/cli/cli.js +162 -62
- package/dist/assets/{index-BXV7FBK0.js → index-C4x5glYN.js} +17 -17
- package/dist/index.html +1 -1
- package/package.json +5 -7
- package/scripts/start.bat +7 -7
- package/scripts/start.ps1 +4 -4
- package/src/components/ErrorBoundary.tsx +0 -1
- package/src/components/Sidebar.tsx +0 -3
- package/src/lib/store.ts +0 -1
- package/src/pages/ConsolePage.tsx +0 -1
- package/src/pages/Dashboard.tsx +1 -1
- package/src/pages/Relay.tsx +11 -12
package/api/app.ts
CHANGED
|
@@ -40,6 +40,7 @@ app.get('/api/health', (_req: Request, res: Response) => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
app.use((error: Error, _req: Request, res: Response, _next: NextFunction) => {
|
|
43
|
+
void _next
|
|
43
44
|
log('ERR', 'http', error.message)
|
|
44
45
|
res.status(500).json({ ok: false, error: error.message })
|
|
45
46
|
})
|
package/api/relay/protocol.ts
CHANGED
|
@@ -239,8 +239,6 @@ export interface AnthropicStreamEvent {
|
|
|
239
239
|
[key: string]: unknown
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
243
|
-
|
|
244
242
|
function makeId(): string {
|
|
245
243
|
// 用 crypto.randomUUID 如果存在
|
|
246
244
|
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
@@ -278,7 +276,6 @@ export function* openAIStreamToAnthropicEvents(
|
|
|
278
276
|
let finishReason: string | null = null
|
|
279
277
|
let hasText = false
|
|
280
278
|
let toolBlockIndex = -1
|
|
281
|
-
let toolInputBuffer = ''
|
|
282
279
|
let toolId: string | null = null
|
|
283
280
|
let toolName: string | null = null
|
|
284
281
|
const usedModel = openAIChunks[0]?.model ?? model
|
|
@@ -357,7 +354,6 @@ export function* openAIStreamToAnthropicEvents(
|
|
|
357
354
|
}
|
|
358
355
|
}
|
|
359
356
|
if (tc.function?.arguments) {
|
|
360
|
-
toolInputBuffer += tc.function.arguments
|
|
361
357
|
yield {
|
|
362
358
|
type: 'content_block_delta',
|
|
363
359
|
index: toolBlockIndex,
|
package/cli/cli.js
CHANGED
|
@@ -1,26 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* BHG-helper CLI — 命令行交互菜单
|
|
4
|
-
* 一级菜单:模型选择 / 工具配置 / 语言配置 /
|
|
4
|
+
* 一级菜单:模型选择 / 工具配置 / 语言配置 / 退出
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { select, confirm, input } from '@inquirer/prompts'
|
|
8
|
-
import { execSync } from 'node:child_process'
|
|
8
|
+
import { execSync, spawn } 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'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
12
13
|
|
|
13
14
|
// ── 色彩 ──────────────────────────────────────────────────────
|
|
14
|
-
const C = {
|
|
15
|
+
const C = {
|
|
16
|
+
r: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
}
|
|
15
24
|
const theme = { prefix: `${C.cyan}?${C.r}` }
|
|
16
25
|
|
|
17
26
|
// ── 常量 ──────────────────────────────────────────────────────
|
|
27
|
+
const VERSION = '1.0.2'
|
|
18
28
|
const RELAY_PORT = 8787
|
|
19
|
-
const
|
|
29
|
+
const API_URL = 'http://127.0.0.1:3001'
|
|
30
|
+
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
31
|
const BHG_HELPER_DIR = path.join(os.homedir(), '.bhg-helper')
|
|
21
32
|
const RELAY_CONFIG_FILE = path.join(BHG_HELPER_DIR, 'relay.config.json')
|
|
22
33
|
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json')
|
|
23
34
|
|
|
35
|
+
const TOOLS_LIST = [
|
|
36
|
+
{ name: 'Claude Code', desc: 'Anthropic 官方 CLI 编程助手' },
|
|
37
|
+
{ name: 'OpenCode', desc: '开源 AI 编程助手' },
|
|
38
|
+
{ name: 'Gemini CLI', desc: 'Google 官方 CLI 编程助手' },
|
|
39
|
+
{ name: 'OpenAI Codex CLI', desc: 'OpenAI 官方 CLI 编程助手' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// ── ANSI 24 位色渐变 Logo ──────────────────────────────────────
|
|
43
|
+
function rgb(r, g, b) {
|
|
44
|
+
return `\x1b[38;2;${r};${g};${b}m`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 蓝 → 紫 → 粉 渐变 10 行
|
|
48
|
+
function colorLine(row, totalRows = 10) {
|
|
49
|
+
const t = row / (totalRows - 1)
|
|
50
|
+
if (t < 0.5) {
|
|
51
|
+
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
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const BHG_LOGO = [
|
|
68
|
+
'██████╗ ██╗ ██╗ ███████╗',
|
|
69
|
+
'██╔══██╗ ██║ ██║ ██╔════╝',
|
|
70
|
+
'██████╔╝ ███████║ ███████╗',
|
|
71
|
+
'██╔══██╗ ██╔══██║ ╚════██║',
|
|
72
|
+
'██████╔╝ ██║ ██║ ███████║',
|
|
73
|
+
'╚═════╝ ╚═╝ ╚═╝ ╚══════╝',
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const HELPER_LOGO = [
|
|
77
|
+
'██╗ ██╗ ███████╗ ██╗ ██████╗ ███████╗ ██████╗ ',
|
|
78
|
+
'██║ ██║ ██╔════╝ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗',
|
|
79
|
+
'███████║ █████╗ ██║ ██████╔╝ █████╗ ██████╔╝',
|
|
80
|
+
'██╔══██║ ██╔══╝ ██║ ██╔═══╝ ██╔══╝ ██╔══██╗',
|
|
81
|
+
'██║ ██║ ███████╗ ███████╗ ██║ ███████╗ ██║ ██║',
|
|
82
|
+
'╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝',
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
function printLogo() {
|
|
86
|
+
const gap = ' '
|
|
87
|
+
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
|
+
)
|
|
91
|
+
}
|
|
92
|
+
console.log()
|
|
93
|
+
}
|
|
94
|
+
|
|
24
95
|
// ── 工具定义 ──────────────────────────────────────────────────
|
|
25
96
|
const TOOLS = {
|
|
26
97
|
claude: {
|
|
@@ -95,7 +166,6 @@ function hasApiConfig() {
|
|
|
95
166
|
function getCurrentModel() {
|
|
96
167
|
const cfg = loadConfig()
|
|
97
168
|
if (!cfg) return 'deepseek-v4-pro'
|
|
98
|
-
// 优先读取当前激活的模型
|
|
99
169
|
return cfg.currentModel || 'deepseek-v4-pro'
|
|
100
170
|
}
|
|
101
171
|
|
|
@@ -108,21 +178,75 @@ function checkInstalled(cmd) {
|
|
|
108
178
|
} catch { return false }
|
|
109
179
|
}
|
|
110
180
|
|
|
111
|
-
function
|
|
181
|
+
async function apiPost(pathname) {
|
|
182
|
+
const res = await fetch(`${API_URL}${pathname}`, { method: 'POST' })
|
|
183
|
+
return res.ok
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function apiGet(pathname) {
|
|
187
|
+
const res = await fetch(`${API_URL}${pathname}`)
|
|
188
|
+
if (!res.ok) return null
|
|
189
|
+
return await res.json().catch(() => null)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function apiHealth() {
|
|
112
193
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
194
|
+
const controller = new AbortController()
|
|
195
|
+
const timer = setTimeout(() => controller.abort(), 800)
|
|
196
|
+
const res = await fetch(`${API_URL}/api/health`, { signal: controller.signal })
|
|
197
|
+
clearTimeout(timer)
|
|
198
|
+
return res.ok
|
|
199
|
+
} catch {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function ensureBackend() {
|
|
205
|
+
if (await apiHealth()) return true
|
|
206
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
207
|
+
const child = spawn(npmCmd, ['run', 'server:raw'], {
|
|
208
|
+
cwd: PROJECT_ROOT,
|
|
209
|
+
detached: true,
|
|
210
|
+
stdio: 'ignore',
|
|
211
|
+
shell: false,
|
|
212
|
+
})
|
|
213
|
+
child.unref()
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
215
|
+
return await apiHealth()
|
|
115
216
|
}
|
|
116
217
|
|
|
117
|
-
|
|
118
|
-
|
|
218
|
+
async function ensureRelay() {
|
|
219
|
+
if (!(await ensureBackend())) return false
|
|
220
|
+
return await apiPost('/api/relay/start')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── 打印头部(ArkHelper 风格)─────────────────────────────────
|
|
224
|
+
async function printHeader() {
|
|
119
225
|
console.clear()
|
|
120
|
-
|
|
226
|
+
printLogo()
|
|
227
|
+
|
|
228
|
+
console.log(` v${VERSION} · DeepSeek LLM bridge for your AI coding tools`)
|
|
229
|
+
console.log()
|
|
230
|
+
console.log(` ${C.bold}一键配置${C.r}`)
|
|
231
|
+
for (const t of TOOLS_LIST) {
|
|
232
|
+
console.log(` ${C.cyan}◆${C.r} ${t.name} ${C.dim}— ${t.desc}${C.r}`)
|
|
233
|
+
}
|
|
234
|
+
console.log()
|
|
235
|
+
|
|
121
236
|
const apiOk = hasApiConfig()
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
237
|
+
const backendOk = await apiHealth()
|
|
238
|
+
const relayJson = await apiGet('/api/relay/status')
|
|
239
|
+
const relayOk = !!(relayJson && relayJson.data?.running)
|
|
240
|
+
|
|
241
|
+
console.log(
|
|
242
|
+
` ${C.dim}API:${C.r} ${apiOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
|
|
243
|
+
+ ` ${C.dim}Backend:${C.r} ${backendOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
|
|
244
|
+
+ ` ${C.dim}Relay:${C.r} ${relayOk ? C.green + '✓' + C.r : C.red + '✗' + C.r}`
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
console.log()
|
|
248
|
+
console.log(` ${C.dim}powered by BH6BHG${C.r}`)
|
|
249
|
+
console.log()
|
|
126
250
|
}
|
|
127
251
|
|
|
128
252
|
// ── 一级菜单 ──────────────────────────────────────────────────
|
|
@@ -133,7 +257,6 @@ async function mainMenu() {
|
|
|
133
257
|
{ name: '模型选择', value: 'model', description: '切换模型 / 输入 API Key' },
|
|
134
258
|
{ name: '工具配置', value: 'tools', description: '配置各 AI 编程工具' },
|
|
135
259
|
{ name: '语言配置', value: 'lang', description: '界面语言设置' },
|
|
136
|
-
{ name: `${C.yellow}BHG-helper 网页界面${C.r}`, value: 'ui', description: '打开网页控制台' },
|
|
137
260
|
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
138
261
|
{ name: '退出', value: 'exit' },
|
|
139
262
|
],
|
|
@@ -163,36 +286,27 @@ async function modelMenu() {
|
|
|
163
286
|
if (choice === 'back') return 'back'
|
|
164
287
|
|
|
165
288
|
if (choice === 'switch') {
|
|
166
|
-
// 从 relay config 读取模型列表
|
|
167
289
|
const models = []
|
|
168
290
|
if (cfg && cfg.modelMap) {
|
|
169
291
|
const seen = new Set()
|
|
170
|
-
for (const [from
|
|
171
|
-
if (!seen.has(
|
|
172
|
-
seen.add(
|
|
173
|
-
models.push(
|
|
292
|
+
for (const [from] of Object.entries(cfg.modelMap)) {
|
|
293
|
+
if (!seen.has(from)) {
|
|
294
|
+
seen.add(from)
|
|
295
|
+
models.push(from)
|
|
174
296
|
}
|
|
175
297
|
}
|
|
176
298
|
} 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
|
-
)
|
|
299
|
+
models.push('deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat')
|
|
184
300
|
}
|
|
185
301
|
|
|
186
302
|
const modelChoice = await select({
|
|
187
303
|
message: '选择模型',
|
|
188
304
|
choices: [
|
|
189
305
|
...models.map(m => ({
|
|
190
|
-
name: m
|
|
191
|
-
|
|
192
|
-
: ` ${m.from} ${C.dim}→ ${m.to}${C.r}`,
|
|
193
|
-
value: m.from,
|
|
306
|
+
name: m === currentModel ? `${C.green}● ${m}${C.r}` : ` ${m}`,
|
|
307
|
+
value: m,
|
|
194
308
|
})),
|
|
195
|
-
{ name: `${C.dim}──────────${C.r}`, value: '---
|
|
309
|
+
{ name: `${C.dim}──────────${C.r}`, value: '---', disabled: true },
|
|
196
310
|
{ name: '返回', value: 'back' },
|
|
197
311
|
],
|
|
198
312
|
theme,
|
|
@@ -224,7 +338,11 @@ async function modelMenu() {
|
|
|
224
338
|
|
|
225
339
|
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
340
|
saveConfig(next)
|
|
227
|
-
console.log(`\n ${C.green}✓ API Key 已保存${C.r}
|
|
341
|
+
console.log(`\n ${C.green}✓ API Key 已保存${C.r}`)
|
|
342
|
+
|
|
343
|
+
console.log(` ${C.dim}正在启动中转服务...${C.r}`)
|
|
344
|
+
const relayOk = await ensureRelay()
|
|
345
|
+
console.log(` ${relayOk ? C.green + '✓ 中转服务已启动' + C.r : C.yellow + '中转服务启动失败(可稍后手动启动)' + C.r}\n`)
|
|
228
346
|
await confirm({ message: '按回车继续...', theme })
|
|
229
347
|
return 'back'
|
|
230
348
|
}
|
|
@@ -254,13 +372,6 @@ async function toolsMenu() {
|
|
|
254
372
|
if (choice === 'exit') return 'exit'
|
|
255
373
|
if (choice === 'back') return 'back'
|
|
256
374
|
|
|
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
375
|
return await toolDetailMenu(choice)
|
|
265
376
|
}
|
|
266
377
|
|
|
@@ -268,23 +379,14 @@ async function toolsMenu() {
|
|
|
268
379
|
async function toolDetailMenu(toolKey) {
|
|
269
380
|
const tool = TOOLS[toolKey]
|
|
270
381
|
|
|
271
|
-
// Cursor / Continue / Trae — 无 CLI,只引导去 UI
|
|
272
382
|
if (!tool.cmd) {
|
|
273
383
|
console.log(`\n ${C.bold}${tool.name}${C.r} — ${tool.desc}`)
|
|
274
384
|
console.log(` ${C.dim}安装方式:${tool.install}${C.r}`)
|
|
275
|
-
console.log(` ${C.yellow}${tool.name}
|
|
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()
|
|
385
|
+
console.log(` ${C.yellow}当前纯命令行版暂不自动配置 ${tool.name}${C.r}\n`)
|
|
283
386
|
await confirm({ message: '按回车继续...', theme })
|
|
284
387
|
return 'back'
|
|
285
388
|
}
|
|
286
389
|
|
|
287
|
-
// 有 CLI 的工具
|
|
288
390
|
const installed = checkInstalled(tool.cmd)
|
|
289
391
|
|
|
290
392
|
const choices = []
|
|
@@ -323,14 +425,19 @@ async function toolDetailMenu(toolKey) {
|
|
|
323
425
|
}
|
|
324
426
|
|
|
325
427
|
if (action === 'launch') {
|
|
326
|
-
// 启动前检查 API 配置
|
|
327
428
|
if (!hasApiConfig()) {
|
|
328
429
|
console.log(`\n ${C.red}API 未配置!请先在「模型选择」→「输入 API」中配置。${C.r}\n`)
|
|
329
430
|
await confirm({ message: '按回车继续...', theme })
|
|
330
431
|
return 'back'
|
|
331
432
|
}
|
|
332
433
|
|
|
333
|
-
|
|
434
|
+
const relayOk = await ensureRelay()
|
|
435
|
+
if (!relayOk) {
|
|
436
|
+
console.log(`\n ${C.red}中转服务启动失败,请重新运行 BHG-helper。${C.r}\n`)
|
|
437
|
+
await confirm({ message: '按回车继续...', theme })
|
|
438
|
+
return 'back'
|
|
439
|
+
}
|
|
440
|
+
|
|
334
441
|
if (toolKey === 'claude') {
|
|
335
442
|
const currentModel = getCurrentModel()
|
|
336
443
|
const dir = path.dirname(CLAUDE_SETTINGS_FILE)
|
|
@@ -355,7 +462,6 @@ async function toolDetailMenu(toolKey) {
|
|
|
355
462
|
console.log(` ${C.green}✓ Claude Code 配置已写入${C.r}`)
|
|
356
463
|
}
|
|
357
464
|
|
|
358
|
-
// 写入 opencode 配置
|
|
359
465
|
if (toolKey === 'opencode') {
|
|
360
466
|
const file = TOOLS.opencode.configFile
|
|
361
467
|
const dir = path.dirname(file)
|
|
@@ -408,7 +514,7 @@ async function langMenu() {
|
|
|
408
514
|
async function main() {
|
|
409
515
|
let running = true
|
|
410
516
|
while (running) {
|
|
411
|
-
printHeader()
|
|
517
|
+
await printHeader()
|
|
412
518
|
const choice = await mainMenu()
|
|
413
519
|
|
|
414
520
|
switch (choice) {
|
|
@@ -434,12 +540,6 @@ async function main() {
|
|
|
434
540
|
if (result === 'exit') { console.log(`\n ${C.dim}再见。${C.r}\n`); running = false }
|
|
435
541
|
break
|
|
436
542
|
}
|
|
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
543
|
}
|
|
444
544
|
}
|
|
445
545
|
}
|
|
@@ -451,4 +551,4 @@ main().catch((err) => {
|
|
|
451
551
|
}
|
|
452
552
|
console.error('Error:', err.message)
|
|
453
553
|
process.exit(1)
|
|
454
|
-
})
|
|
554
|
+
})
|