koishi-plugin-elysia-api-aggregator 0.2.1 → 0.2.3

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.
Files changed (3) hide show
  1. package/lib/index.cjs +100 -39
  2. package/lib/index.mjs +100 -39
  3. package/package.json +1 -1
package/lib/index.cjs CHANGED
@@ -278,52 +278,113 @@ function apply(ctx, config) {
278
278
  getByType: /* @__PURE__ */ __name((type) => service.getByType(type), "getByType")
279
279
  }
280
280
  };
281
- async function loadModels() {
281
+ let isLoading = false;
282
+ let pendingReload = false;
283
+ let lastModelsHash = "";
284
+ let lastConfigHash = "";
285
+ const buildConfigHash = /* @__PURE__ */ __name(() => {
286
+ return JSON.stringify({
287
+ autoFetchSources: config.autoFetchSources,
288
+ manualModels: config.manualModels
289
+ });
290
+ }, "buildConfigHash");
291
+ const buildModelsHash = /* @__PURE__ */ __name((models) => {
292
+ const normalized = [...models].map((m) => ({
293
+ id: m.id,
294
+ name: m.name,
295
+ source: m.source,
296
+ sourceName: m.sourceName,
297
+ baseUrl: m.baseUrl,
298
+ platform: m.platform,
299
+ type: m.type,
300
+ maxTokens: m.maxTokens,
301
+ visionCapable: m.visionCapable,
302
+ toolsCapable: m.toolsCapable,
303
+ structuredOutput: m.structuredOutput,
304
+ thinkingMode: m.thinkingMode,
305
+ available: m.available
306
+ })).sort((a, b) => a.id.localeCompare(b.id));
307
+ return JSON.stringify(normalized);
308
+ }, "buildModelsHash");
309
+ async function loadModels(trigger = "config") {
310
+ if (isLoading) {
311
+ pendingReload = true;
312
+ if (config.debugMode) {
313
+ ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
314
+ }
315
+ return;
316
+ }
317
+ isLoading = true;
282
318
  const loadStartedAt = Date.now();
283
- ctx.logger.info("Loading models...");
284
- const fetchedModels = [];
285
- for (const source of config.autoFetchSources) {
286
- if (!source.enabled) continue;
287
- const sourceStartedAt = Date.now();
288
- const sourceModels = await fetcher.fetchModels(source);
289
- fetchedModels.push(...sourceModels);
290
- const sourceCostMs = Date.now() - sourceStartedAt;
291
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
319
+ try {
320
+ ctx.logger.info("Loading models...");
321
+ const fetchedModels = [];
322
+ for (const source of config.autoFetchSources) {
323
+ if (!source.enabled) continue;
324
+ const sourceStartedAt = Date.now();
325
+ const sourceModels = await fetcher.fetchModels(source);
326
+ fetchedModels.push(...sourceModels);
327
+ const sourceCostMs = Date.now() - sourceStartedAt;
328
+ ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
329
+ }
330
+ const manualModels = config.manualModels.map((m) => {
331
+ return {
332
+ id: m.id,
333
+ name: m.name,
334
+ source: "manual",
335
+ sourceName: m.sourceName,
336
+ baseUrl: m.baseUrl,
337
+ apiKey: m.apiKey,
338
+ platform: m.platform,
339
+ // 使用默认值
340
+ type: "llm",
341
+ maxTokens: 128e3,
342
+ visionCapable: false,
343
+ toolsCapable: false,
344
+ structuredOutput: false,
345
+ thinkingMode: "both",
346
+ available: true,
347
+ lastChecked: /* @__PURE__ */ new Date()
348
+ };
349
+ });
350
+ const allModels = [...fetchedModels, ...manualModels];
351
+ service.updateModels(allModels);
352
+ const totalCostMs = Date.now() - loadStartedAt;
353
+ ctx.logger.info(`[source] manual: ${manualModels.length} models`);
354
+ const modelsHash = buildModelsHash(allModels);
355
+ if (modelsHash === lastModelsHash) {
356
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
357
+ return;
358
+ }
359
+ lastModelsHash = modelsHash;
360
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
361
+ ctx.emit("elysia-api/models-updated", [...allModels]);
362
+ } finally {
363
+ isLoading = false;
364
+ if (pendingReload) {
365
+ pendingReload = false;
366
+ void loadModels("pending");
367
+ }
292
368
  }
293
- const manualModels = config.manualModels.map((m) => {
294
- return {
295
- id: m.id,
296
- name: m.name,
297
- source: "manual",
298
- sourceName: m.sourceName,
299
- baseUrl: m.baseUrl,
300
- apiKey: m.apiKey,
301
- platform: m.platform,
302
- // 使用默认值
303
- type: "llm",
304
- maxTokens: 128e3,
305
- visionCapable: false,
306
- toolsCapable: false,
307
- structuredOutput: false,
308
- thinkingMode: "both",
309
- available: true,
310
- lastChecked: /* @__PURE__ */ new Date()
311
- };
312
- });
313
- const allModels = [...fetchedModels, ...manualModels];
314
- service.updateModels(allModels);
315
- const totalCostMs = Date.now() - loadStartedAt;
316
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
317
- ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
318
- ctx.emit("elysia-api/models-updated", [...allModels]);
319
369
  }
320
370
  __name(loadModels, "loadModels");
321
- ctx.on("ready", loadModels);
371
+ lastConfigHash = buildConfigHash();
372
+ ctx.on("ready", () => {
373
+ void loadModels("ready");
374
+ });
322
375
  ctx.on("config", () => {
323
- loadModels();
376
+ const configHash = buildConfigHash();
377
+ if (configHash === lastConfigHash) {
378
+ if (config.debugMode) {
379
+ ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
380
+ }
381
+ return;
382
+ }
383
+ lastConfigHash = configHash;
384
+ void loadModels("config");
324
385
  });
325
386
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
326
- await loadModels();
387
+ await loadModels("command");
327
388
  const count = service.getAll().length;
328
389
  return `已加载 ${count} 个模型`;
329
390
  });
