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
@@ -0,0 +1,73 @@
1
+ /**
2
+ * GET /api/backups 列出所有快照
3
+ * POST /api/backups 手动创建备份 { scope }
4
+ * POST /api/backups/restore 恢复 { filename }
5
+ * DELETE /api/backups/:filename 删除
6
+ */
7
+ import { Router, type Request, type Response } from 'express'
8
+ import {
9
+ createManualBackup,
10
+ deleteBackup,
11
+ listBackups,
12
+ restoreBackup,
13
+ type Scope,
14
+ } from '../lib/repository.js'
15
+ import { log } from '../lib/logger.js'
16
+
17
+ const router = Router()
18
+
19
+ router.get('/', async (_req: Request, res: Response) => {
20
+ try {
21
+ const list = await listBackups()
22
+ res.json({ ok: true, data: list })
23
+ } catch (err) {
24
+ const msg = err instanceof Error ? err.message : String(err)
25
+ res.status(500).json({ ok: false, error: msg })
26
+ }
27
+ })
28
+
29
+ router.post('/', async (req: Request, res: Response) => {
30
+ const scope = req.body?.scope as Scope | undefined
31
+ if (!scope || !['bridge', 'credentials', 'settings'].includes(scope)) {
32
+ res.status(400).json({ ok: false, error: 'scope must be bridge | credentials | settings' })
33
+ return
34
+ }
35
+ try {
36
+ const result = await createManualBackup(scope)
37
+ res.json({ ok: true, data: result })
38
+ } catch (err) {
39
+ const msg = err instanceof Error ? err.message : String(err)
40
+ log('ERR', 'backups', msg)
41
+ res.status(500).json({ ok: false, error: msg })
42
+ }
43
+ })
44
+
45
+ router.post('/restore', async (req: Request, res: Response) => {
46
+ const filename = req.body?.filename
47
+ if (typeof filename !== 'string' || !filename) {
48
+ res.status(400).json({ ok: false, error: 'filename required' })
49
+ return
50
+ }
51
+ try {
52
+ const result = await restoreBackup(filename)
53
+ res.json({ ok: true, data: result })
54
+ } catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err)
56
+ log('ERR', 'backups', msg)
57
+ res.status(500).json({ ok: false, error: msg })
58
+ }
59
+ })
60
+
61
+ router.delete('/:filename', async (req: Request, res: Response) => {
62
+ const filename = req.params.filename
63
+ try {
64
+ await deleteBackup(filename)
65
+ res.json({ ok: true })
66
+ } catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err)
68
+ log('ERR', 'backups', msg)
69
+ res.status(500).json({ ok: false, error: msg })
70
+ }
71
+ })
72
+
73
+ export default router
@@ -0,0 +1,197 @@
1
+ /**
2
+ * GET /api/config — 读取 bridge / credentials / settings 三个文件
3
+ * PUT /api/bridge — 写回 bridge(model catalog、defaults、oauth)
4
+ * PUT /api/credentials — 写回 credentials
5
+ * PUT /api/settings — 写回 settings
6
+ * POST /api/config/reset-catalog — 重置模型目录为 DeepSeek 官方
7
+ */
8
+ import { Router, type Request, type Response } from 'express'
9
+ import {
10
+ BRIDGE_FILE,
11
+ CREDENTIALS_FILE,
12
+ BHG_HELPER_DIR,
13
+ SETTINGS_FILE,
14
+ } from '../lib/paths.js'
15
+ import { listBackups, readJson, writeJson } from '../lib/repository.js'
16
+ import { log } from '../lib/logger.js'
17
+ import type { BridgeConfig, ModelEntry } from '../lib/types.js'
18
+
19
+ const router = Router()
20
+
21
+ router.get('/', async (_req: Request, res: Response) => {
22
+ const [bridge, credentials, settings] = await Promise.all([
23
+ readJson(BRIDGE_FILE),
24
+ readJson(CREDENTIALS_FILE),
25
+ readJson(SETTINGS_FILE),
26
+ ])
27
+ const backups = await listBackups().catch(() => [])
28
+ res.json({
29
+ ok: true,
30
+ data: {
31
+ bhgHelperDir: BHG_HELPER_DIR,
32
+ files: {
33
+ bridge: BRIDGE_FILE,
34
+ credentials: CREDENTIALS_FILE,
35
+ settings: SETTINGS_FILE,
36
+ },
37
+ bridge,
38
+ credentials,
39
+ settings,
40
+ backups,
41
+ },
42
+ })
43
+ })
44
+
45
+ router.put('/bridge', async (req: Request, res: Response) => {
46
+ const body = req.body
47
+ if (!body || typeof body !== 'object') {
48
+ res.status(400).json({ ok: false, error: 'body must be an object' })
49
+ return
50
+ }
51
+ try {
52
+ const result = await writeJson('bridge', body)
53
+ log('OK', 'config', 'bridge config updated via UI')
54
+ res.json({ ok: true, data: result })
55
+ } catch (err) {
56
+ const msg = err instanceof Error ? err.message : String(err)
57
+ log('ERR', 'config', `bridge write failed: ${msg}`)
58
+ res.status(500).json({ ok: false, error: msg })
59
+ }
60
+ })
61
+
62
+ router.put('/credentials', async (req: Request, res: Response) => {
63
+ const body = req.body
64
+ if (!body || typeof body !== 'object') {
65
+ res.status(400).json({ ok: false, error: 'body must be an object' })
66
+ return
67
+ }
68
+ try {
69
+ const result = await writeJson('credentials', body)
70
+ log('OK', 'config', 'credentials updated via UI')
71
+ res.json({ ok: true, data: result })
72
+ } catch (err) {
73
+ const msg = err instanceof Error ? err.message : String(err)
74
+ log('ERR', 'config', `credentials write failed: ${msg}`)
75
+ res.status(500).json({ ok: false, error: msg })
76
+ }
77
+ })
78
+
79
+ router.put('/settings', async (req: Request, res: Response) => {
80
+ const body = req.body
81
+ if (!body || typeof body !== 'object') {
82
+ res.status(400).json({ ok: false, error: 'body must be an object' })
83
+ return
84
+ }
85
+ try {
86
+ const result = await writeJson('settings', body)
87
+ log('OK', 'config', 'settings updated via UI')
88
+ res.json({ ok: true, data: result })
89
+ } catch (err) {
90
+ const msg = err instanceof Error ? err.message : String(err)
91
+ log('ERR', 'config', `settings write failed: ${msg}`)
92
+ res.status(500).json({ ok: false, error: msg })
93
+ }
94
+ })
95
+
96
+ /**
97
+ * POST /api/config/reset-catalog
98
+ * 用 DeepSeek 官方模型目录替换 bridge 的 bridgeModelCatalogCache.models
99
+ * 保留 oauthAccount 等其他字段
100
+ * 如果当前激活模型已不存在,自动切到新的 middle default (deepseek-chat)
101
+ */
102
+ router.post('/reset-catalog', async (_req: Request, res: Response) => {
103
+ try {
104
+ const cur = (await readJson(BRIDGE_FILE)) as BridgeConfig | null
105
+ if (!cur) {
106
+ res.status(404).json({ ok: false, error: 'bridge config not found' })
107
+ return
108
+ }
109
+
110
+ const dsModels: ModelEntry[] = [
111
+ mkModel('deepseek-chat', 'DeepSeek V3', 'middle', 'DeepSeek V3 · 通用对话与代码,128K 上下文', 128000, 32000, 64000, 2, 8),
112
+ mkModel('deepseek-reasoner', 'DeepSeek R1', 'large', 'DeepSeek R1 · 强推理,64K 上下文', 64000, 16000, 32000, 4, 16),
113
+ mkModel('deepseek-v4-flash', 'DeepSeek V4 Flash', 'small', 'DeepSeek V4 Flash · 高速低成本版本', 128000, 32000, 64000, 0.5, 2),
114
+ ]
115
+
116
+ const newDefaults = { large: 'deepseek-reasoner', middle: 'deepseek-chat', small: 'deepseek-v4-flash' }
117
+ const validIds = new Set(dsModels.map((m) => m.id))
118
+
119
+ const next: BridgeConfig = {
120
+ ...cur,
121
+ bridgeModelCatalogCache: {
122
+ ...(cur.bridgeModelCatalogCache ?? { models: [], defaults: { large: '', middle: '', small: '' } }),
123
+ models: dsModels,
124
+ defaults: newDefaults,
125
+ },
126
+ additionalModelOptionsCache: dsModels.map((m) => ({
127
+ value: m.id,
128
+ label: m.label,
129
+ description: m.description,
130
+ descriptionForModel: m.description,
131
+ })),
132
+ }
133
+
134
+ const result = await writeJson('bridge', next)
135
+ log('OK', 'config', 'model catalog reset to DeepSeek official')
136
+
137
+ // 检查 settings.json 的激活模型是否仍然有效,无效则改写
138
+ let switchedFrom: string | null = null
139
+ try {
140
+ const settings = (await readJson(SETTINGS_FILE)) as { model?: string } | null
141
+ if (settings && typeof settings.model === 'string' && !validIds.has(settings.model)) {
142
+ switchedFrom = settings.model
143
+ const newSettings = { ...settings, model: newDefaults.middle }
144
+ await writeJson('settings', newSettings)
145
+ log('OK', 'config', `active model switched ${settings.model} → ${newDefaults.middle}`)
146
+ }
147
+ } catch {
148
+ // settings 不存在或读不动就不管
149
+ }
150
+
151
+ res.json({
152
+ ok: true,
153
+ data: { ...result, modelCount: dsModels.length, switchedActiveFrom: switchedFrom },
154
+ })
155
+ } catch (err) {
156
+ const msg = err instanceof Error ? err.message : String(err)
157
+ log('ERR', 'config', `reset-catalog failed: ${msg}`)
158
+ res.status(500).json({ ok: false, error: msg })
159
+ }
160
+ })
161
+
162
+ function mkModel(
163
+ id: string,
164
+ label: string,
165
+ tier: 'large' | 'middle' | 'small',
166
+ description: string,
167
+ context_window: number,
168
+ default_max_tokens: number,
169
+ max_tokens: number,
170
+ inputPer1M: number,
171
+ outputPer1M: number
172
+ ): ModelEntry {
173
+ return {
174
+ id,
175
+ label,
176
+ tier,
177
+ provider: 'DeepSeek',
178
+ context_window,
179
+ default_max_tokens,
180
+ max_tokens,
181
+ pricing: {
182
+ currency: 'CNY',
183
+ quota_type: 'token',
184
+ pricing_group: 'deepseek-official',
185
+ input_per_1m_tokens: inputPer1M,
186
+ output_per_1m_tokens: outputPer1M,
187
+ cache_read_per_1m_tokens: 0,
188
+ cache_write_per_1m_tokens: 0,
189
+ },
190
+ description,
191
+ protocol: 'openai',
192
+ enabled: true,
193
+ available: true,
194
+ }
195
+ }
196
+
197
+ export default router
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 安装管理:在 UI 里跑 `npm install`,实时拉日志
3
+ */
4
+ import { Router } from 'express'
5
+ import { spawn, type ChildProcess } from 'node:child_process'
6
+ import { existsSync, statSync } from 'node:fs'
7
+ import { resolve, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { log } from '../lib/logger.js'
10
+
11
+ const router = Router()
12
+
13
+ // Cockpit 后端跑在 api/dist/ 或 api/ 下,项目根是上一级
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = dirname(__filename)
16
+ const PROJECT_ROOT = resolve(__dirname, '..', '..')
17
+ const NODE_MODULES = resolve(PROJECT_ROOT, 'node_modules')
18
+ const PACKAGE_JSON = resolve(PROJECT_ROOT, 'package.json')
19
+
20
+ // 单实例:同一时间只允许一个 npm install
21
+ let running: ChildProcess | null = null
22
+ let logBuffer: { ts: number; line: string }[] = []
23
+ let sseClients: Set<import('express').Response> = new Set()
24
+
25
+ function pushLog(line: string) {
26
+ if (!line) return
27
+ const entry = { ts: Date.now(), line: line.replace(/\r$/, '') }
28
+ logBuffer.push(entry)
29
+ if (logBuffer.length > 2000) logBuffer.splice(0, logBuffer.length - 2000)
30
+ for (const res of sseClients) {
31
+ try {
32
+ res.write(`data: ${JSON.stringify(entry)}\n\n`)
33
+ } catch { /* dead client */ }
34
+ }
35
+ }
36
+
37
+ async function checkNode() {
38
+ return new Promise<{ ok: boolean; version?: string; error?: string }>((resolveP) => {
39
+ const p = spawn(process.platform === 'win32' ? 'node.exe' : 'node', ['-v'], { shell: false })
40
+ let out = ''
41
+ p.stdout.on('data', (c) => out += c.toString())
42
+ p.on('error', (e) => resolveP({ ok: false, error: e.message }))
43
+ p.on('close', (code) => {
44
+ if (code === 0) resolveP({ ok: true, version: out.trim() })
45
+ else resolveP({ ok: false, error: `node exited with code ${code}` })
46
+ })
47
+ })
48
+ }
49
+
50
+ async function checkNpm() {
51
+ return new Promise<{ ok: boolean; version?: string; error?: string }>((resolveP) => {
52
+ const p = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v'], { shell: true })
53
+ let out = ''
54
+ p.stdout.on('data', (c) => out += c.toString())
55
+ p.on('error', (e) => resolveP({ ok: false, error: e.message }))
56
+ p.on('close', (code) => {
57
+ if (code === 0) resolveP({ ok: true, version: out.trim() })
58
+ else resolveP({ ok: false, error: `npm exited with code ${code}` })
59
+ })
60
+ })
61
+ }
62
+
63
+ router.get('/status', async (_req, res) => {
64
+ const hasNodeModules = existsSync(NODE_MODULES)
65
+ const hasPackageJson = existsSync(PACKAGE_JSON)
66
+ // 检查 node_modules 是否真的完整安装(有 react 包确认不是空壳)
67
+ const hasReact = hasNodeModules && existsSync(resolve(NODE_MODULES, 'react'))
68
+ const nodeModMtime = hasNodeModules ? statSync(NODE_MODULES).mtimeMs : 0
69
+ const pkgMtime = hasPackageJson ? statSync(PACKAGE_JSON).mtimeMs : 0
70
+ // 需要安装的条件:node_modules 不存在、关键包缺失、或 package.json 更新时间晚于 node_modules
71
+ const needsInstall = !hasNodeModules || !hasReact || (pkgMtime > nodeModMtime)
72
+
73
+ const node = await checkNode()
74
+ const npm = await checkNpm()
75
+
76
+ res.json({
77
+ ok: true,
78
+ data: {
79
+ projectRoot: PROJECT_ROOT,
80
+ hasNodeModules,
81
+ hasPackageJson,
82
+ needsInstall,
83
+ isRunning: !!running,
84
+ logCount: logBuffer.length,
85
+ node: { ok: node.ok, version: node.version, error: node.error },
86
+ npm: { ok: npm.ok, version: npm.version, error: npm.error },
87
+ },
88
+ })
89
+ })
90
+
91
+ router.get('/logs', (req, res) => {
92
+ res.setHeader('Content-Type', 'text/event-stream')
93
+ res.setHeader('Cache-Control', 'no-cache')
94
+ res.setHeader('Connection', 'keep-alive')
95
+ res.setHeader('X-Accel-Buffering', 'no')
96
+ res.flushHeaders?.()
97
+
98
+ // 重放历史日志
99
+ for (const e of logBuffer) {
100
+ res.write(`data: ${JSON.stringify(e)}\n\n`)
101
+ }
102
+
103
+ sseClients.add(res)
104
+ const heartbeat = setInterval(() => {
105
+ try { res.write(':heartbeat\n\n') } catch { /* ignore */ }
106
+ }, 15000)
107
+
108
+ req.on('close', () => {
109
+ clearInterval(heartbeat)
110
+ sseClients.delete(res)
111
+ })
112
+ })
113
+
114
+ router.post('/run', (_req, res) => {
115
+ if (running) {
116
+ res.status(409).json({ ok: false, error: 'install already running' })
117
+ return
118
+ }
119
+
120
+ pushLog(`[bridge] spawning npm install in ${PROJECT_ROOT}`)
121
+ const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
122
+ const child = spawn(cmd, ['install', '--no-audit', '--no-fund', '--loglevel=error'], {
123
+ cwd: PROJECT_ROOT,
124
+ shell: true,
125
+ env: { ...process.env, FORCE_COLOR: '0', NPM_CONFIG_COLOR: 'false' },
126
+ })
127
+ running = child
128
+
129
+ child.stdout.on('data', (b) => {
130
+ for (const line of b.toString().split(/\r?\n/)) pushLog(line)
131
+ })
132
+ child.stderr.on('data', (b) => {
133
+ for (const line of b.toString().split(/\r?\n/)) pushLog('[stderr] ' + line)
134
+ })
135
+ child.on('error', (e) => {
136
+ pushLog('[error] ' + e.message)
137
+ log('ERR', 'install', e.message)
138
+ })
139
+ child.on('close', (code) => {
140
+ pushLog(code === 0 ? '[bridge] npm install completed ✓' : `[bridge] npm install failed (exit ${code})`)
141
+ log(code === 0 ? 'OK' : 'ERR', 'install', `npm install exited ${code}`)
142
+ running = null
143
+ })
144
+
145
+ res.json({ ok: true, data: { started: true, pid: child.pid } })
146
+ })
147
+
148
+ router.post('/cancel', (_req, res) => {
149
+ if (!running) {
150
+ res.status(400).json({ ok: false, error: 'no install running' })
151
+ return
152
+ }
153
+ running.kill('SIGTERM')
154
+ pushLog('[bridge] cancel requested')
155
+ res.json({ ok: true, data: { killed: true } })
156
+ })
157
+
158
+ export default router
@@ -0,0 +1,20 @@
1
+ /**
2
+ * GET /api/logs 最近 200 条
3
+ * GET /api/logs/stream SSE 实时推送
4
+ */
5
+ import { Router, type Request, type Response } from 'express'
6
+ import { addSseClient, listLogs } from '../lib/logger.js'
7
+
8
+ const router = Router()
9
+
10
+ router.get('/', (_req: Request, res: Response) => {
11
+ res.json({ ok: true, data: listLogs() })
12
+ })
13
+
14
+ router.get('/stream', (req: Request, res: Response) => {
15
+ addSseClient(res)
16
+ // 不结束响应;客户端断开会触发 close 事件
17
+ void req
18
+ })
19
+
20
+ export default router
@@ -0,0 +1,13 @@
1
+ /**
2
+ * GET /api/providers/presets — 列出预置的 Provider 配置
3
+ */
4
+ import { Router, type Request, type Response } from 'express'
5
+ import { PROVIDER_PRESETS } from '../lib/providers.js'
6
+
7
+ const router = Router()
8
+
9
+ router.get('/presets', (_req: Request, res: Response) => {
10
+ res.json({ ok: true, data: PROVIDER_PRESETS })
11
+ })
12
+
13
+ export default router
@@ -0,0 +1,106 @@
1
+ /**
2
+ * GET /api/relay/status 中转服务运行状态
3
+ * POST /api/relay/start 启动
4
+ * POST /api/relay/stop 停止
5
+ * POST /api/relay/restart 重启
6
+ * GET /api/relay/config 读配置(Key 脱敏)
7
+ * PUT /api/relay/config 写配置
8
+ */
9
+ import { Router, type Request, type Response } from 'express'
10
+ import { isRelayRunning, getRelayInfo, startRelay, stopRelay, restartRelay } from '../relay/server.js'
11
+ import { getRelayConfigPath, loadRelayConfig, saveRelayConfig } from '../relay/config.js'
12
+ import { log } from '../lib/logger.js'
13
+
14
+ const router = Router()
15
+
16
+ router.get('/status', async (_req: Request, res: Response) => {
17
+ const cfg = await loadRelayConfig()
18
+ res.json({
19
+ ok: true,
20
+ data: {
21
+ running: isRelayRunning(),
22
+ info: getRelayInfo(),
23
+ config: {
24
+ port: cfg.port,
25
+ deepseekBaseUrl: cfg.deepseekBaseUrl,
26
+ spoofProvider: cfg.spoofProvider,
27
+ verbose: cfg.verbose,
28
+ deepseekApiKey: cfg.deepseekApiKey ? maskKey(cfg.deepseekApiKey) : '',
29
+ modelMap: cfg.modelMap,
30
+ configPath: getRelayConfigPath(),
31
+ apiKeyConfigured: !!cfg.deepseekApiKey,
32
+ },
33
+ },
34
+ })
35
+ })
36
+
37
+ router.get('/config', async (_req: Request, res: Response) => {
38
+ const cfg = await loadRelayConfig()
39
+ res.json({
40
+ ok: true,
41
+ data: {
42
+ port: cfg.port,
43
+ deepseekBaseUrl: cfg.deepseekBaseUrl,
44
+ spoofProvider: cfg.spoofProvider,
45
+ verbose: cfg.verbose,
46
+ deepseekApiKey: cfg.deepseekApiKey ? maskKey(cfg.deepseekApiKey) : '',
47
+ modelMap: cfg.modelMap,
48
+ },
49
+ })
50
+ })
51
+
52
+ router.put('/config', async (req: Request, res: Response) => {
53
+ const body = req.body
54
+ if (!body || typeof body !== 'object') {
55
+ res.status(400).json({ ok: false, error: 'body must be object' })
56
+ return
57
+ }
58
+ // 接受部分字段
59
+ const update: Record<string, unknown> = {}
60
+ if (typeof body.port === 'number' && body.port > 0 && body.port < 65535) update.port = body.port
61
+ if (typeof body.deepseekBaseUrl === 'string') update.deepseekBaseUrl = body.deepseekBaseUrl.trim()
62
+ if (typeof body.deepseekApiKey === 'string') update.deepseekApiKey = body.deepseekApiKey.trim()
63
+ if (typeof body.spoofProvider === 'string') update.spoofProvider = body.spoofProvider.trim()
64
+ if (typeof body.verbose === 'boolean') update.verbose = body.verbose
65
+ if (body.modelMap && typeof body.modelMap === 'object') update.modelMap = body.modelMap
66
+
67
+ const next = await saveRelayConfig(update as never)
68
+ // 端口或 baseUrl 变了 → 自动重启
69
+ if (isRelayRunning() && (update.port || update.deepseekBaseUrl)) {
70
+ await restartRelay()
71
+ }
72
+ res.json({ ok: true, data: { port: next.port, deepseekBaseUrl: next.deepseekBaseUrl } })
73
+ })
74
+
75
+ router.post('/start', async (_req: Request, res: Response) => {
76
+ try {
77
+ await startRelay()
78
+ res.json({ ok: true })
79
+ } catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err)
81
+ log('ERR', 'relay', `start failed: ${msg}`)
82
+ res.status(500).json({ ok: false, error: msg })
83
+ }
84
+ })
85
+
86
+ router.post('/stop', async (_req: Request, res: Response) => {
87
+ await stopRelay()
88
+ res.json({ ok: true })
89
+ })
90
+
91
+ router.post('/restart', async (_req: Request, res: Response) => {
92
+ try {
93
+ await restartRelay()
94
+ res.json({ ok: true })
95
+ } catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err)
97
+ res.status(500).json({ ok: false, error: msg })
98
+ }
99
+ })
100
+
101
+ function maskKey(k: string): string {
102
+ if (k.length <= 8) return '••••'
103
+ return k.slice(0, 4) + '••••' + k.slice(-4)
104
+ }
105
+
106
+ export default router
package/api/server.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * BHG-helper 后端入口
3
+ * - 3001 端口:API (BHG-helper UI 用)
4
+ * - 8787 端口:中转服务 (Claude Code 用)
5
+ */
6
+ import app from './app.js'
7
+ import { loadRelayConfig } from './relay/config.js'
8
+ import { startRelay, stopRelay } from './relay/server.js'
9
+ import { log } from './lib/logger.js'
10
+
11
+ const PORT = Number(process.env.PORT || 3001)
12
+
13
+ const server = app.listen(PORT, () => {
14
+ log('OK', 'http', `BHG-helper api listening on http://127.0.0.1:${PORT}`)
15
+ })
16
+
17
+ // 启动时如果 BHG-helper relay 配置里有 Key,就自动跑中转
18
+ loadRelayConfig().then((cfg) => {
19
+ if (cfg.deepseekApiKey) {
20
+ startRelay().catch((err) => {
21
+ log('ERR', 'relay', `auto-start failed: ${err instanceof Error ? err.message : String(err)}`)
22
+ })
23
+ } else {
24
+ log('INFO', 'relay', 'no deepseek key configured, relay not auto-started')
25
+ }
26
+ })
27
+
28
+ process.on('SIGTERM', () => {
29
+ log('INFO', 'http', 'SIGTERM received, shutting down')
30
+ stopRelay().catch(() => {})
31
+ server.close(() => process.exit(0))
32
+ })
33
+
34
+ process.on('SIGINT', () => {
35
+ log('INFO', 'http', 'SIGINT received, shutting down')
36
+ stopRelay().catch(() => {})
37
+ server.close(() => process.exit(0))
38
+ })
39
+
40
+ export default app