bingocode 1.0.18 → 1.0.20

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/bin/claude CHANGED
@@ -9,7 +9,7 @@ cd "$ROOT_DIR"
9
9
 
10
10
  # When spawned by the desktop/web server as a child CLI process,
11
11
  # skip .env loading — the server has already set the correct env
12
- # via cc-haha/settings.json. Loading .env would re-inject stale
12
+ # via bingo/settings.json. Loading .env would re-inject stale
13
13
  # provider keys (e.g., a MiniMax key as ANTHROPIC_API_KEY) that
14
14
  # override the active provider config.
15
15
  if [[ "${CC_HAHA_SKIP_DOTENV:-0}" == "1" ]]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -50,7 +50,8 @@ type Stage =
50
50
  | 'editing'
51
51
  | 'slot_config'
52
52
  | 'slot_loading'
53
- | 'slot_select_model';
53
+ | 'slot_select_model'
54
+ | 'slot_input_label';
54
55
 
55
56
  export const ProviderPanel: React.FC<{
56
57
  apiUrl: string;
@@ -82,11 +83,14 @@ export const ProviderPanel: React.FC<{
82
83
  const [editKey, setEditKey] = useState('');
83
84
 
84
85
  // 槽位配置状态
85
- type SlotEntry = { providerId: string; modelId: string } | null;
86
+ type SlotEntry = { providerId: string; modelId: string; label?: string | null } | null;
86
87
  const [slotTable, setSlotTable] = useState<Record<string, SlotEntry>>({});
87
88
  const [slotProviderModels, setSlotProviderModels] = useState<Record<string, string[]>>({});
88
89
  const [currentSlotName, setCurrentSlotName] = useState<string>('main');
89
90
  const [slotLoadingMsg, setSlotLoadingMsg] = useState<string>('');
91
+ const [tempSlotProviderId, setTempSlotProviderId] = useState<string>('');
92
+ const [tempSlotModelId, setTempSlotModelId] = useState<string>('');
93
+ const [slotLabelInput, setSlotLabelInput] = useState<string>('');
90
94
 
91
95
  const base = apiUrl.replace(/\/+$/, '');
92
96
 
@@ -609,7 +613,8 @@ export const ProviderPanel: React.FC<{
609
613
  const providerName = entry
610
614
  ? (providers.find(p => p.id === entry.providerId)?.name || entry.providerId)
611
615
  : null;
612
- const status = entry ? `${providerName} / ${entry.modelId}` : '未配置';
616
+ const modelDisplayName = entry?.label || entry?.modelId || '未配置';
617
+ const status = entry ? `${providerName} / ${modelDisplayName}` : '未配置';
613
618
  const label = `[${s}] ${status} — ${SLOT_DESCS[s]}`;
614
619
  return { label, value: s };
615
620
  });
@@ -700,10 +705,42 @@ export const ProviderPanel: React.FC<{
700
705
  const sepIdx = val.indexOf('::');
701
706
  const providerId = val.slice(0, sepIdx);
702
707
  const modelId = val.slice(sepIdx + 2);
703
- axios.put(`${base}/api/providers/slots/${currentSlotName}`, { providerId, modelId })
708
+ setTempSlotProviderId(providerId);
709
+ setTempSlotModelId(modelId);
710
+ setSlotLabelInput(modelId); // 默认建议使用模型名作为 Label
711
+ setStage('slot_input_label');
712
+ }}
713
+ />
714
+ <Text dimColor>↑↓ 选择模型,回车确认,ESC 返回</Text>
715
+ </Box>
716
+ );
717
+ }
718
+
719
+ if (stage === 'slot_input_label') {
720
+ return (
721
+ <Box flexDirection="column">
722
+ <Text color="cyan">配置槽位 [{currentSlotName}] — 设置显示名称</Text>
723
+ <Text>
724
+ 模型:{providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
725
+ </Text>
726
+ <Box marginTop={1}>
727
+ <Text>显示名称 (Label):</Text>
728
+ <TextInput
729
+ value={slotLabelInput}
730
+ onChange={setSlotLabelInput}
731
+ onSubmit={() => {
732
+ const label = slotLabelInput.trim() || tempSlotModelId;
733
+ axios.put(`${base}/api/providers/slots/${currentSlotName}`, {
734
+ providerId: tempSlotProviderId,
735
+ modelId: tempSlotModelId,
736
+ label,
737
+ })
704
738
  .then(() => {
705
- setSlotTable(prev => ({ ...prev, [currentSlotName]: { providerId, modelId } }));
706
- setOpMsg(`已配置 [${currentSlotName}] -> ${providers.find(p => p.id === providerId)?.name || providerId} / ${modelId}`);
739
+ setSlotTable(prev => ({
740
+ ...prev,
741
+ [currentSlotName]: { providerId: tempSlotProviderId, modelId: tempSlotModelId, label }
742
+ }));
743
+ setOpMsg(`已配置 [${currentSlotName}] -> ${label}`);
707
744
  setErr(null);
708
745
  setStage('slot_config');
709
746
  })
@@ -711,9 +748,10 @@ export const ProviderPanel: React.FC<{
711
748
  setErr((e as any)?.response?.data?.message || (e as any)?.message || '保存失败');
712
749
  setStage('slot_config');
713
750
  });
714
- }}
715
- />
716
- <Text dimColor>↑↓ 选择模型,回车确认,ESC 返回</Text>
751
+ }}
752
+ />
753
+ </Box>
754
+ <Text dimColor>回车保存组件名称(显示在 Claude UI),ESC 返回模型选择</Text>
717
755
  </Box>