package/lib/index.mjs CHANGED
@@ -252,52 +252,113 @@ function apply(ctx, config) {
252
252
  getByType: /* @__PURE__ */ __name((type) => service.getByType(type), "getByType")
253
253
  }
254
254
  };
255
- async function loadModels() {
255
+ let isLoading = false;
256
+ let pendingReload = false;
257
+ let lastModelsHash = "";
258
+ let lastConfigHash = "";
259
+ const buildConfigHash = /* @__PURE__ */ __name(() => {
260
+ return JSON.stringify({
261
+ autoFetchSources: config.autoFetchSources,
262
+ manualModels: config.manualModels
263
+ });
264
+ }, "buildConfigHash");
265
+ const buildModelsHash = /* @__PURE__ */ __name((models) => {
266
+ const normalized = [...models].map((m) => ({
267
+ id: m.id,
268
+ name: m.name,
269
+ source: m.source,
270
+ sourceName: m.sourceName,
271
+ baseUrl: m.baseUrl,
272
+ platform: m.platform,
273
+ type: m.type,
274
+ maxTokens: m.maxTokens,
275
+ visionCapable: m.visionCapable,
276
+ toolsCapable: m.toolsCapable,
277
+ structuredOutput: m.structuredOutput,
278
+ thinkingMode: m.thinkingMode,
279
+ available: m.available
280
+ })).sort((a, b) => a.id.localeCompare(b.id));
281
+ return JSON.stringify(normalized);
282
+ }, "buildModelsHash");
283
+ async function loadModels(trigger = "config") {
284
+ if (isLoading) {
285
+ pendingReload = true;
286
+ if (config.debugMode) {
287
+ ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
288
+ }
289
+ return;
290
+ }
291
+ isLoading = true;
256
292
  const loadStartedAt = Date.now();
257
- ctx.logger.info("Loading models...");
258
- const fetchedModels = [];
259
- for (const source of config.autoFetchSources) {
260
- if (!source.enabled) continue;
261
- const sourceStartedAt = Date.now();
262
- const sourceModels = await fetcher.fetchModels(source);
263
- fetchedModels.push(...sourceModels);
264
- const sourceCostMs = Date.now() - sourceStartedAt;
265
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
293
+ try {
294
+ ctx.logger.info("Loading models...");
295
+ const fetchedModels = [];
296
+ for (const source of config.autoFetchSources) {
297
+ if (!source.enabled) continue;
298
+ const sourceStartedAt = Date.now();
299
+ const sourceModels = await fetcher.fetchModels(source);
300
+ fetchedModels.push(...sourceModels);
301
+ const sourceCostMs = Date.now() - sourceStartedAt;
302
+ ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
303
+ }
304
+ const manualModels = config.manualModels.map((m) => {
305
+ return {
306
+ id: m.id,
307
+ name: m.name,
308
+ source: "manual",
309
+ sourceName: m.sourceName,
310
+ baseUrl: m.baseUrl,
311
+ apiKey: m.apiKey,
312
+ platform: m.platform,
313
+ // 使用默认值
314
+ type: "llm",
315
+ maxTokens: 128e3,
316
+ visionCapable: false,
317
+ toolsCapable: false,
318
+ structuredOutput: false,
319
+ thinkingMode: "both",
320
+ available: true,
321
+ lastChecked: /* @__PURE__ */ new Date()
322
+ };
323
+ });
324
+ const allModels = [...fetchedModels, ...manualModels];
325
+ service.updateModels(allModels);
326
+ const totalCostMs = Date.now() - loadStartedAt;
327
+ ctx.logger.info(`[source] manual: ${manualModels.length} models`);
328
+ const modelsHash = buildModelsHash(allModels);
329
+ if (modelsHash === lastModelsHash) {
330
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
331
+ return;
332
+ }
333
+ lastModelsHash = modelsHash;
334
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
335
+ ctx.emit("elysia-api/models-updated", [...allModels]);
336
+ } finally {
337
+ isLoading = false;
338
+ if (pendingReload) {
339
+ pendingReload = false;
340
+ void loadModels("pending");
341
+ }
266
342
  }
267
- const manualModels = config.manualModels.map((m) => {
268
- return {
269
- id: m.id,
270
- name: m.name,
271
- source: "manual",
272
- sourceName: m.sourceName,
273
- baseUrl: m.baseUrl,
274
- apiKey: m.apiKey,
275
- platform: m.platform,
276
- // 使用默认值
277
- type: "llm",
278
- maxTokens: 128e3,
279
- visionCapable: false,
280
- toolsCapable: false,
281
- structuredOutput: false,
282
- thinkingMode: "both",
283
- available: true,
284
- lastChecked: /* @__PURE__ */ new Date()
285
- };
286
- });
287
- const allModels = [...fetchedModels, ...manualModels];
288
- service.updateModels(allModels);
289
- const totalCostMs = Date.now() - loadStartedAt;
290
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
291
- ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
292
- ctx.emit("elysia-api/models-updated", [...allModels]);
293
343
  }
294
344
  __name(loadModels, "loadModels");
295
- ctx.on("ready", loadModels);
345
+ lastConfigHash = buildConfigHash();
346
+ ctx.on("ready", () => {
347
+ void loadModels("ready");
348
+ });
296
349
  ctx.on("config", () => {
297
- loadModels();
350
+ const configHash = buildConfigHash();
351
+ if (configHash === lastConfigHash) {
352
+ if (config.debugMode) {
353
+ ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
354
+ }
355
+ return;
356
+ }
357
+ lastConfigHash = configHash;
358
+ void loadModels("config");
298
359
  });
299
360
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
300
- await loadModels();
361
+ await loadModels("command");
301
362
  const count = service.getAll().length;
302
363
  return `已加载 ${count} 个模型`;
303
364
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-elysia-api-aggregator",
3
3
  "description": "Inspired by New-API, the Elysia-API model aggregator plugin allows automatic fetching and manual configuration of available AI models, designed to work with the orchestrator plugin.",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",