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.
@@ -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 || 13);
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', binName] : ['-c', `./${binName}`];
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', binName, '--resume', sessionId]
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="column" width={VIEW_W} height={MID_H}>
750
- <Hint>{i18nMap[lang].tipsSimple}</Hint>
751
- <Hint>{nowStr}</Hint>
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
- <Panel width={VIEW_W} height={MID_H} borderStyle="single" paddingX={1} paddingY={0} marginY={1}>
964
- {renderCenter()}
965
- </Panel>
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
- const truncate = (s: string, max: number) => (s.length > max ? s.slice(0, Math.max(0, max - 1)) + '…' : s);
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 = 'single',
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
- <Text dimColor>Initializing config...</Text>
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
- const leftEstimated = Math.min(width - 10, menuItems.length * 12);
102
- const rightSpace = Math.max(10, width - leftEstimated - 6);
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 - 2} justifyContent="space-between" alignItems="center">
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
- }> = memo(({ ready, page, width = 80, height = 5, homeLogo, compactLogo, toolbar }) => (
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 inspired by cc-switch (https://github.com/farion1231/cc-switch)
2
- // Original work by Jason Young, MIT License
3
-
4
- //@C:ID=M.PP.providerPresets;K=M;V=1.0;P=Import dependencies;D=API;M=Providers;S=ModelConfiguration
5
- import type { ApiFormat } from '../types/provider.js'
6
- //@C:ID=T.PP.ModelMapping;K=T;V=1.0;P=Define model type mappings;D=API;M=Providers;S=ModelConfiguration
7
- export type ModelMapping = {
8
- main: string
9
- haiku: string
10
- sonnet: string
11
- opus: string
12
- }
13
-
14
- //@C:ID=T.PP.ProviderPreset;K=T;V=1.0;P=Define provider preset structure;D=API;M=Providers;S=ModelConfiguration
15
- export type ProviderPreset = {
16
- id: string
17
- name: string
18
- baseUrl: string
19
- apiFormat: ApiFormat
20
- defaultModels: ModelMapping
21
- needsApiKey: boolean
22
- websiteUrl: string
23
- }
24
-
25
- //@C:ID=D.PP.PROVIDER_PRESETS;K=D;V=1.0;P=Define available provider presets;D=API;M=Providers;S=ModelConfiguration
26
- export const PROVIDER_PRESETS: ProviderPreset[] = [
27
- {
28
- id: 'official',
29
- name: 'Claude Official',
30
- baseUrl: '',
31
- apiFormat: 'anthropic',
32
- defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
33
- needsApiKey: false,
34
- websiteUrl: 'https://www.anthropic.com/claude-code',
35
- },
36
- ///@C:PP.DeepSeekProvider
37
- {
38
- id: 'deepseek',
39
- name: 'DeepSeek',
40
- baseUrl: 'https://api.deepseek.com/anthropic',
41
- apiFormat: 'anthropic',
42
- defaultModels: { main: 'DeepSeek-V3.2', haiku: 'DeepSeek-V3.2', sonnet: 'DeepSeek-V3.2', opus: 'DeepSeek-V3.2' },
43
- needsApiKey: true,
44
- websiteUrl: 'https://platform.deepseek.com',
45
- },
46
- ///@C:PP.ZhipuGLMProvider
47
- {
48
- id: 'zhipuglm',
49
- name: 'Zhipu GLM',
50
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
51
- apiFormat: 'anthropic',
52
- defaultModels: { main: 'glm-5', haiku: 'glm-5', sonnet: 'glm-5', opus: 'glm-5' },
53
- needsApiKey: true,
54
- websiteUrl: 'https://open.bigmodel.cn',
55
- },
56
- ///@C:PP.KimiProvider
57
- {
58
- id: 'kimi',
59
- name: 'Kimi',
60
- baseUrl: 'https://api.moonshot.cn/anthropic',
61
- apiFormat: 'anthropic',
62
- defaultModels: { main: 'kimi-k2.5', haiku: 'kimi-k2.5', sonnet: 'kimi-k2.5', opus: 'kimi-k2.5' },
63
- needsApiKey: true,
64
- websiteUrl: 'https://platform.moonshot.cn',
65
- },
66
- ///@C:PP.MiniMaxProvider
67
- {
68
- id: 'minimax',
69
- name: 'MiniMax',
70
- baseUrl: 'https://api.minimaxi.com/anthropic',
71
- apiFormat: 'anthropic',
72
- defaultModels: { main: 'MiniMax-M2.7', haiku: 'MiniMax-M2.7', sonnet: 'MiniMax-M2.7', opus: 'MiniMax-M2.7' },
73
- needsApiKey: true,
74
- websiteUrl: 'https://platform.minimaxi.com',
75
- },
76
- ///@C:PP.CustomProvider
77
- {
78
- id: 'custom',
79
- name: 'Custom',
80
- baseUrl: '',
81
- apiFormat: 'anthropic',
82
- defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
83
- needsApiKey: true,
84
- websiteUrl: '',
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: 1
2
-
3
- # 支持的 provider 类型定义及字段声明(可添加新类型,无需改 TS 类型)
4
- types:
5
- openai_chat:
6
- fields:
7
- apiKey: { required: true, secret: true, env: OPENAI_API_KEY }
8
- baseUrl: { default: "https://api.openai.com" }
9
- apiFormat: { const: "openai_chat" }
10
- models:
11
- type: object
12
- default: { main: "gpt-4o-mini" }
13
- health:
14
- method: GET
15
- path: /v1/models
16
- success: "2xx-3xx"
17
- anthropic_chat:
18
- fields:
19
- apiKey: { required: true, secret: true, env: ANTHROPIC_API_KEY }
20
- baseUrl: { default: "https://api.anthropic.com" }
21
- apiFormat: { const: "anthropic_chat" }
22
- models:
23
- type: object
24
- default: { main: "claude-3-5-sonnet" }
25
- health:
26
- method: GET
27
- path: /v1/models
28
- success: "2xx-3xx"
29
-
30
- # 预设 provider 实例,可直接派生并覆盖
31
- presets:
32
- - id: openai_official
33
- type: openai_chat
34
- name: OpenAI Official
35
- baseUrl: https://api.openai.com
36
- models: { main: "gpt-4o-mini" }
37
- - id: local_ollama
38
- type: openai_chat
39
- name: Local Ollama
40
- baseUrl: http://127.0.0.1:11434/v1
41
- models: { main: "llama3.1" }
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'