718
756
  );
719
757
  }
@@ -1,3 +1,4 @@
1
+ import { mkdirSync } from 'node:fs'
1
2
  import { profileCheckpoint } from '../utils/startupProfiler.js'
2
3
  import '../bootstrap/state.js'
3
4
  import '../utils/config.js'
@@ -25,7 +26,7 @@ import { logForDebugging } from '../utils/debug.js'
25
26
  import { detectCurrentRepository } from '../utils/detectRepository.js'
26
27
  import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
27
28
  import { initJetBrainsDetection } from '../utils/envDynamic.js'
28
- import { isEnvTruthy } from '../utils/envUtils.js'
29
+ import { getClaudeConfigHomeDir, isEnvTruthy } from '../utils/envUtils.js'
29
30
  import { ConfigParseError, errorMessage } from '../utils/errors.js'
30
31
  // showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
31
32
  import {
@@ -55,6 +56,13 @@ import { setShellIfWindows } from '../utils/windowsPaths.js'
55
56
  let telemetryInitialized = false
56
57
 
57
58
  export const init = memoize(async (): Promise<void> => {
59
+ // Ensure config directory exists for first-time use
60
+ try {
61
+ mkdirSync(getClaudeConfigHomeDir(), { recursive: true });
62
+ } catch (e) {
63
+ // Ignore error if it already exists
64
+ }
65
+
58
66
  const initStartTime = Date.now()
59
67
  logForDiagnosticsNoPII('info', 'init_started')
60
68
  profileCheckpoint('init_function_start')
@@ -1,7 +1,7 @@
1
1
  //@C:M ID=M.CM.CliMenuManager;K=M;V=1.5;P=module;D=CLI;M=cli;S=main
2
2
  import React, { useState, useEffect, useMemo } from 'react';
3
3
  import axios from 'axios';
4
- import { Box, Text, useApp, useInput } from 'ink';
4
+ import { Box, Text, useApp, useInput, useStdout } from 'ink';
5
5
  import SelectInput from 'ink-select-input';
6
6
  import ProviderPanel from '../cli/ProviderPanel.tsx';
7
7
  import { LogoV2 } from '../components/LogoV2/LogoV2.tsx';
@@ -24,9 +24,51 @@ import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
24
24
  // markedSessions 存到 ~/.claude-cli/ 固定目录,不受 cwd 影响
25
25
  const MARKED_FILE = path.join(os.homedir(), '.claude-cli', 'markedSessions.json');
26
26
 
27
- // 固定尺寸视口(可用环境变量覆盖)
28
- const VIEW_W = Number(process.env.CLI_VIEW_W || 96);
29
- const VIEW_H = Number(process.env.CLI_VIEW_H || 30);
27
+ /**
28
+ * 判断是否处于"官方"模式(没有激活任何自定义 provider)。
29
+ * 逻辑与 ConversationService.shouldMarkManagedOAuth() 保持一致。
30
+ */
31
+ function isOfficialMode(): boolean {
32
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
33
+ const settingsPath = path.join(configDir, 'bingo', 'settings.json');
34
+ try {
35
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
36
+ const parsed = JSON.parse(raw) as { env?: Record<string, string> };
37
+ const env = parsed.env ?? {};
38
+ const hasProviderEnv = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL']
39
+ .some(key => typeof env[key] === 'string' && env[key]!.trim().length > 0);
40
+ return !hasProviderEnv;
41
+ } catch {
42
+ return true; // 读不到 settings.json → 按官方模式处理
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 构造子进程的 spawn env。
48
+ * 官方模式下注入 CLAUDE_CODE_ENTRYPOINT=claude-desktop + CLAUDE_CODE_OAUTH_TOKEN,
49
+ * 使新建/恢复的 bingocode 窗口能直接走 OAuth,不显示"未登录"。
50
+ */
51
+ async function buildSpawnEnv(): Promise<NodeJS.ProcessEnv> {
52
+ const base = { ...process.env };
53
+ if (!isOfficialMode()) return base;
54
+
55
+ // 官方模式:标记为 managed-OAuth,并注入 OAuth token
56
+ base.CLAUDE_CODE_ENTRYPOINT = 'claude-desktop';
57
+ try {
58
+ const { hahaOAuthService } = await import('../server/services/hahaOAuthService.js');
59
+ const token = await hahaOAuthService.ensureFreshAccessToken();
60
+ if (token) {
61
+ base.CLAUDE_CODE_OAUTH_TOKEN = token;
62
+ } else {
63
+ // 没有有效 token 时不注入,让 CLI 走正常登录流程
64
+ delete base.CLAUDE_CODE_OAUTH_TOKEN;
65
+ }
66
+ } catch {
67
+ delete base.CLAUDE_CODE_OAUTH_TOKEN;
68
+ }
69
+ return base;
70
+ }
71
+
30
72
  // 顶部高度:首页适配 LogoV2 + Toolbar,其它页面更紧凑
31
73
  const TOP_H_HOME = Number(process.env.CLI_TOP_H_HOME || 9);
32
74
  const TOP_H_COMPACT = Number(process.env.CLI_TOP_H_COMPACT || 6);
@@ -107,6 +149,27 @@ type ChatMessage = {
107
149
 
108
150
  //@C:F ID=F.CM.CliMenuManager;K=F;V=1.5;P=CLI 主菜单;D=CLI;M=cli;S=main;In=;Out=JSX.Element
109
151
  export const CliMenuManager: React.FC = () => {
152
+ const { stdout } = useStdout();
153
+ const [terminalSize, setTerminalSize] = useState({
154
+ columns: stdout?.columns || 80,
155
+ rows: stdout?.rows || 24
156
+ });
157
+
158
+ useEffect(() => {
159
+ const onResize = () => {
160
+ setTerminalSize({
161
+ columns: stdout?.columns || 80,
162
+ rows: stdout?.rows || 24
163
+ });
164
+ };
165
+ stdout?.on('resize', onResize);
166
+ return () => { stdout?.off('resize', onResize); };
167
+ }, [stdout]);
168
+
169
+ // 动态视口(默认优先使用环境变量,否则使用当前终端宽度并留一点余量)
170
+ const VIEW_W = Number(process.env.CLI_VIEW_W || Math.min(terminalSize.columns, 96));
171
+ const VIEW_H = Number(process.env.CLI_VIEW_H || terminalSize.rows);
172
+
110
173
  const [apiUrl, setApiUrl] = useState<string | null>(process.env.BASE_API_URL || null);
111
174
  const [stopIfLast, setStopIfLast] = useState<null | (() => Promise<void>)>(null);
112
175
  const [bootErr, setBootErr] = useState<string | null>(null);
@@ -259,9 +322,10 @@ export const CliMenuManager: React.FC = () => {
259
322
  const spawnCmd = isWin ? 'cmd' : 'sh';
260
323
  // Windows 直接调全局 bingocode 命令,不用 bun 前缀
261
324
  const spawnArgs = isWin ? ['/c', 'start', 'cmd', '/k', 'bingocode'] : ['-c', `${binName}`];
325
+ const spawnEnv = await buildSpawnEnv();
262
326
  spawn(spawnCmd, spawnArgs, {
263
327
  cwd: process.env.CALLER_DIR || process.cwd(),
264
- env: process.env,
328
+ env: spawnEnv,
265
329
  detached: true,
266
330
  stdio: 'ignore'
267
331
  }).unref();
@@ -524,7 +588,7 @@ export const CliMenuManager: React.FC = () => {
524
588
  }
525
589
 
526
590
  // 新增:会话恢复(供快捷键和右侧菜单复用)
527
- function resumeSession(sessionId: string) {
591
+ async function resumeSession(sessionId: string) {
528
592
  try {
529
593
  const fsReq = require('fs');
530
594
  const pathReq = require('path');
@@ -542,9 +606,10 @@ export const CliMenuManager: React.FC = () => {
542
606
  const spawnArgs = isWin
543
607
  ? ['/c', 'start', 'cmd', '/k', `bingocode --resume ${sessionId}`]
544
608
  : ['-c', `${binName} --resume ${sessionId}`];
609
+ const spawnEnv = await buildSpawnEnv();
545
610
  spawn(spawnCmd, spawnArgs, {
546
611
  cwd: process.env.CALLER_DIR || process.cwd(),
547
- env: process.env,
612
+ env: spawnEnv,
548
613
  detached: true,
549
614
  stdio: 'ignore'
550
615
  }).unref();
@@ -753,7 +818,7 @@ export const CliMenuManager: React.FC = () => {
753
818
  <Box flexDirection="column" width={VIEW_W} height={MID_H}>
754
819
  {creating && <Text color="yellow">新建中...</Text>}
755
820
  {createErr && <Text color="red">新建失败: {createErr}</Text>}
756
- {newSessionId && <Text color="green">新建会话(本地Id,仅演示): {newSessionId}</Text>}
821
+ {newSessionId && <Text color="green">新建会话: {newSessionId}</Text>}
757
822
  {!creating && !createErr && !newSessionId && <Text dimColor>已进入新建会话页,等待创建结果...</Text>}
758
823
  </Box>
759
824
  );
@@ -923,6 +988,17 @@ export const CliMenuManager: React.FC = () => {
923
988
  return <Box width={VIEW_W} height={MID_H} />;
924
989
  }
925
990
 
991
+ // 退出逻辑
992
+ if (terminalSize.columns < 60 || terminalSize.rows < 15) {
993
+ return (
994
+ <Box flexDirection="column" padding={2}>
995
+ <Text color="red">终端窗口太小! / Terminal too small!</Text>
996
+ <Text>当前 / Current: {terminalSize.columns}x{terminalSize.rows}</Text>
997
+ <Text>请调节窗口大小以继续... / Please resize to continue...</Text>
998
+ </Box>
999
+ );
1000
+ }
1001
+
926
1002
  // 根渲染:上-中-下三段
927
1003
  return (
928
1004
  <Box flexDirection="column" width={VIEW_W}>
@@ -14,7 +14,7 @@ describe('ConversationService', () => {
14
14
  let originalOAuthToken: string | undefined
15
15
 
16
16
  beforeEach(async () => {
17
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cc-haha-conversation-service-'))
17
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bingo-conversation-service-'))
18
18
  originalConfigDir = process.env.CLAUDE_CONFIG_DIR
19
19
  originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
20
20
  originalBaseUrl = process.env.ANTHROPIC_BASE_URL
@@ -56,7 +56,7 @@ describe('ConversationService', () => {
56
56
 
57
57
  test('keeps inherited provider env when no desktop provider config exists', async () => {
58
58
  const service = new ConversationService() as any
59
- const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
59
+ const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\bingo')) as Record<string, string>
60
60
 
61
61
  expect(env.ANTHROPIC_AUTH_TOKEN).toBe('test-token')
62
62
  expect(env.ANTHROPIC_BASE_URL).toBe('https://example.invalid/anthropic')
@@ -64,7 +64,7 @@ describe('ConversationService', () => {
64
64
  })
65
65
 
66
66
  test('strips inherited provider env when desktop provider config exists', async () => {
67
- const ccHahaDir = path.join(tmpDir, 'cc-haha')
67
+ const ccHahaDir = path.join(tmpDir, 'bingo')
68
68
  await fs.mkdir(ccHahaDir, { recursive: true })
69
69
  await fs.writeFile(
70
70
  path.join(ccHahaDir, 'providers.json'),
@@ -73,7 +73,7 @@ describe('ConversationService', () => {
73
73
  )
74
74
 
75
75
  const service = new ConversationService() as any
76
- const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
76
+ const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\bingo')) as Record<string, string>
77
77
 
78
78
  expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
79
79
  expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
@@ -81,7 +81,7 @@ describe('ConversationService', () => {
81
81
  })
82
82
 
83
83
  test('buildChildEnv injects CLAUDE_CODE_OAUTH_TOKEN when official mode + haha oauth token exists', async () => {
84
- const ccHahaDir = path.join(tmpDir, 'cc-haha')
84
+ const ccHahaDir = path.join(tmpDir, 'bingo')
85
85
  await fs.mkdir(ccHahaDir, { recursive: true })
86
86
  await fs.writeFile(
87
87
  path.join(ccHahaDir, 'settings.json'),
@@ -106,7 +106,7 @@ describe('ConversationService', () => {
106
106
  })
107
107
 
108
108
  test('buildChildEnv does NOT inject CLAUDE_CODE_OAUTH_TOKEN when not official mode', async () => {
109
- const ccHahaDir = path.join(tmpDir, 'cc-haha')
109
+ const ccHahaDir = path.join(tmpDir, 'bingo')
110
110
  await fs.mkdir(ccHahaDir, { recursive: true })
111
111
  await fs.writeFile(
112
112
  path.join(ccHahaDir, 'settings.json'),
@@ -131,7 +131,7 @@ describe('ConversationService', () => {
131
131
  })
132
132
 
133
133
  test('buildChildEnv does not leak inherited CLAUDE_CODE_OAUTH_TOKEN when official token is unavailable', async () => {
134
- const ccHahaDir = path.join(tmpDir, 'cc-haha')
134
+ const ccHahaDir = path.join(tmpDir, 'bingo')
135
135
  await fs.mkdir(ccHahaDir, { recursive: true })
136
136
  await fs.writeFile(
137
137
  path.join(ccHahaDir, 'settings.json'),
@@ -49,7 +49,7 @@ describe('HahaOAuthService — file storage', () => {
49
49
  }
50
50
  await service.saveTokens(tokens)
51
51
 
52
- const oauthPath = path.join(tmpDir, 'cc-haha', 'oauth.json')
52
+ const oauthPath = path.join(tmpDir, 'bingo', 'oauth.json')
53
53
  const stat = await fs.stat(oauthPath)
54
54
  expect(stat.mode & 0o777).toBe(0o600)
55
55
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 用真实的 Provider 配置测试 ProviderService
3
- * 验证添加、激活、cc-haha/settings.json 同步是否正确
4
- * (provider env 写到 ~/.claude/cc-haha/settings.json,不污染原版 settings.json)
3
+ * 验证添加、激活、bingo/settings.json 同步是否正确
4
+ * (provider env 写到 ~/.claude/bingo/settings.json,不污染原版 settings.json)
5
5
  */
6
6
 
7
7
  import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
@@ -34,7 +34,7 @@ describe('Real Provider Configs', () => {
34
34
 
35
35
  // Helper: read the Haha-specific settings file
36
36
  async function readCcHahaSettings(): Promise<Record<string, unknown>> {
37
- const raw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8')
37
+ const raw = await fs.readFile(path.join(tmpDir, 'bingo', 'settings.json'), 'utf-8')
38
38
  return JSON.parse(raw)
39
39
  }
40
40
 
@@ -48,7 +48,7 @@ describe('Real Provider Configs', () => {
48
48
  }
49
49
  }
50
50
 
51
- test('添加 MiniMax Provider 并激活 — 写入 cc-haha/settings.json', async () => {
51
+ test('添加 MiniMax Provider 并激活 — 写入 bingo/settings.json', async () => {
52
52
  const minimax = await service.addProvider({
53
53
  presetId: 'minimax',
54
54
  name: 'MiniMax',
@@ -63,7 +63,7 @@ describe('Real Provider Configs', () => {
63
63
  // 激活 provider
64
64
  await service.activateProvider(minimax.id)
65
65
 
66
- // 验证写入 cc-haha/settings.json
66
+ // 验证写入 bingo/settings.json
67
67
  const settings = await readCcHahaSettings()
68
68
  expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
69
69
  expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
@@ -72,10 +72,10 @@ describe('Real Provider Configs', () => {
72
72
  // 验证原版 settings.json 没有被创建
73
73
  expect(await originalSettingsExists()).toBe(false)
74
74
 
75
- console.log('✅ Provider 写入 cc-haha/settings.json,原版 settings.json 未被污染')
75
+ console.log('✅ Provider 写入 bingo/settings.json,原版 settings.json 未被污染')
76
76
  })
77
77
 
78
- test('切换 Provider — 更新 cc-haha/settings.json', async () => {
78
+ test('切换 Provider — 更新 bingo/settings.json', async () => {
79
79
  const minimax = await service.addProvider({
80
80
  presetId: 'minimax',
81
81
  name: 'MiniMax',
@@ -116,14 +116,14 @@ describe('Real Provider Configs', () => {
116
116
  // 原版 settings.json 依然不存在
117
117
  expect(await originalSettingsExists()).toBe(false)
118
118
 
119
- console.log('✅ 切换 Provider 成功,cc-haha/settings.json 更新正确')
119
+ console.log('✅ 切换 Provider 成功,bingo/settings.json 更新正确')
120
120
  })
121
121
 
122
- test('cc-haha/settings.json 保留已有字段', async () => {
123
- // 预写一个有内容的 cc-haha/settings.json(模拟用户已有配置)
124
- await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
122
+ test('bingo/settings.json 保留已有字段', async () => {
123
+ // 预写一个有内容的 bingo/settings.json(模拟用户已有配置)
124
+ await fs.mkdir(path.join(tmpDir, 'bingo'), { recursive: true })
125
125
  await fs.writeFile(
126
- path.join(tmpDir, 'cc-haha', 'settings.json'),
126
+ path.join(tmpDir, 'bingo', 'settings.json'),
127
127
  JSON.stringify({
128
128
  customField: 'should_be_preserved',
129
129
  env: {
@@ -157,7 +157,7 @@ describe('Real Provider Configs', () => {
157
157
  expect(settings.customField).toBe('should_be_preserved')
158
158
  expect((settings.env as Record<string, string>).EXISTING_VAR).toBe('should_be_preserved')
159
159
 
160
- console.log('✅ cc-haha/settings.json 已有字段全部保留')
160
+ console.log('✅ bingo/settings.json 已有字段全部保留')
161
161
  })
162
162
 
163
163
  test('activateOfficial 清除 provider env', async () => {
@@ -205,7 +205,7 @@ describe('Real Provider Configs', () => {
205
205
  console.log(' error:', result.connectivity.error)
206
206
  })
207
207
 
208
- test('providers.json 和 cc-haha/settings.json 独立于 settings.json', async () => {
208
+ test('providers.json 和 bingo/settings.json 独立于 settings.json', async () => {
209
209
  // 模拟原版 Claude Code 的 settings.json 已存在
210
210
  await fs.writeFile(
211
211
  path.join(tmpDir, 'settings.json'),
@@ -234,7 +234,7 @@ describe('Real Provider Configs', () => {
234
234
  expect((original.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('original-key')
235
235
  expect(original.effortLevel).toBe('high')
236
236
 
237
- // 验证 cc-haha/settings.json 是 Haha 自己的
237
+ // 验证 bingo/settings.json 是 Haha 自己的
238
238
  const haha = await readCcHahaSettings()
239
239
  expect((haha.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
240
240
  expect((haha.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-haha-key')
@@ -109,7 +109,7 @@ const DEFAULT_CONFIG: ComputerUseConfig = {
109
109
  grantFlags: { clipboardRead: true, clipboardWrite: true, systemKeyCombos: true },
110
110
  }
111
111
 
112
- const configPath = join(claudeHome, 'cc-haha', 'computer-use-config.json')
112
+ const configPath = join(claudeHome, 'bingo', 'computer-use-config.json')
113
113
 
114
114
  //@C:ID=F.CU.getRequirementsPath;K=F;V=1.0;P=Get path to requirements file;D=System;M=ComputerUse;S=Paths;In=void;Out=string
115
115
  // Paths that resolve correctly in both dev and bundled modes
@@ -393,7 +393,7 @@ async function runSetup(): Promise<SetupResult> {
393
393
 
394
394
  //@C:ID=F.CU.loadConfig;K=F;V=1.0;P=Load computer use configuration;D=API;M=ComputerUse;S=Authorization;In=void;Out=Promise<ComputerUseConfig>
395
395
  // ============================================================================
396
- // Authorized Apps configuration — stored in ~/.claude/cc-haha/computer-use-config.json
396
+ // Authorized Apps configuration — stored in ~/.claude/bingo/computer-use-config.json
397
397
  // ============================================================================
398
398
  async function loadConfig(): Promise<ComputerUseConfig> {
399
399
  console.log("F.CU.loadConfig");
@@ -81,8 +81,12 @@ export async function handleProvidersApi(
81
81
  const parsed = SlotNameSchema.safeParse(action)
82
82
  if (!parsed.success) throw ApiError.badRequest(`Invalid slot name: ${action}. Must be one of main, haiku, sonnet, opus`)
83
83
  const body = await parseJsonBody(req)
84
- // body can be { providerId, modelId } or null
85
- const entry = body === null ? null : { providerId: String(body.providerId), modelId: String(body.modelId) }
84
+ // body can be { providerId, modelId, label } or null
85
+ const entry = body === null ? null : {
86
+ providerId: String(body.providerId),
87
+ modelId: String(body.modelId),
88
+ label: body.label ? String(body.label) : null,
89
+ }
86
90
  const result = await providerService.setSlot(parsed.data as SlotName, entry)
87
91
  return Response.json(result)
88
92
  }
@@ -4,7 +4,7 @@ import os from 'os'
4
4
 
5
5
  // 统一读取与服务端一致的 providers.json 位置
6
6
  const home = process.env.CLAUDE_CONFIG_DIR || os.homedir();
7
- const configPath = path.resolve(home, '.claude', 'cc-haha', 'providers.json')
7
+ const configPath = path.resolve(home, '.claude', 'bingo', 'providers.json')
8
8
 
9
9
  async function main() {
10
10
  try {
@@ -6,7 +6,7 @@ import { ProvidersIndex, SavedProvider, CreateProviderInput, UpdateProviderInput
6
6
 
7
7
  // 统一与服务端一致的 provider 存储路径
8
8
  const home = process.env.CLAUDE_CONFIG_DIR || os.homedir();
9
- const PROVIDERS_PATH = path.resolve(home, '.claude', 'cc-haha', 'providers.json');
9
+ const PROVIDERS_PATH = path.resolve(home, '.claude', 'bingo', 'providers.json');
10
10
 
11
11
  export class ProviderManager {
12
12
  static async load(): Promise<ProvidersIndex> {
@@ -121,14 +121,13 @@ export class ProviderManager {
121
121
  }
122
122
  }
123
123
 
124
- // 预设列表(可选,providerPresets 不存在则返回空)
124
+ // 预设列表
125
125
  static async listPresets(): Promise<any[]> {
126
126
  try {
127
- // 相对 src/server/cli config 的路径
128
- const mod = await import('../config/providerPresets.ts');
129
- const presets = (mod as any).presets || (mod as any).providerPresets || [];
130
- return presets;
131
- } catch {
127
+ const { loadProviderPresets } = await import('../config/providerPresets.ts');
128
+ return await loadProviderPresets();
129
+ } catch (e) {
130
+ console.error('Failed to load presets:', e);
132
131
  return [];
133
132
  }
134
133
  }
@@ -145,33 +144,83 @@ export class ProviderManager {
145
144
  baseUrl: overrides?.baseUrl ?? p.baseUrl ?? '',
146
145
  apiFormat: overrides?.apiFormat ?? p.apiFormat ?? 'openai_chat',
147
146
  models: overrides?.models ?? { main: p.defaultModel || '', haiku: '', sonnet: '', opus: '' },
148
- notes: overrides?.notes ?? p.notes ?? ''
147
+ notes: overrides?.notes ?? p.notes ?? '',
148
+ extra: { ...p, ...overrides?.extra }
149
149
  };
150
150
  }
151
151
 
152
- // 连通性测试:按 apiFormat 选择探测路径
152
+ // 获取模型列表
153
+ static async fetchModels(target: SavedProvider | any, apiKeyOverride?: string): Promise<string[]> {
154
+ const p = target;
155
+ const apiKey = apiKeyOverride || p.apiKey;
156
+ const baseUrl = (p.baseUrl || '').replace(/\/+$/, '');
157
+ const modelsUrl = p.modelsUrl || p.extra?.modelsUrl;
158
+ const modelsDataPath = p.modelsDataPath || p.extra?.modelsDataPath || 'data';
159
+ const authStyle = p.modelsAuthStyle || p.extra?.modelsAuthStyle || 'bearer';
160
+
161
+ if (!modelsUrl) return [];
162
+
163
+ const url = modelsUrl.startsWith('http') ? modelsUrl : (baseUrl + modelsUrl);
164
+
165
+ try {
166
+ const headers: any = {};
167
+ if (apiKey) {
168
+ if (authStyle === 'bearer') {
169
+ headers['Authorization'] = `Bearer ${apiKey}`;
170
+ } else if (authStyle === 'x-api-key') {
171
+ headers['x-api-key'] = apiKey;
172
+ headers['anthropic-version'] = '2023-06-01';
173
+ }
174
+ }
175
+
176
+ const res = await axios.get(url, { headers, timeout: 5000 });
177
+ const data = res.data;
178
+ const list = modelsDataPath.split('.').reduce((acc: any, part: string) => acc && acc[part], data);
179
+
180
+ if (Array.isArray(list)) {
181
+ return list.map((m: any) => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
182
+ }
183
+ return [];
184
+ } catch (e) {
185
+ console.error('Failed to fetch models:', e);
186
+ return [];
187
+ }
188
+ }
189
+
190
+ // 连通性测试:优先使用配置的 modelsUrl
153
191
  static async testProvider(target: SavedProvider | string): Promise<{ ok: boolean; latencyMs?: number; message?: string }> {
154
192
  const p = typeof target === 'string' ? await this.getProvider(target) : target;
155
193
  if (!p) return { ok: false, message: 'Provider not found' };
156
- const base = (p.baseUrl || '').replace(/\/+$/,'');
194
+
195
+ const baseUrl = (p.baseUrl || '').replace(/\/+$/, '');
196
+ const modelsUrl = p.modelsUrl || p.extra?.modelsUrl || (p.apiFormat?.includes('openai') ? '/v1/models' : '/v1/models');
197
+ const authStyle = p.modelsAuthStyle || p.extra?.modelsAuthStyle || 'bearer';
198
+ const apiKey = p.apiKey;
199
+
200
+ const url = modelsUrl.startsWith('http') ? modelsUrl : (baseUrl + modelsUrl);
157
201
  const start = Date.now();
158
- // 简单路径推断
159
- let url = base;
160
- if ((p.apiFormat || '').includes('openai')) url = base + '/v1/models';
161
- else if ((p.apiFormat || '').includes('anthropic')) url = base + '/v1/models';
202
+
162
203
  try {
204
+ const headers: any = {};
205
+ if (apiKey) {
206
+ if (authStyle === 'bearer') {
207
+ headers['Authorization'] = `Bearer ${apiKey}`;
208
+ } else if (authStyle === 'x-api-key') {
209
+ headers['x-api-key'] = apiKey;
210
+ headers['anthropic-version'] = '2023-06-01';
211
+ }
212
+ }
213
+
163
214
  const res = await axios.get(url, {
164
215
  timeout: 8000,
165
- headers: {
166
- ...(p.apiKey ? { Authorization: `Bearer ${p.apiKey}` } : {}),
167
- },
216
+ headers,
168
217
  validateStatus: () => true
169
218
  });
170
- // 2xx/3xx 都视为连通
219
+
171
220
  if (res.status >= 200 && res.status < 400) {
172
221
  return { ok: true, latencyMs: Date.now() - start };
173
222
  }
174
- return { ok: false, message: `HTTP ${res.status}` };
223
+ return { ok: false, message: `HTTP ${res.status}: ${JSON.stringify(res.data)}` };
175
224
  } catch (e: any) {
176
225
  return { ok: false, message: e?.message || 'network error' };
177
226
  }