aicodeswitch 3.9.3 → 3.9.4

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.
@@ -209,6 +209,8 @@ class FileSystemDatabaseManager {
209
209
  yield this.loadAllData();
210
210
  // 执行数据源类型迁移(在加载数据之后)
211
211
  yield this.migrateSourceTypes();
212
+ // OpenAI base URL 迁移:将末尾 /v1 自动移除
213
+ yield this.migrateOpenAIBaseUrls();
212
214
  // 确保默认配置
213
215
  yield this.ensureDefaultConfig();
214
216
  });
@@ -357,6 +359,41 @@ class FileSystemDatabaseManager {
357
359
  console.log(`[TypeMigration] Migration completed. Migrated ${migratedCount} services.`);
358
360
  });
359
361
  }
362
+ /**
363
+ * 迁移 OpenAI base URL(在初始化时执行)
364
+ * 仅处理 sourceType=openai 且 apiUrl 末尾为 /v1 的服务
365
+ */
366
+ migrateOpenAIBaseUrls() {
367
+ return __awaiter(this, void 0, void 0, function* () {
368
+ console.log('[OpenAIBaseUrlMigration] Checking for OpenAI base URL migration...');
369
+ let migratedCount = 0;
370
+ for (const vendor of this.vendors) {
371
+ if (!vendor.services)
372
+ continue;
373
+ for (const service of vendor.services) {
374
+ if (service.sourceType !== 'openai' || typeof service.apiUrl !== 'string') {
375
+ continue;
376
+ }
377
+ const trimmedUrl = service.apiUrl.trim();
378
+ if (!/\/v1\/?$/i.test(trimmedUrl)) {
379
+ continue;
380
+ }
381
+ const migratedUrl = trimmedUrl.replace(/\/v1\/?$/i, '');
382
+ if (migratedUrl && migratedUrl !== service.apiUrl) {
383
+ console.log(`[OpenAIBaseUrlMigration] Migrated service "${service.name}": ${service.apiUrl} -> ${migratedUrl}`);
384
+ service.apiUrl = migratedUrl;
385
+ migratedCount++;
386
+ }
387
+ }
388
+ }
389
+ if (migratedCount === 0) {
390
+ console.log('[OpenAIBaseUrlMigration] No migration needed');
391
+ return;
392
+ }
393
+ yield this.saveVendors();
394
+ console.log(`[OpenAIBaseUrlMigration] Migration completed. Migrated ${migratedCount} services.`);
395
+ });
396
+ }
360
397
  /**
361
398
  * 迁移导入数据中的类型
362
399
  * 用于导入功能,自动将旧类型转换为新类型
@@ -364,7 +401,13 @@ class FileSystemDatabaseManager {
364
401
  migrateVendorsOnImport(vendors) {
365
402
  return vendors.map(vendor => {
366
403
  var _a;
367
- return (Object.assign(Object.assign({}, vendor), { services: (_a = vendor.services) === null || _a === void 0 ? void 0 : _a.map(service => (Object.assign(Object.assign({}, service), { sourceType: service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined }))) }));
404
+ return (Object.assign(Object.assign({}, vendor), { services: (_a = vendor.services) === null || _a === void 0 ? void 0 : _a.map(service => {
405
+ const normalizedSourceType = service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined;
406
+ const normalizedApiUrl = normalizedSourceType === 'openai' && typeof service.apiUrl === 'string'
407
+ ? service.apiUrl.trim().replace(/\/v1\/?$/i, '')
408
+ : service.apiUrl;
409
+ return Object.assign(Object.assign({}, service), { sourceType: normalizedSourceType, apiUrl: normalizedApiUrl });
410
+ }) }));
368
411
  });
369
412
  }
370
413
  saveVendors() {
@@ -104,6 +104,31 @@ const asyncHandler = (handler) => (req, res, next) => {
104
104
  next(err);
105
105
  });
106
106
  };
107
+ const OPENAI_V1_SUFFIX_RE = /\/v1\/?$/i;
108
+ const validateOpenAIServiceBaseUrl = (service) => {
109
+ if ((service === null || service === void 0 ? void 0 : service.sourceType) !== 'openai') {
110
+ return null;
111
+ }
112
+ if (typeof service.apiUrl !== 'string') {
113
+ return null;
114
+ }
115
+ if (!OPENAI_V1_SUFFIX_RE.test(service.apiUrl.trim())) {
116
+ return null;
117
+ }
118
+ return 'OpenAI 数据源请填写不包含 /v1 的 base URL,例如:https://api.openai.com';
119
+ };
120
+ const validateOpenAIServiceBaseUrlsInVendorPayload = (vendorBody) => {
121
+ if (!Array.isArray(vendorBody === null || vendorBody === void 0 ? void 0 : vendorBody.services)) {
122
+ return null;
123
+ }
124
+ for (const service of vendorBody.services) {
125
+ const error = validateOpenAIServiceBaseUrl(service);
126
+ if (error) {
127
+ return error;
128
+ }
129
+ }
130
+ return null;
131
+ };
107
132
  const writeClaudeConfig = (dbManager, enableAgentTeams, enableBypassPermissionsSupport) => __awaiter(void 0, void 0, void 0, function* () {
108
133
  try {
109
134
  const homeDir = os_1.default.homedir();
@@ -902,8 +927,22 @@ const registerRoutes = (dbManager, proxyServer) => {
902
927
  }
903
928
  });
904
929
  app.get('/api/vendors', (_req, res) => res.json(dbManager.getVendors()));
905
- app.post('/api/vendors', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.createVendor(req.body)); })));
906
- app.put('/api/vendors/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.updateVendor(req.params.id, req.body)); })));
930
+ app.post('/api/vendors', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
931
+ const error = validateOpenAIServiceBaseUrlsInVendorPayload(req.body);
932
+ if (error) {
933
+ res.status(400).json({ error });
934
+ return;
935
+ }
936
+ res.json(yield dbManager.createVendor(req.body));
937
+ })));
938
+ app.put('/api/vendors/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
939
+ const error = validateOpenAIServiceBaseUrlsInVendorPayload(req.body);
940
+ if (error) {
941
+ res.status(400).json({ error });
942
+ return;
943
+ }
944
+ res.json(yield dbManager.updateVendor(req.params.id, req.body));
945
+ })));
907
946
  app.delete('/api/vendors/:id', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
908
947
  try {
909
948
  const result = yield dbManager.deleteVendor(req.params.id);
@@ -921,11 +960,29 @@ const registerRoutes = (dbManager, proxyServer) => {
921
960
  });
922
961
  app.post('/api/services', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
923
962
  console.log('[创建服务] 请求数据:', JSON.stringify(req.body, null, 2));
963
+ const error = validateOpenAIServiceBaseUrl(req.body);
964
+ if (error) {
965
+ res.status(400).json({ error });
966
+ return;
967
+ }
924
968
  const result = yield dbManager.createAPIService(req.body);
925
969
  console.log('[创建服务] 创建结果:', JSON.stringify(result, null, 2));
926
970
  res.json(result);
927
971
  })));
928
- app.put('/api/services/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.json(yield dbManager.updateAPIService(req.params.id, req.body)); })));
972
+ app.put('/api/services/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
973
+ const existingService = dbManager.getAPIService(req.params.id);
974
+ if (!existingService) {
975
+ res.status(404).json({ error: '服务不存在' });
976
+ return;
977
+ }
978
+ const mergedService = Object.assign(Object.assign({}, existingService), req.body);
979
+ const error = validateOpenAIServiceBaseUrl(mergedService);
980
+ if (error) {
981
+ res.status(400).json({ error });
982
+ return;
983
+ }
984
+ res.json(yield dbManager.updateAPIService(req.params.id, req.body));
985
+ })));
929
986
  app.delete('/api/services/:id', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
930
987
  console.log('[删除服务] 请求 ID:', req.params.id);
931
988
  try {
@@ -1714,19 +1714,15 @@ class ProxyServer {
1714
1714
  }
1715
1715
  /**
1716
1716
  * 构建 OpenAI Responses 类型的完整 URL
1717
- * - baseUrl /v{number} 结尾时,直接拼接请求路径
1718
- * - baseUrl 不带版本时,自动补 /v1 再拼接请求路径
1719
- * - 兼容请求路径本身已携带版本前缀(如 /v1/responses)场景
1717
+ * - 用户填写不含 /v1 的 baseUrl
1718
+ * - 服务端固定拼接 /v1 + 请求路径
1720
1719
  */
