@spacex110/core 0.1.12 → 0.1.14

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": "@spacex110/core",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Framework-agnostic AI Provider Selector core utilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/api.ts CHANGED
@@ -58,7 +58,7 @@ export async function testConnection(options: TestConnectionOptions): Promise<Te
58
58
  };
59
59
  }
60
60
 
61
- const headers = strategy.buildHeaders(apiKey);
61
+ const headers = strategy.buildHeaders(apiKey, actualBaseUrl);
62
62
  // 使用聊天接口进行测试,发送最简单的请求
63
63
  const testPayload = strategy.buildChatPayload(targetModel, [{ role: 'user', content: 'Hi' }]);
64
64
  const endpoint = strategy.getChatEndpoint(actualBaseUrl, apiKey, targetModel);
@@ -124,7 +124,7 @@ export async function fetchModels(options: FetchModelsOptions): Promise<Model[]>
124
124
  const strategy = getStrategy(provider.apiFormat);
125
125
  if (strategy.getModelsEndpoint) {
126
126
  const endpoint = strategy.getModelsEndpoint(baseUrl || provider.baseUrl, apiKey || '');
127
- const headers = strategy.buildHeaders(apiKey || '');
127
+ const headers = strategy.buildHeaders(apiKey || '', baseUrl || provider.baseUrl);
128
128
 
129
129
  const response = await fetch(endpoint, {
130
130
  method: 'GET',
@@ -58,6 +58,8 @@ export interface ChannelManagerOptions {
58
58
  storagePrefix?: string;
59
59
  /** 当前激活通道的存储键 */
60
60
  activeChannelKey?: string;
61
+ /** 能力默认通道的存储键 */
62
+ capabilityDefaultsKey?: string;
61
63
  /** 后端代理地址(可选,用于解决 CORS 问题) */
62
64
  proxyUrl?: string;
63
65
  }
@@ -98,17 +100,21 @@ export class AIChannelManager {
98
100
  private activeChannelId: string | null = null;
99
101
  private storagePrefix: string;
100
102
  private activeChannelKey: string;
103
+ private capabilityDefaultsKey: string;
104
+ private capabilityDefaults: Record<ModelCapability, string> = {} as Record<ModelCapability, string>;
101
105
  private proxyUrl?: string;
102
106
  private listeners: Map<ChannelEventType, Set<ChannelEventListener>> = new Map();
103
107
 
104
108
  constructor(options: ChannelManagerOptions = {}) {
105
109
  this.storagePrefix = options.storagePrefix || 'ai_channel';
106
110
  this.activeChannelKey = options.activeChannelKey || 'ai_active_channel';
111
+ this.capabilityDefaultsKey = options.capabilityDefaultsKey || 'ai_capability_defaults';
107
112
  this.proxyUrl = options.proxyUrl;
108
113
  this.storageAdapter = options.storageAdapter || defaultStorageAdapter;
109
114
 
110
115
  this.loadChannels();
111
116
  this.loadActiveChannel();
117
+ this.loadCapabilityDefaults();
112
118
  }
113
119
 
114
120
  // ==================== 事件系统 ====================
@@ -190,6 +196,37 @@ export class AIChannelManager {
190
196
  }
191
197
  }
192
198
 
199
+ private loadCapabilityDefaults(): void {
200
+ try {
201
+ const raw = this.storageAdapter.get(this.capabilityDefaultsKey);
202
+ if (raw) {
203
+ const parsed = JSON.parse(raw);
204
+ this.capabilityDefaults = parsed as Record<ModelCapability, string>;
205
+ }
206
+ } catch (e) {
207
+ console.error('Failed to load capability defaults:', e);
208
+ }
209
+ }
210
+
211
+ private saveCapabilityDefaults(): void {
212
+ try {
213
+ const keys = Object.keys(this.capabilityDefaults).filter(
214
+ k => this.capabilityDefaults[k as ModelCapability]
215
+ );
216
+ if (keys.length > 0) {
217
+ const obj: Record<string, string> = {};
218
+ for (const k of keys) {
219
+ obj[k] = this.capabilityDefaults[k as ModelCapability]!;
220
+ }
221
+ this.storageAdapter.set(this.capabilityDefaultsKey, JSON.stringify(obj));
222
+ } else {
223
+ this.storageAdapter.remove(this.capabilityDefaultsKey);
224
+ }
225
+ } catch (e) {
226
+ console.error('Failed to save capability defaults:', e);
227
+ }
228
+ }
229
+
193
230
  // ==================== 通道 CRUD ====================
194
231
 
195
232
  /** 添加通道 */
@@ -388,6 +425,118 @@ export class AIChannelManager {
388
425
  this.saveActiveChannel();
389
426
  }
390
427
 
428
+ // ==================== 能力默认通道 ====================
429
+
430
+ /**
431
+ * 为指定能力类型设置默认通道
432
+ * 当调用 getChannelForCapability 时,优先返回此通道
433
+ */
434
+ setDefaultForCapability(capability: ModelCapability, channelId: string): void {
435
+ if (!this.getChannel(channelId)) {
436
+ throw new Error(`Channel not found: ${channelId}`);
437
+ }
438
+ this.capabilityDefaults[capability] = channelId;
439
+ this.saveCapabilityDefaults();
440
+ this.emit('update', { type: 'capabilityDefault', capability, channelId });
441
+ }
442
+
443
+ /**
444
+ * 获取指定能力类型的默认通道 ID
445
+ * 返回 undefined 表示未设置
446
+ */
447
+ getDefaultForCapability(capability: ModelCapability): string | undefined {
448
+ return this.capabilityDefaults[capability] || undefined;
449
+ }
450
+
451
+ /**
452
+ * 清除指定能力类型的默认通道
453
+ */
454
+ clearDefaultForCapability(capability: ModelCapability): void {
455
+ delete this.capabilityDefaults[capability];
456
+ this.saveCapabilityDefaults();
457
+ }
458
+
459
+ /**
460
+ * 获取所有能力类型的默认通道映射
461
+ * 返回 { capability: channelId } 的对象
462
+ */
463
+ getAllCapabilityDefaults(): Record<string, string> {
464
+ return { ...this.capabilityDefaults };
465
+ }
466
+
467
+ /**
468
+ * 根据能力类型获取最适合的通道(带降级链)
469
+ *
470
+ * 降级链:
471
+ * 1. 该能力类型的显式默认通道
472
+ * 2. 具有该能力且最后使用时间最新的通道
473
+ * 3. 全局激活通道
474
+ * 4. 第一个可用通道
475
+ */
476
+ getChannelForCapability(capability: ModelCapability): ChannelConfig | undefined {
477
+ // 1. 显式默认
478
+ const defaultId = this.capabilityDefaults[capability];
479
+ if (defaultId) {
480
+ const ch = this.getChannel(defaultId);
481
+ if (ch) return ch;
482
+ }
483
+
484
+ // 2. 该能力中最近使用的
485
+ const byCap = this.getChannelsByCapability(capability)
486
+ .filter(c => c.lastUsedAt)
487
+ .sort((a, b) => (b.lastUsedAt || 0) - (a.lastUsedAt || 0));
488
+ if (byCap.length > 0) return byCap[0];
489
+
490
+ // 3. 全局激活
491
+ const active = this.getActiveChannel();
492
+ if (active) return active;
493
+
494
+ // 4. 第一个可用
495
+ return this.channels[0];
496
+ }
497
+
498
+ /**
499
+ * 使用指定能力类型的默认通道发送聊天请求
500
+ * 自动选择合适的通道,无需手动指定 channelId
501
+ */
502
+ async chatForCapability(
503
+ capability: ModelCapability,
504
+ message: string,
505
+ options?: {
506
+ messages?: Array<{ role: string; content: string }>;
507
+ maxTokens?: number;
508
+ }
509
+ ): Promise<ChatResult> {
510
+ const channel = this.getChannelForCapability(capability);
511
+ if (!channel) {
512
+ throw new Error(`No available channel for capability: ${capability}`);
513
+ }
514
+ return this.chatWith(channel.id, message, options);
515
+ }
516
+
517
+ /**
518
+ * 使用指定能力类型的默认通道发送流式聊天请求
519
+ */
520
+ async chatStreamForCapability(
521
+ capability: ModelCapability,
522
+ message: string,
523
+ options?: {
524
+ messages?: Array<{ role: string; content: string }>;
525
+ maxTokens?: number;
526
+ signal?: AbortSignal;
527
+ onDelta?: (full: string) => void;
528
+ }
529
+ ): Promise<ChatResult> {
530
+ const channel = this.getChannelForCapability(capability);
531
+ if (!channel) {
532
+ throw new Error(`No available channel for capability: ${capability}`);
533
+ }
534
+ return this.chatWithStream(channel.id, message, {
535
+ ...options,
536
+ onDelta: options?.onDelta || ((_: string) => {}),
537
+ });
538
+ }
539
+
391
540
  // ==================== 模型查询 ====================
392
541
 
393
542
  /** 获取 Provider 的可用模型列表(动态获取 + 静态兜底) */
@@ -751,6 +900,8 @@ export class AIChannelManager {
751
900
  this.channels = [];
752
901
  this.activeChannelId = null;
753
902
  this.saveActiveChannel();
903
+ this.capabilityDefaults = {} as Record<ModelCapability, string>;
904
+ this.saveCapabilityDefaults();
754
905
  this.emit('clear', {});
755
906
  }
756
907
 
package/src/strategies.ts CHANGED
@@ -12,7 +12,7 @@ export interface ProviderStrategy {
12
12
  /** 从一个 SSE data 块中提取增量文本(流式)。返回空字符串表示无内容。 */
13
13
  parseStreamChunk?: (data: any) => string;
14
14
  // Headers
15
- buildHeaders: (apiKey: string) => Record<string, string>;
15
+ buildHeaders: (apiKey: string, baseUrl?: string) => Record<string, string>;
16
16
  }
17
17
 
18
18
  /**
@@ -115,13 +115,16 @@ const openaiStrategy: ProviderStrategy = {
115
115
  format: 'openai',
116
116
  getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
117
117
  getChatEndpoint: (baseUrl) => `${baseUrl}/chat/completions`,
118
- buildHeaders: (apiKey) => {
118
+ buildHeaders: (apiKey, baseUrl) => {
119
119
  const headers: Record<string, string> = {
120
120
  'Content-Type': 'application/json',
121
121
  'Authorization': `Bearer ${apiKey}`,
122
122
  };
123
- // OpenRouter compatibility & Best Practices
124
- if (typeof window !== 'undefined' && window.location) {
123
+ // 仅对 OpenRouter HTTP-Referer / X-Title(OpenRouter Best Practices 推荐);
124
+ // 其他 OpenAI 兼容厂商(如 DeepSeek/月之暗面/MiniMax/通义/智谱等)的 CORS 预检响应
125
+ // 通常不包含这两个头,加了会导致浏览器拒绝实际请求(net::ERR_FAILED)。
126
+ const isOpenRouter = !!baseUrl && /openrouter\.ai/i.test(baseUrl);
127
+ if (isOpenRouter && typeof window !== 'undefined' && window.location) {
125
128
  headers['HTTP-Referer'] = window.location.origin;
126
129
  // document.title 可能含中文等非 ASCII 字符,HTTP header 只允许 ISO-8859-1
127
130
  const safeTitle = document.title ? document.title.replace(/[^\x20-\x7E]/g, '').slice(0, 80) : 'AI Selector';
@@ -309,7 +312,7 @@ export async function sendDirectChat(options: DirectChatOptions): Promise<Direct
309
312
  const strategy = getStrategy(apiFormat);
310
313
 
311
314
  const endpoint = strategy.getChatEndpoint(baseUrl, apiKey, model);
312
- const headers = strategy.buildHeaders(apiKey);
315
+ const headers = strategy.buildHeaders(apiKey, baseUrl);
313
316
  const payload = strategy.buildChatPayload(model, messages, maxTokens);
314
317
 
315
318
  const startTime = performance.now();
@@ -373,7 +376,7 @@ export async function sendDirectChatStream(options: DirectChatStreamOptions): Pr
373
376
  endpoint = `${baseUrl}/models/${model}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`;
374
377
  }
375
378
 
376
- const headers = strategy.buildHeaders(apiKey);
379
+ const headers = strategy.buildHeaders(apiKey, baseUrl);
377
380
  const payload = strategy.buildChatPayload(model, messages, maxTokens);
378
381
  (payload as any).stream = true; // gemini 通过端点参数控制流式,stream 字段会被忽略,无副作用
379
382