bingocode 1.0.20 → 1.0.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -5,7 +5,7 @@ import { setupTerminal, shouldOfferTerminalSetup } from '../commands/terminalSet
5
5
  import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js';
6
6
  import { Box, Link, Newline, Text, useTheme } from '../ink.js';
7
7
  import { useKeybindings } from '../keybindings/useKeybinding.js';
8
- import { isAnthropicAuthEnabled } from '../utils/auth.js';
8
+ import { isAnthropicAuthEnabled, isManagedOAuthContext } from '../utils/auth.js';
9
9
  import { normalizeApiKeyForConfig } from '../utils/authPortable.js';
10
10
  import { getCustomApiKeyStatus } from '../utils/config.js';
11
11
  import { env } from '../utils/env.js';
@@ -114,7 +114,11 @@ export function Onboarding({
114
114
  goToNextStep();
115
115
  }
116
116
  const steps: OnboardingStep[] = [];
117
- if (oauthEnabled) {
117
+ // Skip preflight in managed OAuth context (claude-desktop / CCR):
118
+ // The desktop app already verifies local server connectivity,
119
+ // and the Anthropic /api/hello endpoint may return 400 before OAuth login,
120
+ // which would incorrectly kill the onboarding flow.
121
+ if (oauthEnabled && !isManagedOAuthContext()) {
118
122
  steps.push({
119
123
  id: 'preflight',
120
124
  component: preflightStep
@@ -1,4 +1,3 @@
1
- import { mkdirSync } from 'node:fs'
2
1
  import { profileCheckpoint } from '../utils/startupProfiler.js'
3
2
  import '../bootstrap/state.js'
4
3
  import '../utils/config.js'
@@ -26,7 +25,7 @@ import { logForDebugging } from '../utils/debug.js'
26
25
  import { detectCurrentRepository } from '../utils/detectRepository.js'
27
26
  import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
28
27
  import { initJetBrainsDetection } from '../utils/envDynamic.js'
29
- import { getClaudeConfigHomeDir, isEnvTruthy } from '../utils/envUtils.js'
28
+ import { isEnvTruthy } from '../utils/envUtils.js'
30
29
  import { ConfigParseError, errorMessage } from '../utils/errors.js'
31
30
  // showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
32
31
  import {
@@ -56,13 +55,6 @@ import { setShellIfWindows } from '../utils/windowsPaths.js'
56
55
  let telemetryInitialized = false
57
56
 
58
57
  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
-
66
58
  const initStartTime = Date.now()
67
59
  logForDiagnosticsNoPII('info', 'init_started')
68
60
  profileCheckpoint('init_function_start')
@@ -17,6 +17,9 @@ import { TopToolbar } from '../manager/TopToolbar.tsx';
17
17
 
18
18
  // 主题切换(Hook)
19
19
  import { useTheme } from '../components/design-system/ThemeProvider.js';
20
+ // Markdown 渲染(纯函数,不依赖 AppStateProvider context)
21
+ import { applyMarkdown } from '../utils/markdown.js';
22
+ import { Ansi } from '../ink/Ansi.js';
20
23
 
21
24
  // 配置相关(仅使用可用接口)
22
25
  import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
@@ -132,21 +135,57 @@ function loadMarkedSessionIds(): Set<string> {
132
135
  }
133
136
  }
134
137
 
135
- //@C:F ID=F.CM.saveMarkedSessionIds;K=F;V=1.0;P=save marked ids;D=CLI;M=cli;S=persist;In=Set<string>;Out=void
138
+ //@C:F ID=F.CM.saveMarkedSessionIds;K=F;V=1.1;P=save marked ids;D=CLI;M=cli;S=persist;In=Set<string>;Out=void
136
139
  function saveMarkedSessionIds(set: Set<string>) {
137
140
  try {
141
+ const dir = path.dirname(MARKED_FILE);
142
+ if (!fs.existsSync(dir)) {
143
+ fs.mkdirSync(dir, { recursive: true });
144
+ }
138
145
  fs.writeFileSync(MARKED_FILE, JSON.stringify([...set]), 'utf-8');
139
- } catch {}
146
+ } catch (err) {
147
+ console.error('[saveMarkedSessionIds] 写入失败:', err);
148
+ }
140
149
  }
141
150
 
142
- // 通用消息结构,供 <Messages />
143
- type ChatMessage = {
151
+ // 消息条目(与后端 MessageEntry 对齐)
152
+ type MessageEntry = {
144
153
  id: string;
145
- userId: string;
146
- content: string;
147
- createdAt?: string;
154
+ type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result';
155
+ content: unknown; // string 或 ContentBlock[]
156
+ timestamp: string;
157
+ model?: string;
158
+ parentUuid?: string;
159
+ parentToolUseId?: string;
160
+ isSidechain?: boolean;
148
161
  };
149
162
 
163
+ /** 从 MessageEntry.content 提取纯文本 */
164
+ function extractTextFromContent(content: unknown): string {
165
+ if (typeof content === 'string') return content;
166
+ if (Array.isArray(content)) {
167
+ return content
168
+ .map((block: any) => {
169
+ if (block.type === 'text' && typeof block.text === 'string') return block.text;
170
+ if (block.type === 'tool_use') return `[Tool: ${block.name || 'unknown'}]`;
171
+ if (block.type === 'tool_result') {
172
+ if (typeof block.content === 'string') return block.content;
173
+ if (Array.isArray(block.content)) {
174
+ return block.content
175
+ .filter((b: any) => b.type === 'text')
176
+ .map((b: any) => b.text)
177
+ .join('\n');
178
+ }
179
+ return '[Tool Result]';
180
+ }
181
+ return '';
182
+ })
183
+ .filter(Boolean)
184
+ .join('\n');
185
+ }
186
+ return String(content ?? '');
187
+ }
188
+
150
189
  //@C:F ID=F.CM.CliMenuManager;K=F;V=1.5;P=CLI 主菜单;D=CLI;M=cli;S=main;In=;Out=JSX.Element
151
190
  export const CliMenuManager: React.FC = () => {
152
191
  const { stdout } = useStdout();
@@ -208,8 +247,8 @@ export const CliMenuManager: React.FC = () => {
208
247
  const [historyMenuStage, setHistoryMenuStage] = useState<'list'|'window'|'deleteConfirm'>('list');
209
248
  const [selectedHistory, setSelectedHistory] = useState<any|null>(null);
210
249
 
211
- // 历史-消息内容(分页视口)
212
- const [sessionMessages, setSessionMessages] = useState<ChatMessage[]>([]);
250
+ // 历史-消息内容
251
+ const [sessionMessages, setSessionMessages] = useState<MessageEntry[]>([]);
213
252
  const [loadingMsgs, setLoadingMsgs] = useState(false);
214
253
  const [msgsErr, setMsgsErr] = useState<string | null>(null);
215
254
  const [msgsPage, setMsgsPage] = useState(0); // 0=最新页(底部),1=向上翻一页
@@ -380,9 +419,26 @@ export const CliMenuManager: React.FC = () => {
380
419
 
381
420
  // 会话消息获取
382
421
  useEffect(() => {
383
- // 进入 window 不再加载消息(只看属性)
384
- if (false) {
385
- /* 保留原实现以备将来恢复消息视图时启用 */
422
+ if (page === 'history' && historyMenuStage === 'window' && selectedHistory && apiUrl) {
423
+ let cancelled = false;
424
+ setLoadingMsgs(true);
425
+ setMsgsErr(null);
426
+ setSessionMessages([]);
427
+ setMsgsPage(0);
428
+ (async () => {
429
+ try {
430
+ const resp = await axios.get(`${apiUrl}/api/sessions/${selectedHistory.id}/messages`);
431
+ if (!cancelled) {
432
+ const msgs: MessageEntry[] = resp.data?.messages ?? [];
433
+ setSessionMessages(msgs);
434
+ }
435
+ } catch (e: any) {
436
+ if (!cancelled) setMsgsErr(e.message || '消息加载失败');
437
+ } finally {
438
+ if (!cancelled) setLoadingMsgs(false);
439
+ }
440
+ })();
441
+ return () => { cancelled = true; };
386
442
  } else {
387
443
  setSessionMessages([]);
388
444
  setLoadingMsgs(false);
@@ -531,6 +587,15 @@ export const CliMenuManager: React.FC = () => {
531
587
  handleHistoryMenuAction('__back');
532
588
  return;
533
589
  }
590
+ // 消息滚动:↑/k 向上翻页,↓/j 向下翻页
591
+ if (key.upArrow || input === 'k') {
592
+ setMsgsPage(p => Math.max(0, p - 1));
593
+ return;
594
+ }
595
+ if (key.downArrow || input === 'j') {
596
+ setMsgsPage(p => p + 1);
597
+ return;
598
+ }
534
599
 
535
600
  } else if (historyMenuStage === 'deleteConfirm') {
536
601
  if (input === 'q') {
@@ -860,54 +925,62 @@ export const CliMenuManager: React.FC = () => {
860
925
  }
861
926
 
862
927
  if (historyMenuStage === 'window' && selectedHistory) {
863
- const halfH = Math.floor(MID_H / 2);
864
928
  const isMarked = markedSessionIds.has(selectedHistory.id);
865
- const menu = secondaryMenu ?? {
866
- title: lang === 'zh' ? '会话操作' : 'Session Actions',
867
- items: [
868
- { label: (isMarked ? i18nMap[lang].unmark : i18nMap[lang].mark), value: '__toggle_mark' },
869
- { label: '→ 继续会话聊天', value: '__continue' },
870
- { label: '→ 删除会话聊天', value: '__delete' },
871
- { label: '← 返回历史列表', value: '__back' },
872
- ],
873
- onSelect: (item: any) => {
874
- if (item.value === '__back') {
875
- setHistoryMenuStage('list');
876
- setSelectedHistory(null);
877
- setMsgsPage(0);
878
- } else if (item.value === '__continue' && selectedHistory) {
879
- resumeSession(selectedHistory.id);
880
- } else if (item.value === '__delete') {
881
- setHistoryMenuStage('deleteConfirm');
882
- } else if (item.value === '__toggle_mark' && selectedHistory) {
883
- toggleMarkSession(selectedHistory.id);
884
- }
885
- }
886
- };
929
+
930
+ // ── 信息栏高度固定 3 行(标题 + 元信息 + 提示) ──
931
+ const INFO_H = 3;
932
+ const MSGS_H = Math.max(3, MID_H - INFO_H);
933
+
934
+ // ── 过滤出可展示的消息(user / assistant / system,跳过 tool_use / tool_result) ──
935
+ const displayMsgs = sessionMessages.filter(
936
+ m => m.type === 'user' || m.type === 'assistant' || m.type === 'system'
937
+ );
938
+
939
+ // ── 分页:每页 MSGS_PAGE_SIZE 条消息 ──
940
+ const totalPages = Math.max(1, Math.ceil(displayMsgs.length / MSGS_PAGE_SIZE));
941
+ const safePage = Math.min(msgsPage, totalPages - 1);
942
+ const pageStart = safePage * MSGS_PAGE_SIZE;
943
+ const pageMsgs = displayMsgs.slice(pageStart, pageStart + MSGS_PAGE_SIZE);
887
944
 
888
945
  return (
889
946
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
890
- {/* 上半区:只显示会话属性 */}
891
- <Box height={halfH} flexDirection="column">
947
+ {/* ── 顶部信息栏 ── */}
948
+ <Box height={INFO_H} flexDirection="column">
892
949
  <Text color={isMarked ? 'yellow' : 'cyan'}>
893
- {isMarked ? '★ ' : ''}会话属性
950
+ {isMarked ? '★ ' : ''}{selectedHistory.title || 'Untitled'}
951
+ <Text dimColor> {selectedHistory.createdAt?.slice(0, 16).replace('T', ' ') || ''} · {displayMsgs.length} msgs</Text>
894
952
  </Text>
895
- <Text>id: {selectedHistory.id}</Text>
896
- <Text>标题: {selectedHistory.title}</Text>
897
- <Text>创建时间: {selectedHistory.createdAt}</Text>
898
- <Text>消息数: {selectedHistory.messageCount ?? 0}</Text>
899
- <Text>已标记: {isMarked ? '是' : '否'}</Text>
900
- <Hint>m 标记/取消 · c 继续 · d 删除 · q 返回</Hint>
953
+ <Hint>
954
+ j/↓ 下翻 · k/↑ 上翻 · m 标记 · c 继续 · d 删除 · q 返回
955
+ {displayMsgs.length > MSGS_PAGE_SIZE ? ` [${safePage + 1}/${totalPages}]` : ''}
956
+ </Hint>
901
957
  </Box>
902
958
 
903
- {/* 下半区:三级菜单 */}
904
- <Box height={MID_H - halfH} flexDirection="column" borderStyle="round" borderColor="cyan" paddingLeft={1} paddingRight={1}>
905
- <Text>{menu.title}</Text>
906
- <SelectInput
907
- items={menu.items.map(it => ({ label: it.label, value: it.value }))}
908
- onSelect={(item) => handleHistoryMenuAction(String(item.value))}
909
- />
910
- <Hint>↩ 执行 · q 返回</Hint>
959
+ {/* ── 消息区 ── */}
960
+ <Box height={MSGS_H} flexDirection="column" overflow="hidden">
961
+ {loadingMsgs && <Text color="yellow">加载消息中...</Text>}
962
+ {msgsErr && <Text color="red">错误: {msgsErr}</Text>}
963
+ {!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
964
+ <Text dimColor>无消息记录</Text>
965
+ )}
966
+ {pageMsgs.map((msg) => {
967
+ const text = extractTextFromContent(msg.content);
968
+ if (!text.trim()) return null;
969
+ const isUser = msg.type === 'user';
970
+ const isSystem = msg.type === 'system';
971
+ const roleLabel = isUser ? '👤 You' : isSystem ? '⚙ System' : '🤖 Assistant';
972
+ const roleColor = isUser ? 'green' : isSystem ? 'gray' : 'cyan';
973
+ return (
974
+ <Box key={msg.id} flexDirection="column" marginBottom={1}>
975
+ <Text color={roleColor} bold>{roleLabel}</Text>
976
+ {isUser ? (
977
+ <Text>{text}</Text>
978
+ ) : (
979
+ <Ansi>{applyMarkdown(text, theme)}</Ansi>
980
+ )}
981
+ </Box>
982
+ );
983
+ })}
911
984
  </Box>
912
985
  </Box>
913
986
  );
@@ -121,13 +121,14 @@ export class ProviderManager {
121
121
  }
122
122
  }
123
123
 
124
- // 预设列表
124
+ // 预设列表(可选,providerPresets 不存在则返回空)
125
125
  static async listPresets(): Promise<any[]> {
126
126
  try {
127
- const { loadProviderPresets } = await import('../config/providerPresets.ts');
128
- return await loadProviderPresets();
129
- } catch (e) {
130
- console.error('Failed to load presets:', e);
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 {
131
132
  return [];
132
133
  }
133
134
  }
@@ -144,83 +145,33 @@ export class ProviderManager {
144
145
  baseUrl: overrides?.baseUrl ?? p.baseUrl ?? '',
145
146
  apiFormat: overrides?.apiFormat ?? p.apiFormat ?? 'openai_chat',
146
147
  models: overrides?.models ?? { main: p.defaultModel || '', haiku: '', sonnet: '', opus: '' },
147
- notes: overrides?.notes ?? p.notes ?? '',
148
- extra: { ...p, ...overrides?.extra }
148
+ notes: overrides?.notes ?? p.notes ?? ''
149
149
  };
150
150
  }
151
151
 
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
152
+ // 连通性测试:按 apiFormat 选择探测路径
191
153
  static async testProvider(target: SavedProvider | string): Promise<{ ok: boolean; latencyMs?: number; message?: string }> {
192
154
  const p = typeof target === 'string' ? await this.getProvider(target) : target;
193
155
  if (!p) return { ok: false, message: 'Provider not found' };
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);
156
+ const base = (p.baseUrl || '').replace(/\/+$/,'');
201
157
  const start = Date.now();
202
-
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';
203
162
  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
-
214
163
  const res = await axios.get(url, {
215
164
  timeout: 8000,
216
- headers,
165
+ headers: {
166
+ ...(p.apiKey ? { Authorization: `Bearer ${p.apiKey}` } : {}),
167
+ },
217
168
  validateStatus: () => true
218
169
  });
219
-
170
+ // 2xx/3xx 都视为连通
220
171
  if (res.status >= 200 && res.status < 400) {
221
172
  return { ok: true, latencyMs: Date.now() - start };
222
173
  }
223
- return { ok: false, message: `HTTP ${res.status}: ${JSON.stringify(res.data)}` };
174
+ return { ok: false, message: `HTTP ${res.status}` };
224
175
  } catch (e: any) {
225
176
  return { ok: false, message: e?.message || 'network error' };
226
177
  }
@@ -30,9 +30,8 @@ const FIXED_APIFMT = 'openai_chat';
30
30
 
31
31
  const ProvidersMenu: React.FC = () => {
32
32
  const { exit } = useApp();
33
- const [modelOptions, setModelOptions] = useState<string[]>([]);
34
33
  const [modelSelectIdx, setModelSelectIdx] = useState(0);
35
- const [addStep, setAddStep] = useState(0); // 0:输入key并加载模型, 1:选模型
34
+ const [addStep, setAddStep] = useState(0); // 0:选模型, 1:填key
36
35
  const [inputKey, setInputKey] = useState('');
37
36
  const [addError, setAddError] = useState('');
38
37
  const [mode, setMode] = useState<UiMode>('list');
@@ -45,9 +44,6 @@ const ProvidersMenu: React.FC = () => {
45
44
  const [msg, setMsg] = useState<string>('');
46
45
  const [detail, setDetail] = useState<SavedProvider | null>(null);
47
46
  const [presets, setPresets] = useState<any[]>([]);
48
- // 临时存储当前正在配置的 Provider 草案
49
- const [pendingProvider, setPendingProvider] = useState<CreateProviderInput | null>(null);
50
-
51
47
  useEffect(() => { ProviderManager.listPresets().then(setPresets).catch(()=>setPresets([])); }, []);
52
48
 
53
49
  const refresh = async () => {
@@ -58,14 +54,7 @@ const ProvidersMenu: React.FC = () => {
58
54
  };
59
55
  useEffect(() => { refresh(); }, []);
60
56
  useEffect(() => {
61
- if (mode !== 'add') {
62
- setAddStep(0);
63
- setInputKey('');
64
- setAddError('');
65
- setModelSelectIdx(0);
66
- setModelOptions([]);
67
- setPendingProvider(null);
68
- }
57
+ if (mode !== 'add') { setAddStep(0); setInputKey(''); setAddError(''); setModelSelectIdx(0); }
69
58
  }, [mode]);
70
59
 
71
60
  // 主界面 LIST 模式
@@ -107,55 +96,33 @@ const ProvidersMenu: React.FC = () => {
107
96
  setMode('list'); setAddError(''); setInputKey(''); setAddStep(0);
108
97
  return;
109
98
  }
110
- // 步骤1: 选模型
111
- if (addStep === 1 && modelOptions.length > 0) {
112
- if (key.downArrow) setModelSelectIdx(idx => Math.min(modelOptions.length - 1, idx + 1));
99
+ // 步骤0: 主模型选择
100
+ if (addStep === 0) {
101
+ if (key.downArrow) setModelSelectIdx(idx => Math.min(MODEL_OPTIONS.length - 1, idx + 1));
113
102
  else if (key.upArrow) setModelSelectIdx(idx => Math.max(0, idx - 1));
114
- else if (key.return) {
115
- completeAdd(modelOptions[modelSelectIdx]);
116
- }
103
+ else if (key.return) { setAddStep(1); }
117
104
  }
118
105
  });
119
106
 
120
- // 获取模型并进入下一步
121
- const fetchModelsStep = async (keyInput: string) => {
107
+ // 新增表单
108
+ const addSubmit = async (keyInput: string) => {
109
+ const selectedModel = MODEL_OPTIONS[modelSelectIdx];
110
+ const presetId = selectedModel.replace(/[^a-zA-Z0-9]/g, '') + '-preset';
111
+ const name = `${selectedModel} Provider`;
122
112
  if (!keyInput.trim()) { setAddError('API Key 不能为空'); return; }
123
- setAddError('正在获取模型列表...');
124
- try {
125
- // 如果没有 pendingProvider,手动创建一个(比如 Custom 模式)
126
- const draft = pendingProvider || {
127
- presetId: 'custom-' + Date.now(),
128
- name: 'Custom Provider',
129
- baseUrl: FIXED_BASEURL,
130
- apiFormat: FIXED_APIFMT,
131
- apiKey: keyInput.trim(),
132
- models: { main: '', haiku: '', sonnet: '', opus: '' }
133
- };
134
-
135
- const models = await ProviderManager.fetchModels(draft, keyInput.trim());
136
- if (models.length === 0) {
137
- setAddError('未获取到可用模型,请检查 API Key 或网络连通性。');
138
- return;
139
- }
140
- setModelOptions(models);
141
- setPendingProvider({ ...draft, apiKey: keyInput.trim() });
142
- setAddStep(1);
143
- setAddError('');
144
- } catch (e: any) {
145
- setAddError('获取模型失败: ' + (e.message || '未知错误'));
146
- }
147
- };
148
-
149
- const completeAdd = async (selectedModel: string) => {
150
- if (!pendingProvider) return;
151
- const name = pendingProvider.name || `${selectedModel} Provider`;
152
-
113
+ const exists = (await ProviderManager.listProviders()).some(p => p.id === presetId);
114
+ if (exists) { setAddError('该模型已添加过,如要更换Key请在列表编辑。'); return; }
115
+ setAddError('');
116
+ // 支持预设覆盖
117
+ const overrideBase = (global as any).__PM_BASEURL_OVERRIDE__ || FIXED_BASEURL;
118
+ const overrideFmt = (global as any).__PM_APIFMT_OVERRIDE__ || FIXED_APIFMT;
153
119
  await ProviderManager.addProvider({
154
- ...pendingProvider,
155
- name,
120
+ presetId, name, apiKey: keyInput.trim(),
121
+ baseUrl: overrideBase, apiFormat: overrideFmt,
156
122
  models: { main: selectedModel, haiku: '', sonnet: '', opus: '' }
157
123
  });
158
-
124
+ delete (global as any).__PM_BASEURL_OVERRIDE__;
125
+ delete (global as any).__PM_APIFMT_OVERRIDE__;
159
126
  setMode('list');
160
127
  setAddStep(0);
161
128
  setInputKey('');
@@ -186,25 +153,25 @@ const ProvidersMenu: React.FC = () => {
186
153
  {/* 新增 */}
187
154
  {mode === 'add' && (
188
155
  <Box flexDirection="column">
156
+ <Text>选择模型与输入API Key:</Text>
189
157
  {addStep === 0
190
158
  ? <>
191
- <Text>请输入 API Key (将自动拉取可用模型):</Text>
159
+ {MODEL_OPTIONS.map((m, idx) => (
160
+ <Text key={m} color={idx === modelSelectIdx ? 'yellow' : undefined}>
161
+ {idx === modelSelectIdx ? '> ' : ' '}{m}
162
+ </Text>
163
+ ))}
164
+ <Text color="gray">↑↓选择,回车下一步,q返回</Text>
165
+ </>
166
+ : <>
167
+ <Text>已选模型:{MODEL_OPTIONS[modelSelectIdx]}</Text>
192
168
  <TextInput
193
169
  value={inputKey}
194
170
  onChange={setInputKey}
195
- onSubmit={fetchModelsStep}
171
+ onSubmit={addSubmit}
196
172
  placeholder="请输入API Key"
197
173
  />
198
- <Text color="gray">输入后回车继续,q返回</Text>
199
- </>
200
- : <>
201
- <Text>获取成功!请选择主模型:</Text>
202
- {modelOptions.map((m, idx) => (
203
- <Text key={m} color={idx === modelSelectIdx ? 'yellow' : undefined}>
204
- {idx === modelSelectIdx ? '> ' : ' '}{m}
205
- </Text>
206
- ))}
207
- <Text color="gray">↑↓选择,回车保存,q返回</Text>
174
+ <Text color="gray">输入后回车提交,q返回</Text>
208
175
  </>
209
176
  }
210
177
  {addError && <Text color="red">{addError}</Text>}
@@ -292,14 +259,17 @@ const ProvidersMenu: React.FC = () => {
292
259
  <SelectInput
293
260
  items={presets.map((p: any) => ({ label: `${p.name || p.id} ${p.baseUrl || ''}`, value: p.id }))}
294
261
  onSelect={async it => {
295
- try {
296
- const draft = await ProviderManager.applyPreset(it.value);
297
- setPendingProvider(draft);
298
- setMode('add');
299
- setAddStep(0); // 去输入 Key
300
- } catch (e: any) {
301
- setMsg('预设加载失败: ' + e.message);
302
- }
262
+ const p = presets.find((x: any) => x.id === it.value);
263
+ if (!p) return;
264
+ const model = (p.defaultModel || MODEL_OPTIONS[0]);
265
+ const presetBase = p.baseUrl || FIXED_BASEURL;
266
+ const presetFmt = p.apiFormat || FIXED_APIFMT;
267
+ const idx = Math.max(0, MODEL_OPTIONS.findIndex(m => m === model));
268
+ setModelSelectIdx(idx);
269
+ (global as any).__PM_BASEURL_OVERRIDE__ = presetBase;
270
+ (global as any).__PM_APIFMT_OVERRIDE__ = presetFmt;
271
+ setMode('add');
272
+ setAddStep(1); // 直接跳到 Key 输入
303
273
  }}
304
274
  />
305
275
  <Text color="gray">回车选择预设,q 返回</Text>