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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # BHG-helper
2
+
3
+ BHG-helper 是一个本地可视化工具,用于让 Claude Code CLI(`claude.exe`)通过本机中转访问 DeepSeek API。
4
+
5
+ 核心目标:
6
+ - 可视化配置 DeepSeek API Key
7
+ - 本地保存配置,前端读取时自动脱敏
8
+ - 启动 / 停止 / 重启 Anthropic ↔ DeepSeek 中转服务
9
+ - 显示实时事件日志
10
+ - 管理模型名映射,兼容 Claude Code 的模型选择逻辑
11
+
12
+ ## 架构
13
+
14
+ ```
15
+ claude.exe
16
+ ↓ ANTHROPIC_BASE_URL=http://127.0.0.1:8787
17
+ BHG-helper relay
18
+ ↓ Authorization: Bearer <DeepSeek API Key>
19
+ DeepSeek API
20
+ ```
21
+
22
+ BHG-helper 不修改 hosts,不复用其他工具的配置文件,也不依赖外部代理服务。
23
+
24
+ ## 快速开始
25
+
26
+ ```cmd
27
+ scripts\start.bat
28
+ ```
29
+
30
+ 启动后打开:
31
+
32
+ ```text
33
+ http://localhost:5173
34
+ ```
35
+
36
+ 在「中转服务」页:
37
+ 1. 填写 DeepSeek API Key
38
+ 2. 保存配置
39
+ 3. 点击启动
40
+ 4. 新终端运行 `claude`
41
+
42
+ ## API 配置设计
43
+
44
+ BHG-helper 借鉴成熟 CLI 工具的配置方式,但使用自己的目录和文件:
45
+
46
+ ```text
47
+ %USERPROFILE%\.bhg-helper\
48
+ ├── relay.config.json # 中转端口、DeepSeek Key、模型映射
49
+ ├── config.json # 预留运行配置
50
+ ├── credentials\ # 预留凭据目录
51
+ ├── logs\ # 日志目录
52
+ └── backups\ # 备份目录
53
+ ```
54
+
55
+ 关键实践:
56
+ - **配置与程序分离**:程序目录可移动,用户配置保存在用户目录。
57
+ - **密钥服务端保存**:UI 只显示脱敏值,保存时不会用脱敏值覆盖真实密钥。
58
+ - **默认配置合并**:新增模型映射会自动叠加,不破坏用户自定义映射。
59
+ - **环境变量可覆盖**:可通过 `BHG_HELPER_DIR` 指定配置目录。
60
+
61
+ ## 端口
62
+
63
+ - `5173`:BHG-helper UI
64
+ - `3001`:BHG-helper API
65
+ - `8787`:Anthropic ↔ DeepSeek 中转服务
66
+
67
+ ## 常见问题
68
+
69
+ | 现象 | 处理 |
70
+ |---|---|
71
+ | 页面提示 Key 未配置 | 在「中转服务」填 DeepSeek API Key 并保存 |
72
+ | Claude Code 报模型不可用 | 重启 BHG-helper 中转,并在 `/model` 选择 deepseek 模型 |
73
+ | 请求返回 401 | DeepSeek API Key 无效或余额不足 |
74
+ | 端口被占用 | 在「中转服务」修改端口并保存 |
75
+
76
+ ## License
77
+
78
+ MIT
package/api/app.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * BHG-helper API server
3
+ */
4
+ import express, {
5
+ type Request,
6
+ type Response,
7
+ type NextFunction,
8
+ } from 'express'
9
+ import cors from 'cors'
10
+ import { log } from './lib/logger.js'
11
+ import { BHG_HELPER_DIR } from './lib/paths.js'
12
+ import configRoutes from './routes/config.js'
13
+ import backupRoutes from './routes/backups.js'
14
+ import logRoutes from './routes/logs.js'
15
+ import providerRoutes from './routes/providers.js'
16
+ import relayRoutes from './routes/relay.js'
17
+ import installRoutes from './routes/install.js'
18
+
19
+ const app: express.Application = express()
20
+
21
+ app.use(cors())
22
+ app.use(express.json({ limit: '5mb' }))
23
+ app.use(express.urlencoded({ extended: true, limit: '5mb' }))
24
+
25
+ // request log
26
+ app.use((req, _res, next) => {
27
+ if (req.path.startsWith('/api/')) {
28
+ log('INFO', 'http', `${req.method} ${req.path}`)
29
+ }
30
+ next()
31
+ })
32
+
33
+ app.use('/api/config', configRoutes)
34
+ app.use('/api/backups', backupRoutes)
35
+ app.use('/api/logs', logRoutes)
36
+ app.use('/api/providers', providerRoutes)
37
+ app.use('/api/relay', relayRoutes)
38
+ app.use('/api/install', installRoutes)
39
+
40
+ app.get('/api/health', (_req: Request, res: Response) => {
41
+ res.json({ ok: true, data: { bhgHelperDir: BHG_HELPER_DIR } })
42
+ })
43
+
44
+ app.use((error: Error, _req: Request, res: Response, _next: NextFunction) => {
45
+ log('ERR', 'http', error.message)
46
+ res.status(500).json({ ok: false, error: error.message })
47
+ })
48
+
49
+ app.use((req: Request, res: Response) => {
50
+ res.status(404).json({ ok: false, error: `API not found: ${req.path}` })
51
+ })
52
+
53
+ export default app
package/api/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Vercel deploy entry handler, for serverless deployment, please don't modify this file
3
+ */
4
+ import type { VercelRequest, VercelResponse } from '@vercel/node';
5
+ import app from './app.js';
6
+
7
+ export default function handler(req: VercelRequest, res: VercelResponse) {
8
+ return app(req, res);
9
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 内存日志 + SSE 广播
3
+ */
4
+ import type { Response } from 'express'
5
+
6
+ export type LogLevel = 'INFO' | 'OK' | 'WARN' | 'ERR'
7
+
8
+ export interface LogEntry {
9
+ id: number
10
+ ts: string
11
+ level: LogLevel
12
+ scope: string
13
+ message: string
14
+ }
15
+
16
+ const MAX_LOGS = 500
17
+ const buffer: LogEntry[] = []
18
+ let nextId = 1
19
+ const sseClients = new Set<Response>()
20
+
21
+ function nowIso(): string {
22
+ return new Date().toISOString().replace('T', ' ').slice(0, 19)
23
+ }
24
+
25
+ export function log(level: LogLevel, scope: string, message: string): LogEntry {
26
+ const entry: LogEntry = { id: nextId++, ts: nowIso(), level, scope, message }
27
+ buffer.push(entry)
28
+ if (buffer.length > MAX_LOGS) buffer.shift()
29
+
30
+ // 推送给 SSE 客户端
31
+ const payload = `data: ${JSON.stringify(entry)}\n\n`
32
+ for (const res of sseClients) {
33
+ try {
34
+ res.write(payload)
35
+ } catch {
36
+ // ignore
37
+ }
38
+ }
39
+
40
+ // 同时打到 stdout,方便开发时在终端看
41
+ const tag = `[${entry.level}] ${entry.scope}`
42
+ if (level === 'ERR') console.error(tag, message)
43
+ else if (level === 'WARN') console.warn(tag, message)
44
+ else console.log(tag, message)
45
+
46
+ return entry
47
+ }
48
+
49
+ export function listLogs(): LogEntry[] {
50
+ return buffer.slice()
51
+ }
52
+
53
+ export function addSseClient(res: Response): void {
54
+ res.setHeader('Content-Type', 'text/event-stream')
55
+ res.setHeader('Cache-Control', 'no-cache')
56
+ res.setHeader('Connection', 'keep-alive')
57
+ res.setHeader('X-Accel-Buffering', 'no')
58
+ res.flushHeaders?.()
59
+ // 启动时把历史日志先发过去
60
+ for (const entry of buffer) {
61
+ res.write(`data: ${JSON.stringify(entry)}\n\n`)
62
+ }
63
+ sseClients.add(res)
64
+ res.on('close', () => sseClients.delete(res))
65
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * BHG-helper 运行时路径。
3
+ *
4
+ * 设计原则借鉴成熟 CLI 工具:
5
+ * - 项目运行目录与用户数据目录分离
6
+ * - API 配置集中存放
7
+ * - 日志、备份、凭据分目录管理
8
+ * - 可通过环境变量覆盖,便于迁移/打包
9
+ */
10
+ import os from 'node:os'
11
+ import path from 'node:path'
12
+
13
+ const envDir = process.env.BHG_HELPER_DIR?.trim()
14
+ export const BHG_HELPER_DIR = envDir
15
+ ? path.resolve(envDir)
16
+ : path.join(os.homedir(), '.bhg-helper')
17
+
18
+ export const RUNTIME_CONFIG_FILE = path.join(BHG_HELPER_DIR, 'config.json')
19
+ export const RELAY_CONFIG_FILE = path.join(BHG_HELPER_DIR, 'relay.config.json')
20
+ export const CREDENTIALS_DIR = path.join(BHG_HELPER_DIR, 'credentials')
21
+ export const LOGS_DIR = path.join(BHG_HELPER_DIR, 'logs')
22
+ export const BACKUPS_DIR = path.join(BHG_HELPER_DIR, 'backups')
23
+
24
+ // 旧版页面/接口仍引用这些常量;保留为 BHG-helper 自有文件,避免依赖 lucky 目录。
25
+ export const BRIDGE_FILE = path.join(BHG_HELPER_DIR, 'bridge.json')
26
+ export const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json')
27
+ export const SETTINGS_FILE = path.join(BHG_HELPER_DIR, 'settings.json')
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 预置 API Provider 配置
3
+ *
4
+ * 注意:LUCKY 仅作为 geo-bypass 通道使用,**不再使用其模型 API**。
5
+ * 这里的预设只列出"真正能用的"模型服务方。
6
+ */
7
+
8
+ export interface ProviderPreset {
9
+ id: string
10
+ name: string
11
+ protocol: 'anthropic' | 'openai'
12
+ baseUrl?: string
13
+ notes: string
14
+ models: Array<{
15
+ id: string
16
+ label: string
17
+ tier: 'large' | 'middle' | 'small'
18
+ description: string
19
+ }>
20
+ }
21
+
22
+ export const PROVIDER_PRESETS: ProviderPreset[] = [
23
+ {
24
+ id: 'deepseek-official',
25
+ name: 'DeepSeek 官方',
26
+ protocol: 'openai',
27
+ baseUrl: 'https://api.deepseek.com',
28
+ notes:
29
+ 'platform.deepseek.com 申请的 sk-… Key。DeepSeek V3 / R1 / V4-Flash 都是 OpenAI Chat Completions 兼容格式。',
30
+ models: [
31
+ {
32
+ id: 'deepseek-chat',
33
+ label: 'deepseek-chat (V3)',
34
+ tier: 'middle',
35
+ description: 'DeepSeek V3 · 通用对话与代码,128K 上下文',
36
+ },
37
+ {
38
+ id: 'deepseek-reasoner',
39
+ label: 'deepseek-reasoner (R1)',
40
+ tier: 'large',
41
+ description: 'DeepSeek R1 · 强推理,64K 上下文',
42
+ },
43
+ {
44
+ id: 'deepseek-v4-flash',
45
+ label: 'deepseek-v4-flash (V4 Flash)',
46
+ tier: 'small',
47
+ description: 'DeepSeek V4 Flash · 高速低成本版本(如果 API 已上线)',
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ id: 'openai-compatible',
53
+ name: 'OpenAI 兼容端点(自定义)',
54
+ protocol: 'openai',
55
+ notes:
56
+ '任何 OpenAI Chat Completions 兼容服务。预留扩展位——目前主线只用 DeepSeek。',
57
+ models: [
58
+ {
59
+ id: 'deepseek-chat',
60
+ label: 'deepseek-chat',
61
+ tier: 'middle',
62
+ description: '默认转发到自定义端点的 deepseek-chat',
63
+ },
64
+ ],
65
+ },
66
+ ]
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 配置文件的原子读写。
3
+ * 写入前自动复制当前文件到 backups/,保证可回滚。
4
+ */
5
+ import { promises as fs } from 'node:fs'
6
+ import path from 'node:path'
7
+ import {
8
+ BACKUPS_DIR,
9
+ BRIDGE_FILE,
10
+ CREDENTIALS_FILE,
11
+ SETTINGS_FILE,
12
+ } from './paths.js'
13
+ import { log } from './logger.js'
14
+
15
+ export type Scope = 'bridge' | 'credentials' | 'settings'
16
+
17
+ const SCOPE_TO_FILE: Record<Scope, string> = {
18
+ bridge: BRIDGE_FILE,
19
+ credentials: CREDENTIALS_FILE,
20
+ settings: SETTINGS_FILE,
21
+ }
22
+
23
+ const SCOPE_TO_DEFAULT_SCOPE_NAME: Record<Scope, string> = {
24
+ bridge: 'bridge',
25
+ credentials: 'credentials',
26
+ settings: 'settings',
27
+ }
28
+
29
+ async function ensureDir(dir: string): Promise<void> {
30
+ await fs.mkdir(dir, { recursive: true })
31
+ }
32
+
33
+ async function fileExists(p: string): Promise<boolean> {
34
+ try {
35
+ await fs.access(p)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ export async function readJson<T = unknown>(file: string): Promise<T | null> {
43
+ if (!(await fileExists(file))) return null
44
+ const raw = await fs.readFile(file, 'utf-8')
45
+ if (!raw.trim()) return null
46
+ try {
47
+ return JSON.parse(raw) as T
48
+ } catch (err) {
49
+ const msg = err instanceof Error ? err.message : String(err)
50
+ log('ERR', 'repo', `JSON parse failed for ${path.basename(file)}: ${msg}`)
51
+ return null
52
+ }
53
+ }
54
+
55
+ async function backupCurrent(file: string, scope: Scope): Promise<string | null> {
56
+ if (!(await fileExists(file))) return null
57
+ await ensureDir(BACKUPS_DIR)
58
+ const ts = Date.now()
59
+ const base = path.basename(file).replace(/^\./, '')
60
+ const filename = `${base}.${SCOPE_TO_DEFAULT_SCOPE_NAME[scope]}.backup.${ts}`
61
+ const dest = path.join(BACKUPS_DIR, filename)
62
+ await fs.copyFile(file, dest)
63
+ log('INFO', 'repo', `auto-backup → ${filename}`)
64
+ return filename
65
+ }
66
+
67
+ export async function writeJson(
68
+ scope: Scope,
69
+ data: unknown
70
+ ): Promise<{ ok: true; backup: string | null; size: number }> {
71
+ const file = SCOPE_TO_FILE[scope]
72
+ const backup = await backupCurrent(file, scope)
73
+ const json = JSON.stringify(data, null, 2) + '\n'
74
+ // 原子写:写到临时文件再 rename
75
+ const tmp = file + '.tmp'
76
+ await fs.writeFile(tmp, json, 'utf-8')
77
+ await fs.rename(tmp, file)
78
+ const size = Buffer.byteLength(json, 'utf-8')
79
+ log('OK', 'repo', `wrote ${path.basename(file)} (${size} bytes)`)
80
+ return { ok: true, backup, size }
81
+ }
82
+
83
+ export async function listBackups(): Promise<
84
+ Array<{ filename: string; scope: string; size: number; createdAt: string }>
85
+ > {
86
+ await ensureDir(BACKUPS_DIR)
87
+ const entries = await fs.readdir(BACKUPS_DIR, { withFileTypes: true })
88
+ const out: Array<{ filename: string; scope: string; size: number; createdAt: string }> = []
89
+ for (const e of entries) {
90
+ if (!e.isFile()) continue
91
+ if (!e.name.endsWith('.json') && !e.name.startsWith('.')) continue
92
+ const full = path.join(BACKUPS_DIR, e.name)
93
+ const stat = await fs.stat(full)
94
+ // 解析 scope
95
+ let scope = 'unknown'
96
+ if (e.name.startsWith('claude-thkj-bridge')) scope = 'bridge'
97
+ else if (e.name.startsWith('credentials')) scope = 'credentials'
98
+ else if (e.name.startsWith('settings')) scope = 'settings'
99
+ out.push({
100
+ filename: e.name,
101
+ scope,
102
+ size: stat.size,
103
+ createdAt: stat.mtime.toISOString(),
104
+ })
105
+ }
106
+ out.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
107
+ return out
108
+ }
109
+
110
+ export async function deleteBackup(filename: string): Promise<void> {
111
+ // 安全:只允许删除 backups/ 下的文件
112
+ if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
113
+ throw new Error('invalid filename')
114
+ }
115
+ const target = path.join(BACKUPS_DIR, filename)
116
+ await fs.unlink(target)
117
+ log('OK', 'repo', `deleted backup ${filename}`)
118
+ }
119
+
120
+ export async function restoreBackup(filename: string): Promise<{ scope: Scope }> {
121
+ if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
122
+ throw new Error('invalid filename')
123
+ }
124
+ const src = path.join(BACKUPS_DIR, filename)
125
+ if (!(await fileExists(src))) throw new Error('backup not found')
126
+
127
+ let scope: Scope
128
+ if (filename.startsWith('claude-thkj-bridge')) scope = 'bridge'
129
+ else if (filename.startsWith('credentials')) scope = 'credentials'
130
+ else if (filename.startsWith('settings')) scope = 'settings'
131
+ else throw new Error('cannot determine scope from filename')
132
+
133
+ const data = await readJson(src)
134
+ if (data == null) throw new Error('backup file is not valid JSON')
135
+ await writeJson(scope, data)
136
+ log('OK', 'repo', `restored ${filename} → ${scope}`)
137
+ return { scope }
138
+ }
139
+
140
+ export async function createManualBackup(
141
+ scope: Scope
142
+ ): Promise<{ filename: string }> {
143
+ const file = SCOPE_TO_FILE[scope]
144
+ if (!(await fileExists(file))) throw new Error(`${scope} file does not exist`)
145
+ await ensureDir(BACKUPS_DIR)
146
+ const ts = Date.now()
147
+ const base = path.basename(file).replace(/^\./, '')
148
+ const filename = `${base}.manual.${ts}`
149
+ const dest = path.join(BACKUPS_DIR, filename)
150
+ await fs.copyFile(file, dest)
151
+ log('OK', 'repo', `manual backup → ${filename}`)
152
+ return { filename }
153
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 后端类型定义
3
+ */
4
+
5
+ export interface ModelEntry {
6
+ id: string
7
+ label: string
8
+ tier: 'large' | 'middle' | 'small'
9
+ provider: string
10
+ context_window: number
11
+ default_max_tokens: number
12
+ max_tokens: number
13
+ pricing: {
14
+ currency: string
15
+ quota_type: string
16
+ pricing_group: string
17
+ input_per_1m_tokens: number
18
+ output_per_1m_tokens: number
19
+ cache_read_per_1m_tokens: number
20
+ cache_write_per_1m_tokens: number
21
+ }
22
+ description: string
23
+ protocol: 'anthropic' | 'openai'
24
+ enabled: boolean
25
+ available: boolean
26
+ }
27
+
28
+ export interface BridgeConfig {
29
+ bridgeModelCatalogCache?: {
30
+ models: ModelEntry[]
31
+ defaults: { large: string; middle: string; small: string }
32
+ pricing_version?: string
33
+ pricing_group?: string
34
+ }
35
+ additionalModelOptionsCache?: Array<{
36
+ value: string
37
+ label: string
38
+ description: string
39
+ descriptionForModel: string
40
+ }>
41
+ oauthAccount?: null | Record<string, unknown>
42
+ [key: string]: unknown
43
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * BHG-helper API 配置模块。
3
+ *
4
+ * 借鉴成熟 CLI 工具的配置设计:
5
+ * - 配置统一落在用户级运行目录,和程序文件分离
6
+ * - API Key 只在服务端保存,前端读取时脱敏
7
+ * - modelMap 使用默认值 + 用户覆盖的合并策略
8
+ * - 端口、上游地址、Provider 标识集中管理
9
+ */
10
+ import { promises as fs } from 'node:fs'
11
+ import { BHG_HELPER_DIR, RELAY_CONFIG_FILE } from '../lib/paths.js'
12
+ import { log } from '../lib/logger.js'
13
+
14
+ export interface RelayConfig {
15
+ port: number
16
+ deepseekApiKey: string
17
+ deepseekBaseUrl: string
18
+ modelMap: Record<string, string>
19
+ spoofProvider: string
20
+ verbose: boolean
21
+ }
22
+
23
+ const DEFAULT_CONFIG: RelayConfig = {
24
+ port: 8787,
25
+ deepseekApiKey: '',
26
+ deepseekBaseUrl: 'https://api.deepseek.com',
27
+ modelMap: {
28
+ 'deepseek-v4-flash': 'deepseek-v4-flash',
29
+ 'deepseek-v4-pro': 'deepseek-v4-pro',
30
+ 'deepseek-chat': 'deepseek-v4-pro',
31
+ 'deepseek-reasoner': 'deepseek-v4-pro',
32
+ 'deepseek-v4-flash-cc': 'deepseek-v4-flash',
33
+ 'deepseek-v4-pro-cc': 'deepseek-v4-pro',
34
+ 'claude-opus-4-6': 'deepseek-v4-pro',
35
+ 'claude-opus-4-6[1m]': 'deepseek-v4-pro',
36
+ 'claude-opus-4-7': 'deepseek-v4-pro',
37
+ 'claude-opus-4-7[1m]': 'deepseek-v4-pro',
38
+ 'claude-opus-4-8': 'deepseek-v4-pro',
39
+ 'claude-opus-4-8[1m]': 'deepseek-v4-pro',
40
+ 'claude-sonnet-4-6': 'deepseek-v4-pro',
41
+ 'claude-sonnet-4-6[1m]': 'deepseek-v4-pro',
42
+ 'claude-haiku-4-5-20251001': 'deepseek-v4-flash',
43
+ 'claude-haiku-4-5': 'deepseek-v4-flash',
44
+ },
45
+ spoofProvider: 'deepseek',
46
+ verbose: true,
47
+ }
48
+
49
+ export async function loadRelayConfig(): Promise<RelayConfig> {
50
+ let onDisk: Partial<RelayConfig> = {}
51
+ try {
52
+ const raw = await fs.readFile(RELAY_CONFIG_FILE, 'utf-8')
53
+ onDisk = JSON.parse(raw.replace(/^\uFEFF/, ''))
54
+ } catch {
55
+ return DEFAULT_CONFIG
56
+ }
57
+
58
+ return {
59
+ ...DEFAULT_CONFIG,
60
+ ...onDisk,
61
+ modelMap: { ...DEFAULT_CONFIG.modelMap, ...(onDisk.modelMap ?? {}) },
62
+ }
63
+ }
64
+
65
+ export async function saveRelayConfig(cfg: Partial<RelayConfig>): Promise<RelayConfig> {
66
+ const cur = await loadRelayConfig()
67
+ const next = { ...cur, ...cfg }
68
+ await fs.mkdir(BHG_HELPER_DIR, { recursive: true })
69
+ await fs.writeFile(RELAY_CONFIG_FILE, JSON.stringify(next, null, 2) + '\n', 'utf-8')
70
+ log('OK', 'relay', `relay config saved (port=${next.port}, base=${next.deepseekBaseUrl}, mapSize=${Object.keys(next.modelMap).length})`)
71
+ return next
72
+ }
73
+
74
+ export function getRelayConfigPath(): string {
75
+ return RELAY_CONFIG_FILE
76
+ }