1721
1720
  buildOpenAIResponsesUrl(baseUrl, mappedPath) {
1722
1721
  const trimmedBase = baseUrl.trim().replace(/\/+$/, '');
1723
1722
  const normalizedPath = mappedPath.startsWith('/') || mappedPath === '' ? mappedPath : `/${mappedPath}`;
1724
- const baseHasVersionSuffix = /\/v\d+$/i.test(trimmedBase);
1725
- const pathHasVersionPrefix = /^\/v\d+(?:\/|$)/i.test(normalizedPath);
1726
- if (baseHasVersionSuffix || pathHasVersionPrefix) {
1727
- return `${trimmedBase}${normalizedPath}`;
1728
- }
1729
- return `${trimmedBase}/v1${normalizedPath}`;
1723
+ const pathWithoutVersionPrefix = normalizedPath.replace(/^\/v\d+(?=\/|$)/i, '');
1724
+ const normalizedResponsesPath = pathWithoutVersionPrefix || '/responses';
1725
+ return `${trimmedBase}/v1${normalizedResponsesPath}`;
1730
1726
  }
1731
1727
  /**
1732
1728
  * 构建 Gemini API 的完整 URL
@@ -2470,7 +2466,13 @@ class ProxyServer {
2470
2466
  upstreamUrl = this.buildGeminiUrl(service.apiUrl, model, streamRequested);
2471
2467
  }
2472
2468
  else if (sourceType === 'openai') {
2473
- // OpenAI Responses 兼容模式:自动处理 baseUrl 是否包含 /v{number}
2469
+ if (/\/v1\/?$/i.test(service.apiUrl.trim())) {
2470
+ const error = 'OpenAI 数据源请填写不包含 /v1 的 base URL,例如:https://api.openai.com';
2471
+ res.status(400).json({ error });
2472
+ yield finalizeLog(400, error);
2473
+ return;
2474
+ }
2475
+ // OpenAI Responses:固定拼接为 {baseUrl}/v1/*
2474
2476
  upstreamUrl = this.buildOpenAIResponsesUrl(service.apiUrl, mappedPath);
2475
2477
  }
2476
2478
  else if (this.isChatType(sourceType) || this.isGeminiChatSource(sourceType)) {