bingocode 1.0.17 → 1.0.19
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 +1 -1
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +47 -9
- package/src/manager/CliMenuManager.tsx +83 -7
- package/src/server/__tests__/conversation-service.test.ts +7 -7
- package/src/server/__tests__/haha-oauth-service.test.ts +1 -1
- package/src/server/__tests__/providers-real.test.ts +15 -15
- package/src/server/api/computer-use.ts +2 -2
- package/src/server/api/providers.ts +6 -2
- package/src/server/cli/listProviders.ts +1 -1
- package/src/server/cli/providerManager.ts +1 -1
- package/src/server/config/providers.yaml +207 -207
- package/src/server/proxy/handler.ts +30 -47
- package/src/server/proxy/streaming/anthropicStreamLabeler.ts +56 -0
- package/src/server/services/conversationService.ts +5 -5
- package/src/server/services/hahaOAuthService.ts +1 -1
- package/src/server/services/providerManager.ts +1 -1
- package/src/server/services/providerService.ts +32 -14
- package/src/server/types/provider.ts +1 -0
- package/src/utils/computerUse/wrapper.tsx +2 -2
- package/src/utils/managedEnv.ts +23 -15
- package/src/utils/proxy.ts +13 -1
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
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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 => ({
|
|
706
|
-
|
|
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
|
-
|
|
751
|
+
}}
|
|
752
|
+
/>
|
|
753
|
+
</Box>
|
|
754
|
+
<Text dimColor>回车保存组件名称(显示在 Claude UI),ESC 返回模型选择</Text>
|
|
717
755
|
</Box>
|
|
718
756
|
);
|
|
719
757
|
}
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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:
|
|
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:
|
|
612
|
+
env: spawnEnv,
|
|
548
613
|
detached: true,
|
|
549
614
|
stdio: 'ignore'
|
|
550
615
|
}).unref();
|
|
@@ -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(), '
|
|
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\\
|
|
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, '
|
|
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\\
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
-
* 验证添加、激活、
|
|
4
|
-
* (provider env 写到 ~/.claude/
|
|
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, '
|
|
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 并激活 — 写入
|
|
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
|
-
// 验证写入
|
|
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 写入
|
|
75
|
+
console.log('✅ Provider 写入 bingo/settings.json,原版 settings.json 未被污染')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
test('切换 Provider — 更新
|
|
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 成功,
|
|
119
|
+
console.log('✅ 切换 Provider 成功,bingo/settings.json 更新正确')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
test('
|
|
123
|
-
// 预写一个有内容的
|
|
124
|
-
await fs.mkdir(path.join(tmpDir, '
|
|
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, '
|
|
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('✅
|
|
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 和
|
|
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
|
-
// 验证
|
|
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, '
|
|
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/
|
|
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 : {
|
|
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', '
|
|
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', '
|
|
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> {
|