tt-help-cli-ycl 1.4.21 → 1.4.23

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.
@@ -50,11 +50,59 @@ export function insertTag(tag, countries, source = "llm") {
50
50
  }
51
51
  }
52
52
 
53
+ /**
54
+ * 批量插入 tag,只插入不存在的(INSERT OR IGNORE)。
55
+ * 用于非 LLM 来源(如视频页采集),不覆盖已有 tag 的任何字段。
56
+ *
57
+ * @param {string[]} tags - 原始 tag 名列表(可带 # 前缀)
58
+ * @param {string[]} countries - 国家列表
59
+ * @param {string} source - 来源标识,默认 'video'
60
+ * @returns {{ inserted: number, tags: string[] }}
61
+ */
62
+ export function insertTagsIfNew(tags, countries, source = "video") {
63
+ const db = getDb();
64
+ if (!db) return { inserted: 0, tags: [] };
65
+ if (!Array.isArray(tags) || tags.length === 0)
66
+ return { inserted: 0, tags: [] };
67
+ if (!Array.isArray(countries) || countries.length === 0)
68
+ return { inserted: 0, tags: [] };
69
+
70
+ const normalizedList = tags
71
+ .map((t) => t.replace(/^#+/, "").trim().toLowerCase())
72
+ .filter((t) => t.length >= 2);
73
+
74
+ if (normalizedList.length === 0) return { inserted: 0, tags: [] };
75
+
76
+ const countriesJson = JSON.stringify(countries);
77
+ let inserted = 0;
78
+ const insertedTags = [];
79
+
80
+ try {
81
+ const stmt = db.prepare(
82
+ `INSERT OR IGNORE INTO tags (tag, countries, source) VALUES (?, ?, ?)`,
83
+ );
84
+ const txn = db.transaction((items) => {
85
+ for (const tag of items) {
86
+ const result = stmt.run(tag, countriesJson, source);
87
+ if (result.changes > 0) {
88
+ inserted++;
89
+ insertedTags.push(tag);
90
+ }
91
+ }
92
+ });
93
+ txn(normalizedList);
94
+ return { inserted, tags: insertedTags };
95
+ } catch (e) {
96
+ return { inserted, error: e.message };
97
+ }
98
+ }
99
+
53
100
  export function getTagsByStatus(
54
101
  status,
55
102
  limit = 100,
56
103
  offset = 0,
57
104
  country = null,
105
+ search = null,
58
106
  ) {
59
107
  const db = getDb();
60
108
  if (!db) return [];
@@ -64,6 +112,10 @@ export function getTagsByStatus(
64
112
  sql += " AND countries LIKE ?";
65
113
  params.push(`%"${country}"%`);
66
114
  }
115
+ if (search) {
116
+ sql += " AND tag LIKE ?";
117
+ params.push(`%${search}%`);
118
+ }
67
119
  sql +=
68
120
  " ORDER BY refreshed_at DESC NULLS LAST, score ASC, created_at ASC LIMIT ? OFFSET ?";
69
121
  params.push(limit, offset);
@@ -71,21 +123,29 @@ export function getTagsByStatus(
71
123
  return rows.map(parseTagRow);
72
124
  }
73
125
 
74
- export function getTagStats(country = null) {
126
+ export function getTagStats(country = null, search = null) {
75
127
  const db = getDb();
76
128
  if (!db) return null;
129
+ const conditions = [];
130
+ const params = [];
131
+ if (country) {
132
+ conditions.push("countries LIKE ?");
133
+ params.push(`%"${country}"%`);
134
+ }
135
+ if (search) {
136
+ conditions.push("tag LIKE ?");
137
+ params.push(`%${search}%`);
138
+ }
139
+ const whereClause =
140
+ conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
141
+
77
142
  let sql = `SELECT
78
143
  COUNT(*) as total,
79
144
  SUM(CASE WHEN status = 'productive' THEN 1 ELSE 0 END) as productive,
80
145
  SUM(CASE WHEN status = 'dead' THEN 1 ELSE 0 END) as dead,
81
146
  SUM(CASE WHEN status = 'new' THEN 1 ELSE 0 END) as newCount,
82
147
  SUM(CASE WHEN status = 'scoring' THEN 1 ELSE 0 END) as scoring
83
- FROM tags`;
84
- const params = [];
85
- if (country) {
86
- sql += " WHERE countries LIKE ?";
87
- params.push(`%"${country}"%`);
88
- }
148
+ FROM tags${whereClause}`;
89
149
  const row = db.prepare(sql).get(...params);
90
150
  // 获取所有出现过的国家
91
151
  const allRows = db.prepare("SELECT countries FROM tags").all();
@@ -276,15 +336,28 @@ export function reportTagScore(tag, fields) {
276
336
  }
277
337
  }
278
338
 
279
- export function getAllTags(limit = 200, offset = 0, country = null) {
339
+ export function getAllTags(
340
+ limit = 200,
341
+ offset = 0,
342
+ country = null,
343
+ search = null,
344
+ ) {
280
345
  const db = getDb();
281
346
  if (!db) return [];
282
347
  let sql = "SELECT * FROM tags";
283
348
  const params = [];
349
+ const conditions = [];
284
350
  if (country) {
285
- sql += " WHERE countries LIKE ?";
351
+ conditions.push("countries LIKE ?");
286
352
  params.push(`%"${country}"%`);
287
353
  }
354
+ if (search) {
355
+ conditions.push("tag LIKE ?");
356
+ params.push(`%${search}%`);
357
+ }
358
+ if (conditions.length > 0) {
359
+ sql += " WHERE " + conditions.join(" AND ");
360
+ }
288
361
  sql +=
289
362
  " ORDER BY refreshed_at DESC NULLS LAST, score DESC, created_at DESC LIMIT ? OFFSET ?";
290
363
  params.push(limit, offset);
@@ -371,6 +444,7 @@ export function insertDiscoverLog({
371
444
  country,
372
445
  round,
373
446
  strategy,
447
+ niche,
374
448
  tagsGenerated,
375
449
  tagsRaw,
376
450
  tagsAdded,
@@ -382,18 +456,26 @@ export function insertDiscoverLog({
382
456
  const db = getDb();
383
457
  if (!db) return { ok: false, error: "db not ready" };
384
458
 
459
+ // 确保 niche 列存在(兼容旧表)
460
+ try {
461
+ db.prepare("ALTER TABLE discover_log ADD COLUMN niche TEXT").run();
462
+ } catch {
463
+ // 列已存在则忽略
464
+ }
465
+
385
466
  try {
386
467
  const result = db
387
468
  .prepare(
388
469
  `INSERT INTO discover_log
389
- (country, round, strategy, tags_generated, tags_raw, tags_added,
470
+ (country, round, strategy, niche, tags_generated, tags_raw, tags_added,
390
471
  productive_sample, dead_sample, avg_productive_score, avg_dead_score)
391
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
472
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
392
473
  )
393
474
  .run(
394
475
  country,
395
476
  round,
396
477
  strategy || null,
478
+ niche || null,
397
479
  JSON.stringify(tagsGenerated || []),
398
480
  tagsRaw ? JSON.stringify(tagsRaw) : null,
399
481
  tagsAdded || 0,
@@ -714,6 +796,9 @@ export function getCountryHealth(country) {
714
796
  // 推荐的 niche 方向(基于现有 productive tag 的分类分析)
715
797
  const nicheRecommendation = recommendNiche(country, productive);
716
798
 
799
+ // 追踪 LLM 当前在攻击哪个 niche(取最近3轮 discover_log 的 niche 字段,众数)
800
+ const currentNiche = getCurrentNiche(country);
801
+
717
802
  return {
718
803
  country,
719
804
  total,
@@ -732,9 +817,43 @@ export function getCountryHealth(country) {
732
817
  healthStatus,
733
818
  issues,
734
819
  nicheRecommendation,
820
+ currentNiche,
735
821
  };
736
822
  }
737
823
 
824
+ /**
825
+ * 获取 LLM 当前正在攻击的 niche(最近3轮 discover_log 的 niche 众数)
826
+ */
827
+ function getCurrentNiche(country) {
828
+ const db = getDb();
829
+ if (!db) return null;
830
+
831
+ try {
832
+ const rows = db
833
+ .prepare(
834
+ `SELECT niche FROM discover_log
835
+ WHERE country = ? AND niche IS NOT NULL
836
+ ORDER BY round DESC
837
+ LIMIT 3`,
838
+ )
839
+ .all(country);
840
+
841
+ if (rows.length === 0) return null;
842
+
843
+ // 取出现次数最多的 niche
844
+ const counts = {};
845
+ for (const r of rows) {
846
+ const n = r.niche;
847
+ if (n) counts[n] = (counts[n] || 0) + 1;
848
+ }
849
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
850
+ return sorted.length > 0 ? sorted[0][0] : null;
851
+ } catch {
852
+ // niche 列尚不存在(旧表),返回 null
853
+ return null;
854
+ }
855
+ }
856
+
738
857
  /**
739
858
  * 基于现有 productive tag 推荐新的 niche 方向
740
859
  * 分析已有 tag 的类别分布,找出未覆盖或覆盖不足的方向
@@ -1121,13 +1240,68 @@ export function insertReflectionRules(rules) {
1121
1240
  return { ok: true, inserted: inserted.length };
1122
1241
  }
1123
1242
 
1243
+ /**
1244
+ * 计算两段规则文本的相似度(简单的词重叠率)
1245
+ * 返回 0-1,越高越相似
1246
+ */
1247
+ function ruleTextSimilarity(a, b) {
1248
+ const wordsA = new Set(
1249
+ a
1250
+ .toLowerCase()
1251
+ .split(/\s+/)
1252
+ .filter((w) => w.length > 2),
1253
+ );
1254
+ const wordsB = new Set(
1255
+ b
1256
+ .toLowerCase()
1257
+ .split(/\s+/)
1258
+ .filter((w) => w.length > 2),
1259
+ );
1260
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
1261
+ const intersection = [...wordsA].filter((w) => wordsB.has(w)).length;
1262
+ return intersection / Math.max(wordsA.size, wordsB.size);
1263
+ }
1264
+
1124
1265
  /**
1125
1266
  * 废弃旧规则(当新反思覆盖旧规则时)
1267
+ * 加入去重:如果新规则与旧规则语义相似(>70% 词重叠),跳过 supersede
1126
1268
  */
1127
- export function supersedeRules(country, reason = "new reflection") {
1269
+ export function supersedeRules(
1270
+ country,
1271
+ newRules = [],
1272
+ reason = "new reflection",
1273
+ ) {
1128
1274
  const db = getDb();
1129
1275
  if (!db) return { ok: false, error: "db not ready" };
1130
1276
 
1277
+ // 获取当前活跃规则
1278
+ const oldRules = db
1279
+ .prepare(
1280
+ "SELECT id, rule_text FROM reflection_rules WHERE country = ? AND is_active = 1",
1281
+ )
1282
+ .all(country);
1283
+
1284
+ // 规则去重:如果所有新规则与所有旧规则高度相似,跳过
1285
+ if (oldRules.length > 0 && newRules.length > 0) {
1286
+ let allSimilar = true;
1287
+ for (const nr of newRules) {
1288
+ const newText = typeof nr === "string" ? nr : nr.ruleText || "";
1289
+ const hasSimilar = oldRules.some(
1290
+ (or) => ruleTextSimilarity(newText, or.rule_text) > 0.7,
1291
+ );
1292
+ if (!hasSimilar) {
1293
+ allSimilar = false;
1294
+ break;
1295
+ }
1296
+ }
1297
+ if (allSimilar) {
1298
+ console.error(
1299
+ `[reflection] 跳过 supersede: ${newRules.length} 条新规则与 ${oldRules.length} 条旧规则高度相似`,
1300
+ );
1301
+ return { ok: true, superseded: 0, skipped: "rules too similar" };
1302
+ }
1303
+ }
1304
+
1131
1305
  const result = db
1132
1306
  .prepare(
1133
1307
  `UPDATE reflection_rules
@@ -1141,13 +1315,45 @@ export function supersedeRules(country, reason = "new reflection") {
1141
1315
 
1142
1316
  /**
1143
1317
  * 检查是否需要触发反思
1144
- * 条件:最近 N 轮 productive_cnt ≤ threshold
1318
+ * 条件:最近 N 轮 productive_cnt ≤ threshold,且距上次反思 ≥ cooldownRounds 轮
1145
1319
  * @returns {{ shouldReflect: boolean, reason: string, recentRounds: array }}
1146
1320
  */
1147
- export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1321
+ export function shouldTriggerReflection(
1322
+ country,
1323
+ lookback = 5,
1324
+ threshold = 1,
1325
+ cooldownRounds = 10,
1326
+ ) {
1148
1327
  const db = getDb();
1149
1328
  if (!db) return { shouldReflect: false, reason: "db not ready" };
1150
1329
 
1330
+ // 冷却期检查:距上次反思不足 cooldownRounds 轮则跳过
1331
+ const lastReflection = db
1332
+ .prepare(
1333
+ `SELECT MAX(rr.created_at) as lastReflectAt
1334
+ FROM reflection_rules rr
1335
+ WHERE rr.country = ? AND rr.is_active = 0 AND rr.superseded_at IS NOT NULL
1336
+ ORDER BY rr.superseded_at DESC
1337
+ LIMIT 1`,
1338
+ )
1339
+ .get(country);
1340
+
1341
+ if (lastReflection?.lastReflectAt) {
1342
+ const roundsSince = db
1343
+ .prepare(
1344
+ `SELECT COUNT(*) as cnt FROM discover_log
1345
+ WHERE country = ? AND created_at > ?`,
1346
+ )
1347
+ .get(country, lastReflection.lastReflectAt);
1348
+ if ((roundsSince?.cnt || 0) < cooldownRounds) {
1349
+ return {
1350
+ shouldReflect: false,
1351
+ reason: `冷却期: 距上次反思仅${roundsSince?.cnt || 0}轮,需≥${cooldownRounds}轮`,
1352
+ recentRounds: [],
1353
+ };
1354
+ }
1355
+ }
1356
+
1151
1357
  // 获取最近 lookback 轮的 discover_log
1152
1358
  const recentLogs = db
1153
1359
  .prepare(
@@ -1202,14 +1408,14 @@ export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1202
1408
  }
1203
1409
 
1204
1410
  const avgProductive = totalProductive / lookback;
1205
- // 触发条件:平均 productive ≤ threshold,或连续 ≥2 轮 productive ≤ 1
1411
+ // 触发条件:平均 productive ≤ threshold,或连续 ≥3 轮 productive ≤ 1
1206
1412
  const shouldReflect =
1207
- avgProductive <= threshold || consecutiveLowProductiveRounds >= 2;
1413
+ avgProductive <= threshold || consecutiveLowProductiveRounds >= 3;
1208
1414
 
1209
1415
  return {
1210
1416
  shouldReflect,
1211
1417
  reason: shouldReflect
1212
- ? consecutiveLowProductiveRounds >= 2
1418
+ ? consecutiveLowProductiveRounds >= 3
1213
1419
  ? `连续${consecutiveLowProductiveRounds}轮productive≤1,需要反思换方向`
1214
1420
  : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} ≤ ${threshold}`
1215
1421
  : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} > ${threshold}`,
@@ -1222,6 +1428,20 @@ export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1222
1428
  };
1223
1429
  }
1224
1430
 
1431
+ /**
1432
+ * 清理所有已废弃的反思规则(is_active=0)
1433
+ */
1434
+ export function cleanupInactiveReflectionRules() {
1435
+ const db = getDb();
1436
+ if (!db) return { ok: false, error: "db not ready" };
1437
+
1438
+ const result = db
1439
+ .prepare("DELETE FROM reflection_rules WHERE is_active = 0")
1440
+ .run();
1441
+ console.error(`[reflection] 🧹 清理了 ${result.changes} 条废弃规则`);
1442
+ return { ok: true, deleted: result.changes };
1443
+ }
1444
+
1225
1445
  /**
1226
1446
  * 获取反思所需的数据摘要(供 LLM 分析)
1227
1447
  */
@@ -0,0 +1,319 @@
1
+ /**
2
+ * LLM 配置持久化模块
3
+ *
4
+ * 运行时动态读写 LLM 配置(Provider、Model、URL、APIKey),
5
+ * 优先从 SQLite 读取,fallback 到环境变量。
6
+ * 通过 API 修改后立即生效,无需重启。
7
+ *
8
+ * 结构:Provider(服务商) → Models(模型列表)
9
+ * 选择 Provider 自动填入对应 URL,选择 Model 使用对应 model id。
10
+ */
11
+
12
+ import { getDb } from "./db-schema.js";
13
+
14
+ // ====== Provider + Model 定义 ======
15
+
16
+ export const LLM_PROVIDERS = [
17
+ {
18
+ id: "legacy",
19
+ name: "旧 LLM (zc-fast)",
20
+ url: "http://82.156.52.214:18000/v1/chat/completions",
21
+ models: [{ id: "zc-fast", label: "zc-fast" }],
22
+ },
23
+ {
24
+ id: "dashscope",
25
+ name: "阿里云 DashScope",
26
+ url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
27
+ models: [
28
+ { id: "auto", label: "🤖 自动(程序选择)" },
29
+ { id: "qwen3.7-max", label: "Qwen3.7-Max" },
30
+ { id: "qwen3.7-max-2026-06-08", label: "Qwen3.7-Max-2026-06-08" },
31
+ { id: "qwen3.7-max-2026-05-20", label: "Qwen3.7-Max-2026-05-20" },
32
+ { id: "qwen3.7-max-2026-05-17", label: "Qwen3.7-Max-2026-05-17" },
33
+ { id: "qwen3.7-max-preview", label: "Qwen3.7-Max-Preview" },
34
+ { id: "qwen3.7-plus", label: "Qwen3.7-Plus" },
35
+ { id: "qwen3.7-plus-2026-05-26", label: "Qwen3.7-Plus-2026-05-26" },
36
+ { id: "qwen3.6-plus", label: "Qwen3.6-Plus" },
37
+ { id: "qwen3.6-plus-2026-04-02", label: "Qwen3.6-Plus-2026-04-02" },
38
+ { id: "qwen3.6-max-preview", label: "Qwen3.6-Max-Preview" },
39
+ { id: "qwen3.6-flash", label: "Qwen3.6-Flash" },
40
+ { id: "qwen3.6-flash-2026-04-16", label: "Qwen3.6-Flash-2026-04-16" },
41
+ { id: "qwen3.6-35b-a3b", label: "Qwen3.6-35B-A3B" },
42
+ { id: "qwen3.6-27b", label: "Qwen3.6-27B" },
43
+ { id: "qwen3.5-plus", label: "Qwen3.5-Plus" },
44
+ { id: "qwen3.5-plus-2026-04-20", label: "Qwen3.5-Plus-2026-04-20" },
45
+ { id: "qwen3.5-ocr", label: "Qwen3.5-OCR" },
46
+ { id: "qwen-plus", label: "Qwen-Plus" },
47
+ { id: "qwen-turbo", label: "Qwen-Turbo(即将下线)" },
48
+ { id: "qwen-max", label: "Qwen-Max" },
49
+ { id: "deepseek-v4-flash", label: "DeepSeek-V4-Flash" },
50
+ { id: "deepseek-v4-pro", label: "DeepSeek-V4-Pro" },
51
+ { id: "glm-5.2", label: "GLM-5.2" },
52
+ { id: "kimi-k2.7-code", label: "Kimi-K2.7-Code" },
53
+ { id: "kimi-k2.6", label: "Kimi-K2.6" },
54
+ ],
55
+ },
56
+ ];
57
+
58
+ /** 根据 providerId 查找 provider */
59
+ export function findProvider(providerId) {
60
+ return LLM_PROVIDERS.find((p) => p.id === providerId) || LLM_PROVIDERS[0];
61
+ }
62
+
63
+ /** 根据 providerId + modelId 查找模型 */
64
+ export function findModel(providerId, modelId) {
65
+ const provider = findProvider(providerId);
66
+ return provider.models.find((m) => m.id === modelId) || provider.models[0];
67
+ }
68
+
69
+ // 兼容旧接口:扁平模型选项列表
70
+ export const LLM_MODEL_OPTIONS = LLM_PROVIDERS.flatMap((p) =>
71
+ p.models.map((m) => ({
72
+ id: m.id,
73
+ label: `${m.label} (${p.name})`,
74
+ url: p.url,
75
+ })),
76
+ );
77
+
78
+ // ====== 内存缓存 ======
79
+
80
+ let _cachedProvider = null;
81
+ let _cachedModel = null;
82
+ let _cachedUrl = null;
83
+ let _cachedApiKey = null;
84
+ let _loaded = false;
85
+
86
+ function ensureTable() {
87
+ const db = getDb();
88
+ if (!db) return;
89
+ db.prepare(
90
+ `CREATE TABLE IF NOT EXISTS _llm_config (
91
+ key TEXT PRIMARY KEY,
92
+ value TEXT NOT NULL
93
+ )`,
94
+ ).run();
95
+ }
96
+
97
+ function loadFromDb() {
98
+ const db = getDb();
99
+ if (!db) return {};
100
+ ensureTable();
101
+ try {
102
+ const rows = db.prepare("SELECT key, value FROM _llm_config").all();
103
+ const config = {};
104
+ for (const row of rows) {
105
+ config[row.key] = row.value;
106
+ }
107
+ return config;
108
+ } catch {
109
+ return {};
110
+ }
111
+ }
112
+
113
+ function saveToDb(key, value) {
114
+ const db = getDb();
115
+ if (!db) return;
116
+ ensureTable();
117
+ db.prepare(
118
+ `INSERT INTO _llm_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
119
+ ).run(key, String(value));
120
+ }
121
+
122
+ /**
123
+ * 加载配置(服务启动时调用一次)
124
+ * 优先级:SQLite > 环境变量 > 默认值
125
+ */
126
+ export function loadLlmConfig() {
127
+ if (_loaded) return;
128
+ const dbConfig = loadFromDb();
129
+
130
+ _cachedUrl = dbConfig.llm_url || process.env.LLM_URL || LLM_PROVIDERS[0].url;
131
+ _cachedModel =
132
+ dbConfig.llm_model ||
133
+ process.env.LLM_MODEL ||
134
+ LLM_PROVIDERS[0].models[0].id;
135
+ _cachedProvider = dbConfig.llm_provider || LLM_PROVIDERS[0].id;
136
+ // 按 provider 隔离读取 API Key:优先 llm_apikey_{provider},兼容旧 llm_apikey,最后 env
137
+ _cachedApiKey =
138
+ dbConfig[`llm_apikey_${_cachedProvider}`] ||
139
+ dbConfig.llm_apikey ||
140
+ process.env.APIKEY ||
141
+ "";
142
+ _loaded = true;
143
+
144
+ console.error(
145
+ `[llm-config] 加载完成: provider=${_cachedProvider}, model=${_cachedModel}, url=${_cachedUrl}, hasKey=${!!_cachedApiKey}`,
146
+ );
147
+ }
148
+
149
+ /**
150
+ * 获取当前 LLM 配置(供前端 API 使用)
151
+ */
152
+ export function getLlmConfig() {
153
+ if (!_loaded) loadLlmConfig();
154
+ return {
155
+ provider: _cachedProvider,
156
+ model: _cachedModel,
157
+ url: _cachedUrl,
158
+ hasApiKey: !!_cachedApiKey,
159
+ apiKeyHint: _cachedApiKey
160
+ ? _cachedApiKey.slice(0, 6) + "..." + _cachedApiKey.slice(-4)
161
+ : "",
162
+ };
163
+ }
164
+
165
+ /** 获取当前 LLM URL(内部调用) */
166
+ export function getLlmUrl() {
167
+ if (!_loaded) loadLlmConfig();
168
+ return _cachedUrl;
169
+ }
170
+
171
+ // Auto 模式的候选模型池缓存(从 DashScope provider 动态构建,排除 auto 自身)
172
+ let _autoPoolCache = null;
173
+
174
+ /** 从 DashScope provider 动态构建 auto 候选池(按优先级:flash/turbo > plus > 27b/35b > max/pro > 第三方) */
175
+ function buildAutoPool() {
176
+ if (_autoPoolCache) return _autoPoolCache;
177
+ const dashscope = LLM_PROVIDERS.find((p) => p.id === "dashscope");
178
+ if (!dashscope) return [];
179
+ const models = dashscope.models.filter((m) => m.id !== "auto");
180
+
181
+ const priority = (id) => {
182
+ if (/flash|turbo/i.test(id)) return 0;
183
+ if (/plus/i.test(id)) return 1;
184
+ if (/27b|35b/i.test(id)) return 2;
185
+ if (/max|pro/i.test(id)) return 3;
186
+ return 4; // deepseek, glm, kimi 等第三方
187
+ };
188
+
189
+ // 记录原始顺序,排序时优先按 capability tier,同 tier 保持原始顺序
190
+ const indexed = models.map((m, i) => ({ id: m.id, idx: i }));
191
+ indexed.sort((a, b) => {
192
+ const pa = priority(a.id);
193
+ const pb = priority(b.id);
194
+ if (pa !== pb) return pa - pb;
195
+ return a.idx - b.idx;
196
+ });
197
+
198
+ _autoPoolCache = indexed.map((m) => m.id);
199
+ console.error(
200
+ `[llm-config] auto 候选池已构建: ${_autoPoolCache.length} 个模型(${_autoPoolCache.slice(0, 5).join(", ")}...)`,
201
+ );
202
+ return _autoPoolCache;
203
+ }
204
+
205
+ /** 清除 auto 候选池缓存(provider 变更后调用) */
206
+ function clearAutoPoolCache() {
207
+ _autoPoolCache = null;
208
+ }
209
+
210
+ let _lastAutoSuccess = null; // 上次成功的模型,下次优先用它
211
+
212
+ /** 是否处于 auto 模式 */
213
+ export function isAutoMode() {
214
+ if (!_loaded) loadLlmConfig();
215
+ return _cachedModel === "auto";
216
+ }
217
+
218
+ /** 记录 auto 模式下成功调用的模型 */
219
+ export function recordAutoSuccess(model) {
220
+ _lastAutoSuccess = model;
221
+ }
222
+
223
+ /** 获取 auto 模式候选池(上次成功的排最前,失败时依次尝试) */
224
+ export function getAutoPool() {
225
+ const pool = [...buildAutoPool()];
226
+ if (_lastAutoSuccess) {
227
+ const idx = pool.indexOf(_lastAutoSuccess);
228
+ if (idx > 0) {
229
+ // 把上次成功的移到第一位,其余保持相对顺序
230
+ pool.splice(idx, 1);
231
+ pool.unshift(_lastAutoSuccess);
232
+ }
233
+ }
234
+ return pool;
235
+ }
236
+
237
+ /** 解析当前实际使用的 model(auto 模式下轮询选一个) */
238
+ function resolveModel() {
239
+ if (_cachedModel !== "auto") return _cachedModel;
240
+ const pool = buildAutoPool();
241
+ const idx = _autoRoundIdx % pool.length;
242
+ _autoRoundIdx++;
243
+ return pool[idx];
244
+ }
245
+
246
+ /** 获取当前 LLM Model(内部调用,auto 模式自动轮询) */
247
+ export function getLlmModel() {
248
+ if (!_loaded) loadLlmConfig();
249
+ return resolveModel();
250
+ }
251
+
252
+ /** 获取当前 API Key(内部调用) */
253
+ export function getLlmApiKey() {
254
+ if (!_loaded) loadLlmConfig();
255
+ return _cachedApiKey;
256
+ }
257
+
258
+ /**
259
+ * 更新 LLM 配置(通过 API 调用)
260
+ * @param {{ provider?: string, model?: string, url?: string, apiKey?: string }} updates
261
+ */
262
+ export function updateLlmConfig(updates = {}) {
263
+ if (!_loaded) loadLlmConfig();
264
+
265
+ // Provider 变更:切换 URL,Model 用指定的或默认第一个,同时加载对应 provider 的 API Key
266
+ if (updates.provider !== undefined) {
267
+ const provider = findProvider(updates.provider);
268
+ const prevProvider = _cachedProvider;
269
+ _cachedProvider = provider.id;
270
+ _cachedUrl = provider.url;
271
+ clearAutoPoolCache(); // provider 变了,auto 候选池也需重建
272
+ // 如果同时传了 model 且在 provider 中存在,用指定的;否则用第一个
273
+ const specifiedModel =
274
+ updates.model !== undefined
275
+ ? provider.models.find((m) => m.id === updates.model)
276
+ : null;
277
+ _cachedModel = specifiedModel ? specifiedModel.id : provider.models[0].id;
278
+ // 切换到新 provider:从 DB 加载该 provider 的 API Key
279
+ if (provider.id !== prevProvider) {
280
+ const dbConfig = loadFromDb();
281
+ _cachedApiKey =
282
+ dbConfig[`llm_apikey_${provider.id}`] ||
283
+ dbConfig.llm_apikey ||
284
+ process.env.APIKEY ||
285
+ "";
286
+ console.error(
287
+ `[llm-config] 切换到 ${provider.id},API Key: ${_cachedApiKey ? "已加载" : "未设置"}`,
288
+ );
289
+ }
290
+ saveToDb("llm_provider", provider.id);
291
+ saveToDb("llm_url", provider.url);
292
+ saveToDb("llm_model", _cachedModel);
293
+ } else if (updates.model !== undefined) {
294
+ // Model 单独变更(同 provider 内切换)
295
+ _cachedModel = updates.model;
296
+ saveToDb("llm_model", updates.model);
297
+ }
298
+
299
+ // URL 手动覆盖
300
+ if (updates.url !== undefined) {
301
+ _cachedUrl = updates.url;
302
+ saveToDb("llm_url", updates.url);
303
+ }
304
+
305
+ // API Key(按 provider 隔离存储,跳过提示字符串)
306
+ if (updates.apiKey !== undefined && updates.apiKey !== "") {
307
+ const isHint = updates.apiKey.includes("...") && updates.apiKey.length < 20;
308
+ if (!isHint) {
309
+ _cachedApiKey = updates.apiKey;
310
+ saveToDb(`llm_apikey_${_cachedProvider}`, updates.apiKey);
311
+ }
312
+ }
313
+
314
+ console.error(
315
+ `[llm-config] 配置已更新: provider=${_cachedProvider}, model=${_cachedModel}, url=${_cachedUrl}, hasKey=${!!_cachedApiKey}`,
316
+ );
317
+
318
+ return getLlmConfig();
319
+ }