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 +1 -1
- package/src/components/Onboarding.tsx +6 -2
- package/src/entrypoints/init.ts +1 -9
- package/src/manager/CliMenuManager.tsx +125 -52
- package/src/server/cli/providerManager.ts +18 -67
- package/src/server/cli/providersMenu.tsx +44 -74
- package/src/server/proxy/handler.ts +289 -302
- package/src/server/services/providerService.ts +24 -10
- package/src/utils/auth.ts +1 -1
- package/src/utils/config.ts +5 -11
- package/src/utils/preflightChecks.tsx +3 -3
- package/src/utils/proxy.ts +1 -13
- package/src/server/proxy/streaming/anthropicStreamLabeler.ts +0 -56
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/entrypoints/init.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
//
|
|
143
|
-
type
|
|
151
|
+
// 消息条目(与后端 MessageEntry 对齐)
|
|
152
|
+
type MessageEntry = {
|
|
144
153
|
id: string;
|
|
145
|
-
|
|
146
|
-
content: string
|
|
147
|
-
|
|
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<
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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={
|
|
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
|
-
<
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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={
|
|
905
|
-
<Text>
|
|
906
|
-
<
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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}
|
|
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
|
|
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
|
-
// 步骤
|
|
111
|
-
if (addStep ===
|
|
112
|
-
if (key.downArrow) setModelSelectIdx(idx => Math.min(
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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={
|
|
171
|
+
onSubmit={addSubmit}
|
|
196
172
|
placeholder="请输入API Key"
|
|
197
173
|
/>
|
|
198
|
-
<Text color="gray"
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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>
|