bingocode 1.0.0 → 1.0.1
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/package.json +2 -4
- package/src/cli/ProviderPanel.tsx +725 -418
- package/src/manager/CliMenuManager.tsx +30 -25
- package/src/manager/CliMenuUi.tsx +43 -15
- package/src/server/api/providers.ts +26 -0
- package/src/server/config/providerPresets.ts +93 -86
- package/src/server/config/providers.yaml +145 -41
- package/src/server/proxy/handler.ts +319 -206
- package/src/server/services/providerService.ts +94 -0
- package/src/server/types/provider.ts +19 -0
|
@@ -11,6 +11,7 @@ import path from 'path';
|
|
|
11
11
|
import { ensureSingletonLocalServer } from '../server/ensureSingletonLocalServer.ts';
|
|
12
12
|
// 新增:通用 UI 元素与顶部工具栏
|
|
13
13
|
import { TopBar, BottomBar, Panel, Hint, Kbd, SecondaryMenu } from '../manager/CliMenuUi.tsx';
|
|
14
|
+
import { WelcomeV2 } from '../components/LogoV2/WelcomeV2.tsx';
|
|
14
15
|
import { TopToolbar } from '../manager/TopToolbar.tsx';
|
|
15
16
|
|
|
16
17
|
// 主题切换(Hook)
|
|
@@ -24,8 +25,8 @@ const MARKED_FILE = path.join(process.cwd(), 'markedSessions.json');
|
|
|
24
25
|
// 固定尺寸视口(可用环境变量覆盖)
|
|
25
26
|
const VIEW_W = Number(process.env.CLI_VIEW_W || 96);
|
|
26
27
|
const VIEW_H = Number(process.env.CLI_VIEW_H || 30);
|
|
27
|
-
//
|
|
28
|
-
const TOP_H_HOME = Number(process.env.CLI_TOP_H_HOME ||
|
|
28
|
+
// 顶部高度:首页适配 LogoV2 + Toolbar,其它页面更紧凑
|
|
29
|
+
const TOP_H_HOME = Number(process.env.CLI_TOP_H_HOME || 9);
|
|
29
30
|
const TOP_H_COMPACT = Number(process.env.CLI_TOP_H_COMPACT || 6);
|
|
30
31
|
// 底栏高度
|
|
31
32
|
const BOTTOM_H = Number(process.env.CLI_BOTTOM_H || 3);
|
|
@@ -41,8 +42,8 @@ const i18nMap = {
|
|
|
41
42
|
exit: '退出',
|
|
42
43
|
},
|
|
43
44
|
about: 'Bingo CLI 终端 - 版本信息与产品说明',
|
|
44
|
-
mark: '标记会话',
|
|
45
|
-
unmark: '取消标记',
|
|
45
|
+
mark: '→ 标记会话',
|
|
46
|
+
unmark: '→ 取消标记',
|
|
46
47
|
tipsSimple: 'L 语言 | ESC 返回 | ←→ 菜单 | ↩ 进入 | ? 帮助',
|
|
47
48
|
noData: '暂无数据',
|
|
48
49
|
emptyHistory: '这里还空空的,不如先新建一个会话?',
|
|
@@ -60,8 +61,8 @@ const i18nMap = {
|
|
|
60
61
|
exit: 'Exit',
|
|
61
62
|
},
|
|
62
63
|
about: 'Bingo CLI Terminal - Version Info & About',
|
|
63
|
-
mark: 'Mark Session',
|
|
64
|
-
unmark: 'Unmark Session',
|
|
64
|
+
mark: '→ Mark Session',
|
|
65
|
+
unmark: '→ Unmark Session',
|
|
65
66
|
tipsSimple: 'L Lang | ESC Back | ←→ Menu | ↩ Enter | ? Help',
|
|
66
67
|
noData: 'No data',
|
|
67
68
|
emptyHistory: 'Nothing here yet. Start a new session?',
|
|
@@ -253,7 +254,7 @@ export const CliMenuManager: React.FC = () => {
|
|
|
253
254
|
? (bins['claude-haha'] ? 'claude-haha' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]))
|
|
254
255
|
: (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
|
|
255
256
|
const spawnCmd = isWin ? 'cmd' : 'sh';
|
|
256
|
-
const spawnArgs = isWin ? ['/c',
|
|
257
|
+
const spawnArgs = isWin ? ['/c', 'start', 'cmd', '/k', 'bun bingocode'] : ['-c', `./${binName}`];
|
|
257
258
|
spawn(spawnCmd, spawnArgs, {
|
|
258
259
|
cwd: process.cwd(),
|
|
259
260
|
env: process.env,
|
|
@@ -533,7 +534,7 @@ export const CliMenuManager: React.FC = () => {
|
|
|
533
534
|
: (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
|
|
534
535
|
const spawnCmd = isWin ? 'cmd' : 'sh';
|
|
535
536
|
const spawnArgs = isWin
|
|
536
|
-
? ['/c',
|
|
537
|
+
? ['/c', 'start', 'cmd', '/k', `bun bingocode --resume ${sessionId}`]
|
|
537
538
|
: ['-c', `./${binName} --resume ${sessionId}`];
|
|
538
539
|
spawn(spawnCmd, spawnArgs, {
|
|
539
540
|
cwd: process.cwd(),
|
|
@@ -743,21 +744,16 @@ export const CliMenuManager: React.FC = () => {
|
|
|
743
744
|
function renderCenter() {
|
|
744
745
|
if (showHelp) return renderHelpOverlay();
|
|
745
746
|
|
|
746
|
-
//
|
|
747
|
+
// 首页:默认显示欢迎页图像,水平居中(WelcomeV2 固定 58 字符宽)
|
|
747
748
|
if (page === null) {
|
|
749
|
+
// 首页 Panel 无边框无 padding,内容区宽 = VIEW_W = 96
|
|
750
|
+
// WelcomeV2 宽 58,左偏移 = floor((96 - 58) / 2) = 19
|
|
751
|
+
const WELCOME_W = 58;
|
|
752
|
+
const leftPad = Math.max(0, Math.floor((VIEW_W - WELCOME_W) / 2));
|
|
748
753
|
return (
|
|
749
|
-
<Box flexDirection="
|
|
750
|
-
<
|
|
751
|
-
<
|
|
752
|
-
<Text> </Text>
|
|
753
|
-
<Hint>{lang === 'zh' ? '←→ 选择下方菜单,↩ 进入 · 试试 N/R/P/G/?' : 'Use bottom menu: ←→ choose, ↩ open · Try N/R/P/G/?'}</Hint>
|
|
754
|
-
<Text> </Text>
|
|
755
|
-
<Text dimColor>
|
|
756
|
-
{lang==='zh'
|
|
757
|
-
? '小贴士:按 ' : 'Tip: Press '}
|
|
758
|
-
<Kbd>L</Kbd> {lang==='zh' ? ' 切换语言;' : ' to switch language; '}
|
|
759
|
-
<Kbd>ESC</Kbd> {lang==='zh' ? ' 返回首页' : ' to go Home'}
|
|
760
|
-
</Text>
|
|
754
|
+
<Box flexDirection="row" width={VIEW_W} height={MID_H}>
|
|
755
|
+
<Box width={leftPad} flexShrink={0} />
|
|
756
|
+
<WelcomeV2 />
|
|
761
757
|
</Box>
|
|
762
758
|
);
|
|
763
759
|
}
|
|
@@ -949,6 +945,7 @@ export const CliMenuManager: React.FC = () => {
|
|
|
949
945
|
height={TOP_H}
|
|
950
946
|
homeLogo={<LogoV2 />}
|
|
951
947
|
compactLogo={<CondensedLogo />}
|
|
948
|
+
ip={apiUrl ? apiUrl.replace(/^https?:\/\//, '') : undefined}
|
|
952
949
|
toolbar={
|
|
953
950
|
<TopToolbar
|
|
954
951
|
ready={configReady}
|
|
@@ -959,10 +956,18 @@ export const CliMenuManager: React.FC = () => {
|
|
|
959
956
|
}
|
|
960
957
|
/>
|
|
961
958
|
|
|
962
|
-
{/* 中心业务区(固定高度,内部处理分页/滚动)
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
959
|
+
{/* 中心业务区(固定高度,内部处理分页/滚动)
|
|
960
|
+
首页:无边框无 margin,WelcomeV2 直接撑满,避免 border 额外占行导致超出;
|
|
961
|
+
其它页面:single border + marginY */}
|
|
962
|
+
{page === null ? (
|
|
963
|
+
<Panel width={VIEW_W} height={MID_H} noBorder paddingX={0} paddingY={0} marginY={0}>
|
|
964
|
+
{renderCenter()}
|
|
965
|
+
</Panel>
|
|
966
|
+
) : (
|
|
967
|
+
<Panel width={VIEW_W} height={MID_H} borderStyle="single" paddingX={1} paddingY={0} marginY={1}>
|
|
968
|
+
{renderCenter()}
|
|
969
|
+
</Panel>
|
|
970
|
+
)}
|
|
966
971
|
|
|
967
972
|
{/* 底部菜单与二级菜单 */}
|
|
968
973
|
<BottomBar
|
|
@@ -4,7 +4,20 @@ import SelectInput from 'ink-select-input';
|
|
|
4
4
|
|
|
5
5
|
// 小工具
|
|
6
6
|
const repeatChar = (ch: string, n: number) => ch.repeat(Math.max(0, n));
|
|
7
|
-
|
|
7
|
+
// 按终端显示宽度截断(中文/全角字符占 2 列,ASCII 占 1 列)
|
|
8
|
+
const charDisplayWidth = (c: string) => c.charCodeAt(0) > 127 ? 2 : 1;
|
|
9
|
+
const displayWidth = (s: string) => [...s].reduce((acc, c) => acc + charDisplayWidth(c), 0);
|
|
10
|
+
const truncate = (s: string, maxCols: number) => {
|
|
11
|
+
let cols = 0;
|
|
12
|
+
let out = '';
|
|
13
|
+
for (const c of s) {
|
|
14
|
+
const w = charDisplayWidth(c);
|
|
15
|
+
if (cols + w > maxCols - 1) return out + '…';
|
|
16
|
+
out += c;
|
|
17
|
+
cols += w;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
// Kbd(去掉多余空格,保持紧凑视觉)
|
|
10
23
|
export const Kbd: React.FC<{ children: React.ReactNode }> = memo(({ children }) => (
|
|
@@ -27,13 +40,14 @@ export const Divider: React.FC<{ width?: number; pad?: boolean }> = memo(({ widt
|
|
|
27
40
|
return <Text dimColor>{pad ? ` ${line} ` : line}</Text>;
|
|
28
41
|
});
|
|
29
42
|
|
|
30
|
-
// Panel(更灵活,支持 title
|
|
43
|
+
// Panel(更灵活,支持 title 插槽、最小/最大宽度,borderStyle=undefined 时无边框)
|
|
31
44
|
export const Panel: React.FC<{
|
|
32
45
|
width?: number;
|
|
33
46
|
height?: number;
|
|
34
47
|
minWidth?: number;
|
|
35
48
|
maxWidth?: number;
|
|
36
|
-
borderStyle?: 'round' | 'single' | 'double' | 'bold' | 'classic';
|
|
49
|
+
borderStyle?: 'round' | 'single' | 'double' | 'bold' | 'classic' | undefined;
|
|
50
|
+
noBorder?: boolean;
|
|
37
51
|
paddingX?: number;
|
|
38
52
|
paddingY?: number;
|
|
39
53
|
marginY?: number;
|
|
@@ -44,7 +58,8 @@ export const Panel: React.FC<{
|
|
|
44
58
|
height,
|
|
45
59
|
minWidth,
|
|
46
60
|
maxWidth,
|
|
47
|
-
borderStyle
|
|
61
|
+
borderStyle,
|
|
62
|
+
noBorder = false,
|
|
48
63
|
paddingX = 1,
|
|
49
64
|
paddingY = 0,
|
|
50
65
|
marginY = 0,
|
|
@@ -52,12 +67,13 @@ export const Panel: React.FC<{
|
|
|
52
67
|
children
|
|
53
68
|
}) => {
|
|
54
69
|
const props: any = {
|
|
55
|
-
borderStyle,
|
|
56
70
|
paddingX,
|
|
57
71
|
paddingY,
|
|
58
72
|
marginY,
|
|
59
73
|
flexDirection: 'column',
|
|
60
74
|
};
|
|
75
|
+
// 只有明确传入 borderStyle 且未设置 noBorder 时才渲染边框
|
|
76
|
+
if (!noBorder && borderStyle !== undefined) props.borderStyle = borderStyle;
|
|
61
77
|
if (typeof width === 'number') props.width = width;
|
|
62
78
|
if (typeof height === 'number') props.height = height;
|
|
63
79
|
if (typeof minWidth === 'number') props.minWidth = minWidth;
|
|
@@ -72,10 +88,17 @@ export const Panel: React.FC<{
|
|
|
72
88
|
});
|
|
73
89
|
|
|
74
90
|
// Fallback top(更紧凑)
|
|
75
|
-
export const FallbackTop = memo(() => (
|
|
91
|
+
export const FallbackTop = memo(({ ip }: { ip?: string }) => (
|
|
76
92
|
<Box flexDirection="column">
|
|
77
93
|
<Text>Welcome Bingo Code</Text>
|
|
78
|
-
|
|
94
|
+
{ip ? (
|
|
95
|
+
<Text color="green">IP:{ip}</Text>
|
|
96
|
+
) : (
|
|
97
|
+
<>
|
|
98
|
+
<Text color="yellow">Server is starting, please do not close.</Text>
|
|
99
|
+
<Text dimColor>Initializing config...</Text>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
79
102
|
</Box>
|
|
80
103
|
));
|
|
81
104
|
|
|
@@ -97,13 +120,17 @@ export const BottomBar: React.FC<{
|
|
|
97
120
|
tips: string;
|
|
98
121
|
secondaryMenu: SecondaryMenu;
|
|
99
122
|
}> = memo(({ width = 60, height = 3, menuItems, page, navIndex, tips, secondaryMenu }) => {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
const
|
|
123
|
+
// 精确计算左侧菜单实际占用宽度:prefix(1) + 空格(1) + label + marginRight(2)
|
|
124
|
+
// 中文字符宽度按 2 计算,ASCII 按 1 计算
|
|
125
|
+
const charWidth = (s: string) => [...s].reduce((acc, c) => acc + (c.charCodeAt(0) > 127 ? 2 : 1), 0);
|
|
126
|
+
const leftActual = menuItems.reduce((acc, it) => acc + 1 + 1 + charWidth(it.label) + 2, 0);
|
|
127
|
+
// Panel paddingX=1 占去两侧各1,Box width=width-2,左侧Box不设width(自然宽),右侧需要精确限制
|
|
128
|
+
// 留出 4 字符作为两侧 padding 和分隔缓冲
|
|
129
|
+
const rightSpace = Math.max(10, width - 4 - leftActual - 2);
|
|
103
130
|
return (
|
|
104
131
|
<Panel width={width} height={height} borderStyle="round" paddingX={1} paddingY={0}>
|
|
105
|
-
<Box width={width -
|
|
106
|
-
<Box>
|
|
132
|
+
<Box width={width - 4} justifyContent="space-between" alignItems="center">
|
|
133
|
+
<Box flexShrink={1}>
|
|
107
134
|
{menuItems.map((it, idx) => {
|
|
108
135
|
const isActive = page === it.value;
|
|
109
136
|
const isCursor = page === null && navIndex === idx;
|
|
@@ -117,7 +144,7 @@ export const BottomBar: React.FC<{
|
|
|
117
144
|
})}
|
|
118
145
|
</Box>
|
|
119
146
|
|
|
120
|
-
<Box width={rightSpace} justifyContent="flex-end">
|
|
147
|
+
<Box width={rightSpace} flexShrink={0} justifyContent="flex-end">
|
|
121
148
|
{secondaryMenu ? (
|
|
122
149
|
<Box flexDirection="column" alignItems="flex-end">
|
|
123
150
|
<Text color="magenta">{secondaryMenu.title}</Text>
|
|
@@ -178,11 +205,12 @@ export const TopBar: React.FC<{
|
|
|
178
205
|
homeLogo: React.ReactNode;
|
|
179
206
|
compactLogo: React.ReactNode;
|
|
180
207
|
toolbar?: React.ReactNode;
|
|
181
|
-
|
|
208
|
+
ip?: string;
|
|
209
|
+
}> = memo(({ ready, page, width = 80, height = 5, homeLogo, compactLogo, toolbar, ip }) => (
|
|
182
210
|
<Panel width={width} height={height} borderStyle="round" paddingX={1} paddingY={0}>
|
|
183
211
|
<Box width={width - 2} flexDirection="row" justifyContent="space-between" alignItems="center">
|
|
184
212
|
<Box>
|
|
185
|
-
{ready ? (page === null ? homeLogo : compactLogo) : <FallbackTop />}
|
|
213
|
+
{ready ? (page === null ? homeLogo : compactLogo) : <FallbackTop ip={ip} />}
|
|
186
214
|
</Box>
|
|
187
215
|
{toolbar ? <Box>{toolbar}</Box> : <Box><Hint dim>{ready ? '' : '…'}</Hint></Box>}
|
|
188
216
|
</Box>
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
CreateProviderSchema,
|
|
21
21
|
UpdateProviderSchema,
|
|
22
22
|
TestProviderSchema,
|
|
23
|
+
SlotNameSchema,
|
|
23
24
|
} from '../types/provider.js'
|
|
25
|
+
import type { SlotName } from '../types/provider.js'
|
|
24
26
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
25
27
|
|
|
26
28
|
const providerService = new ProviderService()
|
|
@@ -68,6 +70,23 @@ export async function handleProvidersApi(
|
|
|
68
70
|
return Response.json({ ok: true })
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// GET /api/providers/slots — read slot table
|
|
74
|
+
if (id === 'slots' && !action && req.method === 'GET') {
|
|
75
|
+
const slots = await providerService.readSlots()
|
|
76
|
+
return Response.json(slots)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// PUT /api/providers/slots/:slotName — set one slot
|
|
80
|
+
if (id === 'slots' && action && req.method === 'PUT') {
|
|
81
|
+
const parsed = SlotNameSchema.safeParse(action)
|
|
82
|
+
if (!parsed.success) throw ApiError.badRequest(`Invalid slot name: ${action}. Must be one of main, haiku, sonnet, opus`)
|
|
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) }
|
|
86
|
+
const result = await providerService.setSlot(parsed.data as SlotName, entry)
|
|
87
|
+
return Response.json(result)
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
// /api/providers (no ID)
|
|
72
91
|
if (!id) {
|
|
73
92
|
if (req.method === 'GET') {
|
|
@@ -87,6 +106,13 @@ export async function handleProvidersApi(
|
|
|
87
106
|
return Response.json({ ok: true })
|
|
88
107
|
}
|
|
89
108
|
|
|
109
|
+
// /api/providers/:id/models
|
|
110
|
+
if (action === 'models') {
|
|
111
|
+
if (req.method !== 'GET') throw methodNotAllowed(req.method)
|
|
112
|
+
const models = await providerService.fetchProviderModels(id)
|
|
113
|
+
return Response.json({ models })
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
// /api/providers/:id/test
|
|
91
117
|
if (action === 'test') {
|
|
92
118
|
if (req.method !== 'POST') throw methodNotAllowed(req.method)
|
|
@@ -1,86 +1,93 @@
|
|
|
1
|
-
// Provider presets
|
|
2
|
-
// Original work by Jason Young, MIT License
|
|
3
|
-
|
|
4
|
-
//@C:ID=M.PP.providerPresets;K=M;V=
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
// Provider presets — loaded from providers.yaml at startup
|
|
2
|
+
// Original work inspired by cc-switch (https://github.com/farion1231/cc-switch) by Jason Young, MIT License
|
|
3
|
+
|
|
4
|
+
//@C:ID=M.PP.providerPresets;K=M;V=2.0;P=Import dependencies;D=API;M=Providers;S=ModelConfiguration
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { parse } from 'yaml'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import type { ApiFormat } from '../types/provider.js'
|
|
10
|
+
|
|
11
|
+
//@C:ID=T.PP.ModelMapping;K=T;V=1.0;P=Define model type mappings;D=API;M=Providers;S=ModelConfiguration
|
|
12
|
+
export type ModelMapping = {
|
|
13
|
+
main: string
|
|
14
|
+
haiku: string
|
|
15
|
+
sonnet: string
|
|
16
|
+
opus: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//@C:ID=T.PP.ProviderField;K=T;V=1.0;P=Define per-provider field descriptor;D=API;M=Providers;S=ModelConfiguration
|
|
20
|
+
export type ProviderField = {
|
|
21
|
+
/** Field key: 'name' | 'apiKey' | 'baseUrl' map to top-level fields; others go into extra.<key> */
|
|
22
|
+
key: string
|
|
23
|
+
/** Human-readable label shown in the CLI form */
|
|
24
|
+
label: string
|
|
25
|
+
required?: boolean
|
|
26
|
+
/** If true, input is masked in the terminal */
|
|
27
|
+
secret?: boolean
|
|
28
|
+
placeholder?: string
|
|
29
|
+
/** Default value pre-filled in the form */
|
|
30
|
+
default?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//@C:ID=T.PP.ProviderPreset;K=T;V=2.0;P=Define provider preset structure;D=API;M=Providers;S=ModelConfiguration
|
|
34
|
+
export type ProviderPreset = {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
baseUrl: string
|
|
38
|
+
apiFormat: ApiFormat
|
|
39
|
+
defaultModels: ModelMapping
|
|
40
|
+
needsApiKey: boolean
|
|
41
|
+
websiteUrl: string
|
|
42
|
+
/** Ordered list of fields to render when adding a new provider from this preset */
|
|
43
|
+
fields: ProviderField[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//@C:ID=D.PP.PROVIDER_PRESETS;K=D;V=2.0;P=Load provider presets from yaml;D=API;M=Providers;S=ModelConfiguration
|
|
47
|
+
function loadPresetsFromYaml(): ProviderPreset[] {
|
|
48
|
+
try {
|
|
49
|
+
const yamlPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'providers.yaml')
|
|
50
|
+
const raw = parse(readFileSync(yamlPath, 'utf-8')) as { presets?: ProviderPreset[] }
|
|
51
|
+
const presets = raw?.presets
|
|
52
|
+
if (!Array.isArray(presets) || presets.length === 0) {
|
|
53
|
+
throw new Error('providers.yaml missing presets array')
|
|
54
|
+
}
|
|
55
|
+
// Ensure fields is always an array
|
|
56
|
+
return presets.map(p => ({ ...p, fields: Array.isArray(p.fields) ? p.fields : [] }))
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('[providerPresets] Failed to load providers.yaml, falling back to defaults:', err)
|
|
59
|
+
// Minimal fallback so the server can still start
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
id: 'official',
|
|
63
|
+
name: 'Claude Official',
|
|
64
|
+
baseUrl: '',
|
|
65
|
+
apiFormat: 'anthropic',
|
|
66
|
+
defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
|
|
67
|
+
needsApiKey: false,
|
|
68
|
+
websiteUrl: 'https://www.anthropic.com/claude-code',
|
|
69
|
+
fields: [{ key: 'name', label: 'Provider 昵称', required: true }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'custom',
|
|
73
|
+
name: 'Custom',
|
|
74
|
+
baseUrl: '',
|
|
75
|
+
apiFormat: 'anthropic',
|
|
76
|
+
defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
|
|
77
|
+
needsApiKey: true,
|
|
78
|
+
websiteUrl: '',
|
|
79
|
+
fields: [
|
|
80
|
+
{ key: 'name', label: 'Provider 昵称', required: true },
|
|
81
|
+
{ key: 'baseUrl', label: 'Base URL', required: true },
|
|
82
|
+
{ key: 'apiKey', label: 'API Key', required: false, secret: true },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const PROVIDER_PRESETS: ProviderPreset[] = loadPresetsFromYaml()
|
|
90
|
+
|
|
91
|
+
export async function loadProviderPresets(): Promise<ProviderPreset[]> {
|
|
92
|
+
return PROVIDER_PRESETS
|
|
93
|
+
}
|
|
@@ -1,41 +1,145 @@
|
|
|
1
|
-
version:
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Provider 预设配置
|
|
4
|
+
# fields 数组声明新增时需填写的字段
|
|
5
|
+
# key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
|
|
6
|
+
# secret: true 时前端使用密码掩码显示
|
|
7
|
+
|
|
8
|
+
presets:
|
|
9
|
+
- id: official
|
|
10
|
+
name: Claude Official
|
|
11
|
+
baseUrl: ''
|
|
12
|
+
apiFormat: anthropic
|
|
13
|
+
needsApiKey: false
|
|
14
|
+
websiteUrl: https://www.anthropic.com/claude-code
|
|
15
|
+
defaultModels:
|
|
16
|
+
main: ''
|
|
17
|
+
haiku: ''
|
|
18
|
+
sonnet: ''
|
|
19
|
+
opus: ''
|
|
20
|
+
fields:
|
|
21
|
+
- key: name
|
|
22
|
+
label: Provider 昵称
|
|
23
|
+
required: true
|
|
24
|
+
secret: false
|
|
25
|
+
placeholder: 'e.g. Claude Official'
|
|
26
|
+
|
|
27
|
+
- id: deepseek
|
|
28
|
+
name: DeepSeek
|
|
29
|
+
baseUrl: https://api.deepseek.com/anthropic
|
|
30
|
+
apiFormat: anthropic
|
|
31
|
+
needsApiKey: true
|
|
32
|
+
websiteUrl: https://platform.deepseek.com
|
|
33
|
+
defaultModels:
|
|
34
|
+
main: DeepSeek-V3.2
|
|
35
|
+
haiku: DeepSeek-V3.2
|
|
36
|
+
sonnet: DeepSeek-V3.2
|
|
37
|
+
opus: DeepSeek-V3.2
|
|
38
|
+
fields:
|
|
39
|
+
- key: name
|
|
40
|
+
label: Provider 昵称
|
|
41
|
+
required: true
|
|
42
|
+
secret: false
|
|
43
|
+
placeholder: 'e.g. My DeepSeek'
|
|
44
|
+
- key: apiKey
|
|
45
|
+
label: API Key
|
|
46
|
+
required: true
|
|
47
|
+
secret: true
|
|
48
|
+
placeholder: 'sk-...'
|
|
49
|
+
|
|
50
|
+
- id: zhipuglm
|
|
51
|
+
name: Zhipu GLM
|
|
52
|
+
baseUrl: https://open.bigmodel.cn/api/anthropic
|
|
53
|
+
apiFormat: anthropic
|
|
54
|
+
needsApiKey: true
|
|
55
|
+
websiteUrl: https://open.bigmodel.cn
|
|
56
|
+
defaultModels:
|
|
57
|
+
main: glm-5
|
|
58
|
+
haiku: glm-5
|
|
59
|
+
sonnet: glm-5
|
|
60
|
+
opus: glm-5
|
|
61
|
+
fields:
|
|
62
|
+
- key: name
|
|
63
|
+
label: Provider 昵称
|
|
64
|
+
required: true
|
|
65
|
+
secret: false
|
|
66
|
+
placeholder: 'e.g. My GLM'
|
|
67
|
+
- key: apiKey
|
|
68
|
+
label: API Key
|
|
69
|
+
required: true
|
|
70
|
+
secret: true
|
|
71
|
+
placeholder: '智谱 API Key'
|
|
72
|
+
|
|
73
|
+
- id: kimi
|
|
74
|
+
name: Kimi
|
|
75
|
+
baseUrl: https://api.moonshot.cn/anthropic
|
|
76
|
+
apiFormat: anthropic
|
|
77
|
+
needsApiKey: true
|
|
78
|
+
websiteUrl: https://platform.moonshot.cn
|
|
79
|
+
defaultModels:
|
|
80
|
+
main: kimi-k2.5
|
|
81
|
+
haiku: kimi-k2.5
|
|
82
|
+
sonnet: kimi-k2.5
|
|
83
|
+
opus: kimi-k2.5
|
|
84
|
+
fields:
|
|
85
|
+
- key: name
|
|
86
|
+
label: Provider 昵称
|
|
87
|
+
required: true
|
|
88
|
+
secret: false
|
|
89
|
+
placeholder: 'e.g. My Kimi'
|
|
90
|
+
- key: apiKey
|
|
91
|
+
label: API Key
|
|
92
|
+
required: true
|
|
93
|
+
secret: true
|
|
94
|
+
placeholder: 'Moonshot API Key'
|
|
95
|
+
|
|
96
|
+
- id: minimax
|
|
97
|
+
name: MiniMax
|
|
98
|
+
baseUrl: https://api.minimaxi.com/anthropic
|
|
99
|
+
apiFormat: anthropic
|
|
100
|
+
needsApiKey: true
|
|
101
|
+
websiteUrl: https://platform.minimaxi.com
|
|
102
|
+
defaultModels:
|
|
103
|
+
main: MiniMax-M2.7
|
|
104
|
+
haiku: MiniMax-M2.7
|
|
105
|
+
sonnet: MiniMax-M2.7
|
|
106
|
+
opus: MiniMax-M2.7
|
|
107
|
+
fields:
|
|
108
|
+
- key: name
|
|
109
|
+
label: Provider 昵称
|
|
110
|
+
required: true
|
|
111
|
+
secret: false
|
|
112
|
+
placeholder: 'e.g. My MiniMax'
|
|
113
|
+
- key: apiKey
|
|
114
|
+
label: API Key
|
|
115
|
+
required: true
|
|
116
|
+
secret: true
|
|
117
|
+
placeholder: 'MiniMax API Key'
|
|
118
|
+
|
|
119
|
+
- id: custom
|
|
120
|
+
name: Custom
|
|
121
|
+
baseUrl: ''
|
|
122
|
+
apiFormat: anthropic
|
|
123
|
+
needsApiKey: true
|
|
124
|
+
websiteUrl: ''
|
|
125
|
+
defaultModels:
|
|
126
|
+
main: ''
|
|
127
|
+
haiku: ''
|
|
128
|
+
sonnet: ''
|
|
129
|
+
opus: ''
|
|
130
|
+
fields:
|
|
131
|
+
- key: name
|
|
132
|
+
label: Provider 昵称
|
|
133
|
+
required: true
|
|
134
|
+
secret: false
|
|
135
|
+
placeholder: 'e.g. My Custom Provider'
|
|
136
|
+
- key: baseUrl
|
|
137
|
+
label: Base URL
|
|
138
|
+
required: true
|
|
139
|
+
secret: false
|
|
140
|
+
placeholder: 'https://your-api-endpoint.com/anthropic'
|
|
141
|
+
- key: apiKey
|
|
142
|
+
label: API Key
|
|
143
|
+
required: false
|
|
144
|
+
secret: true
|
|
145
|
+
placeholder: '(可选)API Key'
|