@spacex110/core 0.1.11
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/dist/index.d.ts +620 -0
- package/dist/index.js +6401 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +16 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +41 -0
- package/src/api.ts +158 -0
- package/src/channel-manager.ts +790 -0
- package/src/config.ts +104 -0
- package/src/i18n.ts +66 -0
- package/src/index.ts +97 -0
- package/src/models.ts +202 -0
- package/src/providers.ts +249 -0
- package/src/storage.ts +135 -0
- package/src/strategies.ts +469 -0
- package/src/styles.css +246 -0
- package/src/types.ts +163 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Channel Manager - Headless AI Channel Management
|
|
3
|
+
* 统一的 AI 通道管理接口,无需 UI 即可集成到任何项目
|
|
4
|
+
*
|
|
5
|
+
* 功能分类:
|
|
6
|
+
* 1. 通道 CRUD:addChannel / updateChannel / deleteChannel / getChannel / getChannels
|
|
7
|
+
* 2. 通道查询:getChannelsByProvider / getChannelsByCapability / getChannelsByFormat / searchChannels
|
|
8
|
+
* 3. 激活管理:setActiveChannel / getActiveChannel / clearActiveChannel
|
|
9
|
+
* 4. 模型查询:getProviderModels / getChannelModels / getStaticModelList / getModelsByCapability
|
|
10
|
+
* 5. API Key 管理:updateApiKey / validateApiKey / maskApiKey
|
|
11
|
+
* 6. 聊天执行:chat (active channel) / chatWith (specific channel)
|
|
12
|
+
* 7. 连接测试:testChannel / testChannelById / testAllChannels
|
|
13
|
+
* 8. Provider 查询:getAvailableProviders / getProvider / getProvidersByFormat
|
|
14
|
+
* 9. 导入导出:exportChannels / importChannels
|
|
15
|
+
* 10. 事件系统:on / off / emit
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AIConfig, Provider, Model, ModelCapability, StorageAdapter, ApiFormat } from './types';
|
|
19
|
+
import { getProvider, getAllProviders, getProvidersByFormat } from './providers';
|
|
20
|
+
import { defaultStorageAdapter } from './storage';
|
|
21
|
+
import { sendDirectChat, sendDirectChatStream, testDirectConnection } from './strategies';
|
|
22
|
+
import { fetchModels } from './api';
|
|
23
|
+
import { getStaticModels } from './models';
|
|
24
|
+
|
|
25
|
+
// ============ Types ============
|
|
26
|
+
|
|
27
|
+
/** 通道配置(扩展 AIConfig,包含更多元数据) */
|
|
28
|
+
export interface ChannelConfig extends AIConfig {
|
|
29
|
+
/** 通道唯一标识 */
|
|
30
|
+
id: string;
|
|
31
|
+
/** 通道名称(用户自定义) */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Provider 名称 */
|
|
34
|
+
providerName: string;
|
|
35
|
+
/** 模型显示名称 */
|
|
36
|
+
modelName: string;
|
|
37
|
+
/** API 格式 */
|
|
38
|
+
apiFormat: ApiFormat;
|
|
39
|
+
/** 模型能力列表 */
|
|
40
|
+
capabilities?: ModelCapability[];
|
|
41
|
+
/** 上下文窗口大小 */
|
|
42
|
+
contextLength?: number;
|
|
43
|
+
/** 创建时间 */
|
|
44
|
+
createdAt: number;
|
|
45
|
+
/** 最后使用时间 */
|
|
46
|
+
lastUsedAt?: number;
|
|
47
|
+
/** 连接状态 */
|
|
48
|
+
status?: 'untested' | 'connected' | 'error';
|
|
49
|
+
/** 最后错误信息 */
|
|
50
|
+
lastError?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 通道管理器配置选项 */
|
|
54
|
+
export interface ChannelManagerOptions {
|
|
55
|
+
/** 存储适配器(默认使用加密存储) */
|
|
56
|
+
storageAdapter?: StorageAdapter;
|
|
57
|
+
/** 存储键名前缀 */
|
|
58
|
+
storagePrefix?: string;
|
|
59
|
+
/** 当前激活通道的存储键 */
|
|
60
|
+
activeChannelKey?: string;
|
|
61
|
+
/** 后端代理地址(可选,用于解决 CORS 问题) */
|
|
62
|
+
proxyUrl?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 通道查询过滤条件 */
|
|
66
|
+
export interface ChannelFilter {
|
|
67
|
+
/** 按 Provider ID 过滤 */
|
|
68
|
+
providerId?: string;
|
|
69
|
+
/** 按 API 格式过滤 */
|
|
70
|
+
apiFormat?: ApiFormat;
|
|
71
|
+
/** 按模型能力过滤 */
|
|
72
|
+
capability?: ModelCapability;
|
|
73
|
+
/** 按连接状态过滤 */
|
|
74
|
+
status?: ChannelConfig['status'];
|
|
75
|
+
/** 关键字搜索(匹配名称、模型名、Provider 名) */
|
|
76
|
+
keyword?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 聊天结果 */
|
|
80
|
+
export interface ChatResult {
|
|
81
|
+
success: boolean;
|
|
82
|
+
content?: string;
|
|
83
|
+
message?: string;
|
|
84
|
+
latencyMs?: number;
|
|
85
|
+
channelId?: string;
|
|
86
|
+
model?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 事件类型 */
|
|
90
|
+
export type ChannelEventType = 'add' | 'update' | 'delete' | 'activate' | 'chat' | 'test' | 'clear';
|
|
91
|
+
type ChannelEventListener = (data: any) => void;
|
|
92
|
+
|
|
93
|
+
// ============ AIChannelManager ============
|
|
94
|
+
|
|
95
|
+
export class AIChannelManager {
|
|
96
|
+
private storageAdapter: StorageAdapter;
|
|
97
|
+
private channels: ChannelConfig[] = [];
|
|
98
|
+
private activeChannelId: string | null = null;
|
|
99
|
+
private storagePrefix: string;
|
|
100
|
+
private activeChannelKey: string;
|
|
101
|
+
private proxyUrl?: string;
|
|
102
|
+
private listeners: Map<ChannelEventType, Set<ChannelEventListener>> = new Map();
|
|
103
|
+
|
|
104
|
+
constructor(options: ChannelManagerOptions = {}) {
|
|
105
|
+
this.storagePrefix = options.storagePrefix || 'ai_channel';
|
|
106
|
+
this.activeChannelKey = options.activeChannelKey || 'ai_active_channel';
|
|
107
|
+
this.proxyUrl = options.proxyUrl;
|
|
108
|
+
this.storageAdapter = options.storageAdapter || defaultStorageAdapter;
|
|
109
|
+
|
|
110
|
+
this.loadChannels();
|
|
111
|
+
this.loadActiveChannel();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ==================== 事件系统 ====================
|
|
115
|
+
|
|
116
|
+
/** 监听事件 */
|
|
117
|
+
on(event: ChannelEventType, listener: ChannelEventListener): () => void {
|
|
118
|
+
if (!this.listeners.has(event)) {
|
|
119
|
+
this.listeners.set(event, new Set());
|
|
120
|
+
}
|
|
121
|
+
this.listeners.get(event)!.add(listener);
|
|
122
|
+
return () => this.off(event, listener);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 取消监听 */
|
|
126
|
+
off(event: ChannelEventType, listener: ChannelEventListener): void {
|
|
127
|
+
this.listeners.get(event)?.delete(listener);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private emit(event: ChannelEventType, data: any): void {
|
|
131
|
+
this.listeners.get(event)?.forEach(fn => fn(data));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ==================== 存储相关 ====================
|
|
135
|
+
|
|
136
|
+
private getStorageKey(id: string): string {
|
|
137
|
+
return `${this.storagePrefix}_${id}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private loadChannels(): void {
|
|
141
|
+
try {
|
|
142
|
+
const keys = Object.keys(localStorage || {}).filter(key =>
|
|
143
|
+
key.startsWith(this.storagePrefix)
|
|
144
|
+
);
|
|
145
|
+
this.channels = keys.map(key => {
|
|
146
|
+
const raw = this.storageAdapter.get(key);
|
|
147
|
+
if (raw) return JSON.parse(raw) as ChannelConfig;
|
|
148
|
+
return null;
|
|
149
|
+
}).filter((c): c is ChannelConfig => c !== null);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error('Failed to load channels:', e);
|
|
152
|
+
this.channels = [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private loadActiveChannel(): void {
|
|
157
|
+
try {
|
|
158
|
+
const raw = this.storageAdapter.get(this.activeChannelKey);
|
|
159
|
+
if (raw) this.activeChannelId = raw;
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error('Failed to load active channel:', e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private saveChannel(channel: ChannelConfig): void {
|
|
166
|
+
try {
|
|
167
|
+
this.storageAdapter.set(this.getStorageKey(channel.id), JSON.stringify(channel));
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error('Failed to save channel:', e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private deleteChannelFromStorage(id: string): void {
|
|
174
|
+
try {
|
|
175
|
+
this.storageAdapter.remove(this.getStorageKey(id));
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('Failed to delete channel from storage:', e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private saveActiveChannel(): void {
|
|
182
|
+
try {
|
|
183
|
+
if (this.activeChannelId) {
|
|
184
|
+
this.storageAdapter.set(this.activeChannelKey, this.activeChannelId);
|
|
185
|
+
} else {
|
|
186
|
+
this.storageAdapter.remove(this.activeChannelKey);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('Failed to save active channel:', e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ==================== 通道 CRUD ====================
|
|
194
|
+
|
|
195
|
+
/** 添加通道 */
|
|
196
|
+
async addChannel(config: AIConfig, name?: string): Promise<ChannelConfig> {
|
|
197
|
+
const provider = getProvider(config.providerId);
|
|
198
|
+
if (!provider) {
|
|
199
|
+
throw new Error(`Provider not found: ${config.providerId}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let modelName = config.modelName || config.model;
|
|
203
|
+
let capabilities: ModelCapability[] = [];
|
|
204
|
+
let contextLength: number | undefined;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const models = await this.getProviderModels(config.providerId, config.apiKey, config.baseUrl);
|
|
208
|
+
const model = models.find(m => m.id === config.model);
|
|
209
|
+
if (model) {
|
|
210
|
+
modelName = model.name;
|
|
211
|
+
capabilities = model.capabilities || [];
|
|
212
|
+
contextLength = model.contextLength;
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.warn('Failed to fetch models, using static info:', e);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const channel: ChannelConfig = {
|
|
219
|
+
id: Date.now().toString() + Math.random().toString(36).slice(2, 6),
|
|
220
|
+
providerId: config.providerId,
|
|
221
|
+
providerName: provider.name,
|
|
222
|
+
model: config.model,
|
|
223
|
+
modelName,
|
|
224
|
+
apiKey: config.apiKey,
|
|
225
|
+
baseUrl: config.baseUrl,
|
|
226
|
+
apiFormat: provider.apiFormat,
|
|
227
|
+
capabilities,
|
|
228
|
+
contextLength,
|
|
229
|
+
name,
|
|
230
|
+
createdAt: Date.now(),
|
|
231
|
+
status: 'untested',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// 去重:相同 providerId + model + baseUrl 视为同一通道
|
|
235
|
+
const existingIndex = this.channels.findIndex(
|
|
236
|
+
c => c.providerId === channel.providerId &&
|
|
237
|
+
c.model === channel.model &&
|
|
238
|
+
c.baseUrl === channel.baseUrl
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (existingIndex >= 0) {
|
|
242
|
+
this.channels[existingIndex] = { ...channel, id: this.channels[existingIndex].id };
|
|
243
|
+
this.saveChannel(this.channels[existingIndex]);
|
|
244
|
+
this.emit('update', this.channels[existingIndex]);
|
|
245
|
+
return this.channels[existingIndex];
|
|
246
|
+
} else {
|
|
247
|
+
this.channels.unshift(channel);
|
|
248
|
+
this.saveChannel(channel);
|
|
249
|
+
this.emit('add', channel);
|
|
250
|
+
return channel;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 删除通道 */
|
|
255
|
+
deleteChannel(id: string): void {
|
|
256
|
+
const channel = this.getChannel(id);
|
|
257
|
+
if (!channel) return;
|
|
258
|
+
|
|
259
|
+
this.channels = this.channels.filter(c => c.id !== id);
|
|
260
|
+
this.deleteChannelFromStorage(id);
|
|
261
|
+
|
|
262
|
+
if (this.activeChannelId === id) {
|
|
263
|
+
this.activeChannelId = null;
|
|
264
|
+
this.saveActiveChannel();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.emit('delete', { id, channel });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 更新通道 */
|
|
271
|
+
async updateChannel(id: string, config: Partial<AIConfig>, name?: string): Promise<ChannelConfig> {
|
|
272
|
+
const channel = this.channels.find(c => c.id === id);
|
|
273
|
+
if (!channel) {
|
|
274
|
+
throw new Error(`Channel not found: ${id}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const updated: ChannelConfig = { ...channel, ...config };
|
|
278
|
+
if (name !== undefined) updated.name = name;
|
|
279
|
+
|
|
280
|
+
// 如果关键字段变更,重新获取模型信息
|
|
281
|
+
if (config.providerId || config.model || config.baseUrl) {
|
|
282
|
+
const provider = getProvider(updated.providerId);
|
|
283
|
+
if (provider) {
|
|
284
|
+
updated.providerName = provider.name;
|
|
285
|
+
updated.apiFormat = provider.apiFormat;
|
|
286
|
+
try {
|
|
287
|
+
const models = await this.getProviderModels(updated.providerId, updated.apiKey, updated.baseUrl);
|
|
288
|
+
const model = models.find(m => m.id === updated.model);
|
|
289
|
+
if (model) {
|
|
290
|
+
updated.modelName = model.name;
|
|
291
|
+
updated.capabilities = model.capabilities || [];
|
|
292
|
+
updated.contextLength = model.contextLength;
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.warn('Failed to fetch models:', e);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const index = this.channels.findIndex(c => c.id === id);
|
|
301
|
+
this.channels[index] = updated;
|
|
302
|
+
this.saveChannel(updated);
|
|
303
|
+
this.emit('update', updated);
|
|
304
|
+
|
|
305
|
+
return updated;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ==================== 通道查询 ====================
|
|
309
|
+
|
|
310
|
+
/** 获取所有通道 */
|
|
311
|
+
getChannels(): ChannelConfig[] {
|
|
312
|
+
return [...this.channels];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 根据 ID 获取通道 */
|
|
316
|
+
getChannel(id: string): ChannelConfig | undefined {
|
|
317
|
+
return this.channels.find(c => c.id === id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 按 Provider ID 过滤通道 */
|
|
321
|
+
getChannelsByProvider(providerId: string): ChannelConfig[] {
|
|
322
|
+
return this.channels.filter(c => c.providerId === providerId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 按模型能力过滤通道(如 vision, reasoning, audio) */
|
|
326
|
+
getChannelsByCapability(capability: ModelCapability): ChannelConfig[] {
|
|
327
|
+
return this.channels.filter(c => c.capabilities?.includes(capability));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 按 API 格式过滤通道(如 openai, anthropic, gemini) */
|
|
331
|
+
getChannelsByFormat(format: ApiFormat): ChannelConfig[] {
|
|
332
|
+
return this.channels.filter(c => c.apiFormat === format);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 按连接状态过滤通道 */
|
|
336
|
+
getChannelsByStatus(status: ChannelConfig['status']): ChannelConfig[] {
|
|
337
|
+
return this.channels.filter(c => c.status === status);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** 综合条件查询通道 */
|
|
341
|
+
queryChannels(filter: ChannelFilter): ChannelConfig[] {
|
|
342
|
+
return this.channels.filter(c => {
|
|
343
|
+
if (filter.providerId && c.providerId !== filter.providerId) return false;
|
|
344
|
+
if (filter.apiFormat && c.apiFormat !== filter.apiFormat) return false;
|
|
345
|
+
if (filter.capability && !c.capabilities?.includes(filter.capability)) return false;
|
|
346
|
+
if (filter.status && c.status !== filter.status) return false;
|
|
347
|
+
if (filter.keyword) {
|
|
348
|
+
const kw = filter.keyword.toLowerCase();
|
|
349
|
+
const haystack = `${c.name || ''} ${c.modelName} ${c.providerName} ${c.model}`.toLowerCase();
|
|
350
|
+
if (!haystack.includes(kw)) return false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 获取通道数量 */
|
|
357
|
+
getChannelCount(): number {
|
|
358
|
+
return this.channels.length;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ==================== 激活管理 ====================
|
|
362
|
+
|
|
363
|
+
/** 获取当前激活的通道 */
|
|
364
|
+
getActiveChannel(): ChannelConfig | undefined {
|
|
365
|
+
if (!this.activeChannelId) return undefined;
|
|
366
|
+
return this.getChannel(this.activeChannelId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 设置当前激活的通道 */
|
|
370
|
+
setActiveChannel(id: string): void {
|
|
371
|
+
const channel = this.getChannel(id);
|
|
372
|
+
if (!channel) {
|
|
373
|
+
throw new Error(`Channel not found: ${id}`);
|
|
374
|
+
}
|
|
375
|
+
this.activeChannelId = id;
|
|
376
|
+
this.saveActiveChannel();
|
|
377
|
+
|
|
378
|
+
const index = this.channels.findIndex(c => c.id === id);
|
|
379
|
+
this.channels[index].lastUsedAt = Date.now();
|
|
380
|
+
this.saveChannel(this.channels[index]);
|
|
381
|
+
|
|
382
|
+
this.emit('activate', channel);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 清除当前激活的通道 */
|
|
386
|
+
clearActiveChannel(): void {
|
|
387
|
+
this.activeChannelId = null;
|
|
388
|
+
this.saveActiveChannel();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ==================== 模型查询 ====================
|
|
392
|
+
|
|
393
|
+
/** 获取 Provider 的可用模型列表(动态获取 + 静态兜底) */
|
|
394
|
+
async getProviderModels(providerId: string, apiKey?: string, baseUrl?: string): Promise<Model[]> {
|
|
395
|
+
const provider = getProvider(providerId);
|
|
396
|
+
if (!provider) {
|
|
397
|
+
throw new Error(`Provider not found: ${providerId}`);
|
|
398
|
+
}
|
|
399
|
+
return fetchModels({
|
|
400
|
+
provider,
|
|
401
|
+
apiKey,
|
|
402
|
+
baseUrl,
|
|
403
|
+
proxyUrl: this.proxyUrl,
|
|
404
|
+
fallbackToStatic: true,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** 获取通道对应的 Provider 的模型列表 */
|
|
409
|
+
async getChannelModels(id: string): Promise<Model[]> {
|
|
410
|
+
const channel = this.getChannel(id);
|
|
411
|
+
if (!channel) {
|
|
412
|
+
throw new Error(`Channel not found: ${id}`);
|
|
413
|
+
}
|
|
414
|
+
return this.getProviderModels(channel.providerId, channel.apiKey, channel.baseUrl);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 获取 Provider 的静态模型列表(不发起网络请求) */
|
|
418
|
+
getStaticModelList(providerId: string): Model[] {
|
|
419
|
+
return getStaticModels(providerId);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 按能力过滤模型列表 */
|
|
423
|
+
getModelsByCapability(models: Model[], capability: ModelCapability): Model[] {
|
|
424
|
+
return models.filter(m => m.capabilities?.includes(capability));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** 获取所有已配置的模型摘要列表 */
|
|
428
|
+
getConfiguredModels(): Array<{
|
|
429
|
+
channelId: string;
|
|
430
|
+
providerId: string;
|
|
431
|
+
providerName: string;
|
|
432
|
+
model: string;
|
|
433
|
+
modelName: string;
|
|
434
|
+
apiFormat: ApiFormat;
|
|
435
|
+
capabilities: ModelCapability[];
|
|
436
|
+
status: ChannelConfig['status'];
|
|
437
|
+
}> {
|
|
438
|
+
return this.channels.map(c => ({
|
|
439
|
+
channelId: c.id,
|
|
440
|
+
providerId: c.providerId,
|
|
441
|
+
providerName: c.providerName,
|
|
442
|
+
model: c.model,
|
|
443
|
+
modelName: c.modelName,
|
|
444
|
+
apiFormat: c.apiFormat,
|
|
445
|
+
capabilities: c.capabilities || [],
|
|
446
|
+
status: c.status,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ==================== API Key 管理 ====================
|
|
451
|
+
|
|
452
|
+
/** 更新通道的 API Key */
|
|
453
|
+
async updateApiKey(id: string, apiKey: string): Promise<ChannelConfig> {
|
|
454
|
+
return this.updateChannel(id, { apiKey });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** 验证 API Key 格式(简单校验,非空且长度合理) */
|
|
458
|
+
validateApiKey(providerId: string, apiKey: string): { valid: boolean; message?: string } {
|
|
459
|
+
const provider = getProvider(providerId);
|
|
460
|
+
if (!provider) {
|
|
461
|
+
return { valid: false, message: `Provider not found: ${providerId}` };
|
|
462
|
+
}
|
|
463
|
+
if (!provider.needsApiKey) {
|
|
464
|
+
return { valid: true, message: '此 Provider 不需要 API Key' };
|
|
465
|
+
}
|
|
466
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
467
|
+
return { valid: false, message: 'API Key 不能为空' };
|
|
468
|
+
}
|
|
469
|
+
if (apiKey.length < 10) {
|
|
470
|
+
return { valid: false, message: 'API Key 长度过短' };
|
|
471
|
+
}
|
|
472
|
+
return { valid: true };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** 脱敏显示 API Key(如 sk-••••••••J4xK) */
|
|
476
|
+
maskApiKey(apiKey: string): string {
|
|
477
|
+
if (!apiKey || apiKey.length <= 8) return '••••••••';
|
|
478
|
+
return `${apiKey.slice(0, 4)}••••••••${apiKey.slice(-4)}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 获取通道的脱敏 API Key */
|
|
482
|
+
getMaskedApiKey(id: string): string {
|
|
483
|
+
const channel = this.getChannel(id);
|
|
484
|
+
if (!channel) return '';
|
|
485
|
+
return this.maskApiKey(channel.apiKey);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ==================== 聊天执行 ====================
|
|
489
|
+
|
|
490
|
+
/** 使用当前激活的通道发送聊天请求 */
|
|
491
|
+
async chat(
|
|
492
|
+
message: string,
|
|
493
|
+
options?: {
|
|
494
|
+
messages?: Array<{ role: string; content: string }>;
|
|
495
|
+
maxTokens?: number;
|
|
496
|
+
}
|
|
497
|
+
): Promise<ChatResult> {
|
|
498
|
+
const channel = this.getActiveChannel();
|
|
499
|
+
if (!channel) {
|
|
500
|
+
throw new Error('No active channel. Please set an active channel first.');
|
|
501
|
+
}
|
|
502
|
+
return this.chatWith(channel.id, message, options);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** 使用指定通道发送聊天请求 */
|
|
506
|
+
async chatWith(
|
|
507
|
+
channelId: string,
|
|
508
|
+
message: string,
|
|
509
|
+
options?: {
|
|
510
|
+
messages?: Array<{ role: string; content: string }>;
|
|
511
|
+
maxTokens?: number;
|
|
512
|
+
}
|
|
513
|
+
): Promise<ChatResult> {
|
|
514
|
+
const channel = this.getChannel(channelId);
|
|
515
|
+
if (!channel) {
|
|
516
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const provider = getProvider(channel.providerId);
|
|
520
|
+
if (!provider) {
|
|
521
|
+
throw new Error(`Provider not found: ${channel.providerId}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const messages = options?.messages || [{ role: 'user', content: message }];
|
|
525
|
+
|
|
526
|
+
const result = await sendDirectChat({
|
|
527
|
+
apiFormat: channel.apiFormat,
|
|
528
|
+
baseUrl: channel.baseUrl || provider.baseUrl,
|
|
529
|
+
apiKey: channel.apiKey,
|
|
530
|
+
model: channel.model,
|
|
531
|
+
messages,
|
|
532
|
+
maxTokens: options?.maxTokens,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const chatResult: ChatResult = {
|
|
536
|
+
...result,
|
|
537
|
+
channelId: channel.id,
|
|
538
|
+
model: channel.model,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
if (result.success) {
|
|
542
|
+
const index = this.channels.findIndex(c => c.id === channel.id);
|
|
543
|
+
this.channels[index].lastUsedAt = Date.now();
|
|
544
|
+
this.channels[index].status = 'connected';
|
|
545
|
+
this.channels[index].lastError = undefined;
|
|
546
|
+
this.saveChannel(this.channels[index]);
|
|
547
|
+
} else {
|
|
548
|
+
const index = this.channels.findIndex(c => c.id === channel.id);
|
|
549
|
+
this.channels[index].status = 'error';
|
|
550
|
+
this.channels[index].lastError = result.message;
|
|
551
|
+
this.saveChannel(this.channels[index]);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.emit('chat', chatResult);
|
|
555
|
+
return chatResult;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** 使用当前激活通道发送流式聊天。onDelta 每次收到「累计完整文本」。 */
|
|
559
|
+
async chatStream(
|
|
560
|
+
message: string,
|
|
561
|
+
options: {
|
|
562
|
+
messages?: Array<{ role: string; content: string }>;
|
|
563
|
+
maxTokens?: number;
|
|
564
|
+
onDelta: (full: string) => void;
|
|
565
|
+
signal?: AbortSignal;
|
|
566
|
+
}
|
|
567
|
+
): Promise<ChatResult> {
|
|
568
|
+
const channel = this.getActiveChannel();
|
|
569
|
+
if (!channel) {
|
|
570
|
+
throw new Error('No active channel. Please set an active channel first.');
|
|
571
|
+
}
|
|
572
|
+
return this.chatWithStream(channel.id, message, options);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 使用指定通道发送流式聊天。onDelta 每次收到「累计完整文本」。 */
|
|
576
|
+
async chatWithStream(
|
|
577
|
+
channelId: string,
|
|
578
|
+
message: string,
|
|
579
|
+
options: {
|
|
580
|
+
messages?: Array<{ role: string; content: string }>;
|
|
581
|
+
maxTokens?: number;
|
|
582
|
+
onDelta: (full: string) => void;
|
|
583
|
+
signal?: AbortSignal;
|
|
584
|
+
}
|
|
585
|
+
): Promise<ChatResult> {
|
|
586
|
+
const channel = this.getChannel(channelId);
|
|
587
|
+
if (!channel) {
|
|
588
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const provider = getProvider(channel.providerId);
|
|
592
|
+
if (!provider) {
|
|
593
|
+
throw new Error(`Provider not found: ${channel.providerId}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const messages = options.messages || [{ role: 'user', content: message }];
|
|
597
|
+
|
|
598
|
+
const result = await sendDirectChatStream({
|
|
599
|
+
apiFormat: channel.apiFormat,
|
|
600
|
+
baseUrl: channel.baseUrl || provider.baseUrl,
|
|
601
|
+
apiKey: channel.apiKey,
|
|
602
|
+
model: channel.model,
|
|
603
|
+
messages,
|
|
604
|
+
maxTokens: options.maxTokens,
|
|
605
|
+
onDelta: options.onDelta,
|
|
606
|
+
signal: options.signal,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const chatResult: ChatResult = {
|
|
610
|
+
...result,
|
|
611
|
+
channelId: channel.id,
|
|
612
|
+
model: channel.model,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const index = this.channels.findIndex(c => c.id === channel.id);
|
|
616
|
+
if (result.success) {
|
|
617
|
+
this.channels[index].lastUsedAt = Date.now();
|
|
618
|
+
this.channels[index].status = 'connected';
|
|
619
|
+
this.channels[index].lastError = undefined;
|
|
620
|
+
} else {
|
|
621
|
+
this.channels[index].status = 'error';
|
|
622
|
+
this.channels[index].lastError = result.message;
|
|
623
|
+
}
|
|
624
|
+
this.saveChannel(this.channels[index]);
|
|
625
|
+
this.emit('chat', chatResult);
|
|
626
|
+
return chatResult;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** 测试指定通道的连接 */
|
|
630
|
+
async testChannel(id: string): Promise<{ success: boolean; latencyMs?: number; message?: string }> {
|
|
631
|
+
const channel = this.getChannel(id);
|
|
632
|
+
if (!channel) {
|
|
633
|
+
throw new Error(`Channel not found: ${id}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const provider = getProvider(channel.providerId);
|
|
637
|
+
if (!provider) {
|
|
638
|
+
throw new Error(`Provider not found: ${channel.providerId}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const result = await testDirectConnection({
|
|
642
|
+
apiFormat: channel.apiFormat,
|
|
643
|
+
baseUrl: channel.baseUrl || provider.baseUrl,
|
|
644
|
+
apiKey: channel.apiKey,
|
|
645
|
+
model: channel.model,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// 更新通道状态
|
|
649
|
+
const index = this.channels.findIndex(c => c.id === id);
|
|
650
|
+
if (result.success) {
|
|
651
|
+
this.channels[index].status = 'connected';
|
|
652
|
+
this.channels[index].lastError = undefined;
|
|
653
|
+
} else {
|
|
654
|
+
this.channels[index].status = 'error';
|
|
655
|
+
this.channels[index].lastError = result.message;
|
|
656
|
+
}
|
|
657
|
+
this.saveChannel(this.channels[index]);
|
|
658
|
+
|
|
659
|
+
this.emit('test', { id, ...result });
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** 测试所有通道连接 */
|
|
664
|
+
async testAllChannels(): Promise<Array<{ id: string; channel: ChannelConfig; result: { success: boolean; latencyMs?: number; message?: string } }>> {
|
|
665
|
+
const results = await Promise.all(
|
|
666
|
+
this.channels.map(async channel => {
|
|
667
|
+
try {
|
|
668
|
+
const result = await this.testChannel(channel.id);
|
|
669
|
+
return { id: channel.id, channel, result };
|
|
670
|
+
} catch (e) {
|
|
671
|
+
return { id: channel.id, channel, result: { success: false, message: e instanceof Error ? e.message : String(e) } };
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
return results;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ==================== Provider 查询 ====================
|
|
679
|
+
|
|
680
|
+
/** 获取所有可用的 Provider */
|
|
681
|
+
getAvailableProviders(): Provider[] {
|
|
682
|
+
return getAllProviders();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** 获取单个 Provider 信息 */
|
|
686
|
+
getProvider(providerId: string): Provider | undefined {
|
|
687
|
+
return getProvider(providerId);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** 按 API 格式获取 Provider 列表 */
|
|
691
|
+
getProvidersByFormat(format: ApiFormat): Provider[] {
|
|
692
|
+
return getProvidersByFormat(format);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ==================== 导入导出 ====================
|
|
696
|
+
|
|
697
|
+
/** 导出所有通道配置(不包含 API Key 明文) */
|
|
698
|
+
exportChannels(includeApiKeys?: boolean): string {
|
|
699
|
+
const data = this.channels.map(c => {
|
|
700
|
+
if (includeApiKeys) return c;
|
|
701
|
+
const { apiKey, ...rest } = c;
|
|
702
|
+
return { ...rest, apiKey: this.maskApiKey(apiKey) };
|
|
703
|
+
});
|
|
704
|
+
return JSON.stringify({
|
|
705
|
+
version: 1,
|
|
706
|
+
exportedAt: Date.now(),
|
|
707
|
+
channels: data,
|
|
708
|
+
}, null, 2);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** 导入通道配置 */
|
|
712
|
+
async importChannels(json: string, merge: boolean = true): Promise<ChannelConfig[]> {
|
|
713
|
+
const data = JSON.parse(json);
|
|
714
|
+
if (!data.channels || !Array.isArray(data.channels)) {
|
|
715
|
+
throw new Error('Invalid import format: missing channels array');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!merge) {
|
|
719
|
+
this.clearAllChannels();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const imported: ChannelConfig[] = [];
|
|
723
|
+
for (const ch of data.channels) {
|
|
724
|
+
// 跳过被脱敏的 API Key
|
|
725
|
+
if (ch.apiKey && ch.apiKey.includes('••••')) {
|
|
726
|
+
console.warn(`Skipping channel ${ch.modelName}: API Key is masked`);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const config: AIConfig = {
|
|
731
|
+
providerId: ch.providerId,
|
|
732
|
+
apiKey: ch.apiKey,
|
|
733
|
+
model: ch.model,
|
|
734
|
+
modelName: ch.modelName,
|
|
735
|
+
baseUrl: ch.baseUrl,
|
|
736
|
+
};
|
|
737
|
+
const channel = await this.addChannel(config, ch.name);
|
|
738
|
+
imported.push(channel);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return imported;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ==================== 批量操作 ====================
|
|
745
|
+
|
|
746
|
+
/** 清空所有通道 */
|
|
747
|
+
clearAllChannels(): void {
|
|
748
|
+
this.channels.forEach(channel => {
|
|
749
|
+
this.deleteChannelFromStorage(channel.id);
|
|
750
|
+
});
|
|
751
|
+
this.channels = [];
|
|
752
|
+
this.activeChannelId = null;
|
|
753
|
+
this.saveActiveChannel();
|
|
754
|
+
this.emit('clear', {});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** 批量删除通道 */
|
|
758
|
+
deleteChannels(ids: string[]): void {
|
|
759
|
+
ids.forEach(id => this.deleteChannel(id));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** 获取统计信息 */
|
|
763
|
+
getStats(): {
|
|
764
|
+
total: number;
|
|
765
|
+
byStatus: Record<string, number>;
|
|
766
|
+
byFormat: Record<string, number>;
|
|
767
|
+
byProvider: Record<string, number>;
|
|
768
|
+
byCapability: Record<string, number>;
|
|
769
|
+
} {
|
|
770
|
+
const stats = {
|
|
771
|
+
total: this.channels.length,
|
|
772
|
+
byStatus: {} as Record<string, number>,
|
|
773
|
+
byFormat: {} as Record<string, number>,
|
|
774
|
+
byProvider: {} as Record<string, number>,
|
|
775
|
+
byCapability: {} as Record<string, number>,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
for (const c of this.channels) {
|
|
779
|
+
const status = c.status || 'untested';
|
|
780
|
+
stats.byStatus[status] = (stats.byStatus[status] || 0) + 1;
|
|
781
|
+
stats.byFormat[c.apiFormat] = (stats.byFormat[c.apiFormat] || 0) + 1;
|
|
782
|
+
stats.byProvider[c.providerId] = (stats.byProvider[c.providerId] || 0) + 1;
|
|
783
|
+
for (const cap of c.capabilities || []) {
|
|
784
|
+
stats.byCapability[cap] = (stats.byCapability[cap] || 0) + 1;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return stats;
|
|
789
|
+
}
|
|
790
|
+
}
|