tt-help-cli-ycl 1.4.2 → 1.4.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.
@@ -2,6 +2,7 @@
2
2
  * Tag 发现与打分服务
3
3
  * - LLM 调用与 prompt 组装
4
4
  * - 单国家标签发现(discoverTagsForCountry)
5
+ * - 自闭环:健康检查 → 数据清理 → niche 切换 → 生成 → 记录
5
6
  * - 标签打分(scoreTag / scoreTagBatch)
6
7
  */
7
8
 
@@ -31,7 +32,7 @@ export function getLang(country) {
31
32
 
32
33
  // ====== LLM 调用 ======
33
34
 
34
- export async function callLLM(prompt) {
35
+ export async function callLLM(prompt, temperature = 0.7) {
35
36
  const apiKey = process.env.APIKEY || "";
36
37
  const { fetch } = await import("undici");
37
38
 
@@ -45,7 +46,7 @@ export async function callLLM(prompt) {
45
46
  model: LLM_MODEL,
46
47
  messages: [{ role: "user", content: prompt }],
47
48
  max_tokens: 1024,
48
- temperature: 0.7,
49
+ temperature,
49
50
  }),
50
51
  });
51
52
 
@@ -86,28 +87,30 @@ export function parseTagsFromResponse(content) {
86
87
  */
87
88
  export function parseDiscoverResponse(content) {
88
89
  let strategy = null;
90
+ let niche = null;
89
91
  let tags = [];
90
92
 
91
93
  try {
92
94
  const parsed = JSON.parse(content);
93
95
 
94
- // 新格式: { strategy, tags }
96
+ // 新格式: { strategy, niche, tags }
95
97
  if (parsed.strategy && Array.isArray(parsed.tags)) {
96
98
  strategy = parsed.strategy;
99
+ niche = parsed.niche || null;
97
100
  tags = parsed.tags.map(normalizeTag).filter((t) => t && t.length >= 2);
98
- return { strategy, tags };
101
+ return { strategy, niche, tags };
99
102
  }
100
103
 
101
104
  // 旧格式: 纯数组
102
105
  if (Array.isArray(parsed)) {
103
106
  tags = parsed.map(normalizeTag).filter((t) => t && t.length >= 2);
104
- return { strategy, tags };
107
+ return { strategy, niche, tags };
105
108
  }
106
109
 
107
110
  // 旧格式: { tags: [...] }
108
111
  if (Array.isArray(parsed.tags)) {
109
112
  tags = parsed.tags.map(normalizeTag).filter((t) => t && t.length >= 2);
110
- return { strategy, tags };
113
+ return { strategy, niche, tags };
111
114
  }
112
115
  } catch {
113
116
  // JSON 解析失败,尝试从文本中提取
@@ -116,6 +119,10 @@ export function parseDiscoverResponse(content) {
116
119
  if (strategyMatch) {
117
120
  strategy = strategyMatch[1];
118
121
  }
122
+ const nicheMatch = content.match(/"niche"\s*:\s*"([^"]+)"/);
123
+ if (nicheMatch) {
124
+ niche = nicheMatch[1];
125
+ }
119
126
  }
120
127
 
121
128
  // 降级:从文本行中提取 tags
@@ -127,7 +134,7 @@ export function parseDiscoverResponse(content) {
127
134
  }
128
135
  }
129
136
 
130
- return { strategy, tags };
137
+ return { strategy, niche, tags };
131
138
  }
132
139
 
133
140
  // ====== Prompt 组装 ======
@@ -138,6 +145,9 @@ export function buildDiscoverPrompt(
138
145
  history,
139
146
  userPrompt,
140
147
  pastStrategies,
148
+ health,
149
+ nicheRecommendation,
150
+ hardRules = [],
141
151
  ) {
142
152
  const lang = getLang(country);
143
153
  const langNames = {
@@ -166,13 +176,36 @@ export function buildDiscoverPrompt(
166
176
  )}. Use these as reference for the STYLE and TYPE of tag that works — prefer commonly used words like these.`
167
177
  : "";
168
178
 
169
- // 负样本:该国 dead tag
179
+ // 负样本:该国 dead tag(只传最近50个,避免 prompt 过长)
170
180
  const dead = history.dead || [];
181
+ const recentDead = dead.slice(0, 50);
171
182
  const deadHint =
172
- dead.length > 0
173
- ? `\nTags that failed for ${country} (found no matching users): ${dead.map((t) => t.tag).join(", ")}. Avoid these and similar patterns.`
183
+ recentDead.length > 0
184
+ ? `\nTags that RECENTLY failed for ${country} (found no matching users): ${recentDead.map((t) => t.tag).join(", ")}. Avoid these EXACT tags and similar patterns.`
185
+ : "";
186
+
187
+ // 高频重复 dead 标签警告(这些是反复生成又反复失败的)
188
+ const deadTagNames = new Set(dead.map((t) => t.tag));
189
+ const recentGenerated = (history.recentGenerated || []).filter((t) =>
190
+ deadTagNames.has(t),
191
+ );
192
+ const repeatedDeadHint =
193
+ recentGenerated.length > 0
194
+ ? `\n⚠️ These tags have been generated MULTIPLE TIMES and always fail — NEVER generate them again: ${recentGenerated.join(", ")}`
174
195
  : "";
175
196
 
197
+ // 语言模式分析:统计 dead 标签的语言分布
198
+ const langPatternHint = (() => {
199
+ if (dead.length < 20) return "";
200
+ const englishDead = dead.filter((t) => /^[a-zA-Z]+$/.test(t.tag)).length;
201
+ const localDead = dead.filter((t) => !/^[a-zA-Z]+$/.test(t.tag)).length;
202
+ const englishRatio = Math.round((englishDead / dead.length) * 100);
203
+ if (englishRatio > 60 && lang !== "en") {
204
+ return `\n⚠️ LANGUAGE WARNING: ${englishRatio}% of dead tags for ${country} are English-only words — they DON'T work in a ${langName}-speaking country. You MUST generate tags in ${langName} or bilingual (like greekshop, ελληνικαπροιοντα).`;
205
+ }
206
+ return "";
207
+ })();
208
+
176
209
  // 死因分析
177
210
  const errorPatterns = [
178
211
  ...new Set(dead.filter((t) => t.last_error).map((t) => t.last_error)),
@@ -216,6 +249,45 @@ Use this analysis to decide your strategy for this round.`;
216
249
  ? `\n⚠️ WARNING: Currently ${deadRatio}% of our generated tags fail (no real TikTok posts). This is critically high. You MUST be more conservative — only suggest hashtags you have HIGH CONFIDENCE actually exist on TikTok.`
217
250
  : "";
218
251
 
252
+ // ====== 自闭环:健康指标 & niche 推荐 ======
253
+ let healthHint = "";
254
+ let nicheHint = "";
255
+
256
+ if (health && health.healthStatus !== "healthy") {
257
+ const issuesText = health.issues.map((i) => ` - ${i}`).join("\n");
258
+ healthHint = `
259
+ === 🏥 Country Health Analysis (${health.healthStatus}) ===
260
+ Country: ${health.country}
261
+ Total tags: ${health.total} | Productive: ${health.productive} (${health.productiveRatio}%) | Dead: ${health.dead} (${health.deadRatio}%) | Scored: ${health.scored}
262
+ Recent effectiveness: productive ${health.recentProductiveRatio}% | dead ${health.recentDeadRatio}% | empty rounds ${health.recentEmptyRounds}
263
+ Saturation: ${health.saturation}%
264
+
265
+ Issues detected:
266
+ ${issuesText}
267
+
268
+ ⚠️ Based on this health analysis, you MUST adjust your strategy:
269
+ ${health.healthStatus === "critical" ? "- Be EXTREMELY conservative — only suggest the most common, well-established hashtags\n- Focus on niches that have proven to work (see productive tags above)\n- Avoid experimental or creative combinations" : "- Be more conservative than usual — prioritize proven tag patterns\n- Try different niche directions that haven't been explored yet"}
270
+ `;
271
+ }
272
+
273
+ if (nicheRecommendation) {
274
+ const uncoveredText = nicheRecommendation.uncovered
275
+ .slice(0, 5)
276
+ .map((n) => `${n.niche} (current: ${n.currentCoverage} tags)`)
277
+ .join(", ");
278
+ const saturatedText = nicheRecommendation.saturated
279
+ .map((n) => `${n.niche} (${n.currentCoverage} tags)`)
280
+ .join(", ");
281
+
282
+ nicheHint = `
283
+ === 🎯 Niche Direction Recommendation ===
284
+ Uncovered niches (EXPLORE these — few existing productive tags): ${uncoveredText || "none remaining"}
285
+ Saturated niches (AVOID these — already well-covered): ${saturatedText || "none saturated"}
286
+
287
+ ${nicheRecommendation.uncovered.length > 0 ? `Focus your tags on the UNCOVERED niches above. Generate hashtags related to those product/service categories in ${langName}.` : "All major niches are covered. Try hyper-local or seasonal trends, or very specific sub-categories."}
288
+ `;
289
+ }
290
+
219
291
  return `You are discovering TikTok hashtags used by people who sell things in ${country}.
220
292
 
221
293
  ⛔ CRITICAL RULE: ONLY suggest hashtags that you are CONFIDENT actually exist on TikTok with real posts.
@@ -237,7 +309,19 @@ Think about the MOST COMMON hashtags that real sellers in ${country} use on TikT
237
309
  - Common seller identities (shop, boutique, small business, creator...)
238
310
  - Well-known community tags (support local, marketplace, second round...)
239
311
 
240
- ${qualityWarning}${productiveHint}${deadHint}${errorHint}${userHint}${strategyReview}
312
+ ${qualityWarning}
313
+
314
+ ${
315
+ hardRules.length > 0
316
+ ? `
317
+ === ⛔ HARD RULES (from self-reflection — MUST follow these) ===
318
+ These rules were generated by analyzing past failures and successes. Violating them will produce dead tags.
319
+ ${hardRules.map((r) => ` ${r.ruleType === "hard" ? "⛔" : "💡"} ${r.ruleText}`).join("\n")}
320
+ `
321
+ : ""
322
+ }
323
+
324
+ ${productiveHint}${deadHint}${repeatedDeadHint}${langPatternHint}${errorHint}${userHint}${strategyReview}${healthHint}${nicheHint}
241
325
 
242
326
  ## Quality check before responding
243
327
 
@@ -245,9 +329,10 @@ For each tag you consider, ask yourself: "Have I actually seen this hashtag used
245
329
 
246
330
  ## Output format
247
331
 
248
- Return ONLY a JSON object with two fields:
332
+ Return ONLY a JSON object with three fields:
249
333
  {
250
- "strategy": "Your analysis (2-3 sentences). Acknowledge what worked/failed before and explain why your chosen tags are likely real and commonly used.",
334
+ "strategy": "Your analysis (2-3 sentences). Acknowledge what worked/failed before, explain why your chosen tags are likely real and commonly used, and state which niche direction you're targeting.",
335
+ "niche": "The primary niche category you focused on (e.g. 'fashion', 'food', 'handmade_crafts', 'electronics_tech', 'pets_animals', 'services_professional', 'marketplace_local')",
251
336
  "tags": ["tag1", "tag2", "tag3", "tag4", "..."]
252
337
  }
253
338
 
@@ -257,6 +342,7 @@ Rules:
257
342
  - QUALITY over creativity — 3 real, commonly used tags beat 4 invented ones
258
343
  - NEVER invent compound words or translate concepts into hashtags
259
344
  - Prefer simpler, more common tags over hyper-specific ones
345
+ - If health analysis shows saturation in certain niches, EXPLICITLY pivot to uncovered niches
260
346
  - Look for tags with broad appeal (many potential posters)`;
261
347
  }
262
348
 
@@ -272,35 +358,140 @@ export async function discoverTagsForCountry(
272
358
  return { country, error: `不支持的国家代码: ${country}` };
273
359
  }
274
360
 
361
+ // ====== 自闭环 Step 1: 健康检查 & 数据清理 ======
362
+ const {
363
+ getDiscoverRound,
364
+ getDiscoverEffectiveness,
365
+ getCountryHealth,
366
+ cleanupLowScoredTags,
367
+ cleanupStaleScoringTags,
368
+ cleanupErrorTags,
369
+ getActiveRules,
370
+ } = await import("./db-tags.js");
371
+
372
+ const health = getCountryHealth(country);
373
+ console.error(
374
+ `[tag-service] 🏥 ${country} health: ${health?.healthStatus || "unknown"} | deadRatio=${health?.deadRatio || "?"}% | productiveRatio=${health?.productiveRatio || "?"}% | saturation=${health?.saturation || "?"}%`,
375
+ );
376
+
377
+ // 根据健康状态执行数据清理
378
+ const cleanupActions = {};
379
+ if (health) {
380
+ // 清理超时 scoring tag(始终执行)
381
+ const staleResult = cleanupStaleScoringTags();
382
+ cleanupActions.staleScoringReset = staleResult.reset;
383
+
384
+ // 清理 error tag(始终执行)
385
+ const errorResult = cleanupErrorTags();
386
+ cleanupActions.errorReset = errorResult.reset;
387
+
388
+ // 低分 scored tag 清理:当 lowScored > 2x productive 时执行
389
+ if (health.lowScored > health.productive * 2) {
390
+ const lowResult = cleanupLowScoredTags(20);
391
+ cleanupActions.lowScoredCleanup = lowResult.cleaned;
392
+ }
393
+ }
394
+
395
+ // ====== 自闭环 Step 2: 自动 niche 切换 ======
396
+ let autoNichePrompt = null;
397
+ let nicheRecommendation = null;
398
+
399
+ if (
400
+ health &&
401
+ health.healthStatus !== "healthy" &&
402
+ health.nicheRecommendation
403
+ ) {
404
+ nicheRecommendation = health.nicheRecommendation;
405
+
406
+ // 当健康状态为 warning/critical 且有未覆盖 niche 时,自动切换
407
+ if (nicheRecommendation.uncovered.length > 0) {
408
+ const targetNiche = nicheRecommendation.uncovered[0];
409
+ const nicheDescriptions = {
410
+ fashion:
411
+ "clothing, shoes, accessories, vintage fashion, secondhand clothes",
412
+ food: "bakery, food delivery, local food, restaurant, cooking supplies",
413
+ handmade_crafts:
414
+ "handmade items, DIY crafts, ceramics, candles, resin art",
415
+ home_living:
416
+ "home decoration, furniture, interior design, cleaning services",
417
+ electronics_tech:
418
+ "electronics, gadgets, phones, computers, tech accessories",
419
+ pets_animals: "pet supplies, dog accessories, cat food, animal care",
420
+ beauty_health:
421
+ "beauty products, cosmetics, skincare, hair care, wellness",
422
+ books_collectibles:
423
+ "books, collectibles, vinyl records, trading cards, antiques",
424
+ services_professional:
425
+ "professional services, repair, cleaning, photography, florist",
426
+ marketplace_local:
427
+ "local market, marketplace, shop, boutique, online store",
428
+ };
429
+ autoNichePrompt = `Focus on the "${targetNiche.niche}" niche: ${nicheDescriptions[targetNiche.niche] || targetNiche.niche}. We have only ${targetNiche.currentCoverage} productive tags in this category — explore more hashtags in this direction.`;
430
+ console.error(
431
+ `[tag-service] 🎯 ${country} auto niche switch → ${targetNiche.niche} (coverage: ${targetNiche.currentCoverage})`,
432
+ );
433
+ }
434
+ }
435
+
436
+ // 合并用户 prompt 和自动 niche prompt
437
+ const effectiveUserPrompt = autoNichePrompt || userPrompt;
438
+
439
+ // ====== 自闭环 Step 3: 动态温度调整 ======
440
+ let temperature = 0.7; // 默认
441
+ if (health) {
442
+ if (health.healthStatus === "critical") {
443
+ temperature = 0.3; // 极度保守
444
+ } else if (health.healthStatus === "warning") {
445
+ temperature = 0.5; // 较保守
446
+ }
447
+ }
448
+
449
+ // ====== 原有 discover 流程 ======
275
450
  // 读取历史打分记录
276
451
  const productive = store.getTagsByCountry(country, 50);
277
452
  const dead = store.getDeadTags(country);
278
453
  // 获取该国所有已存在的 tag 名(应用层去重用)
279
454
  const allTags = store.getTagsByCountry(country, 0);
280
455
  const allExisting = new Set(allTags.map((t) => t.tag));
281
- const history = { productive, dead };
456
+
457
+ // 获取最近生成的标签(用于检测重复生成)
458
+ const { getRecentGeneratedTags } = await import("./db-tags.js");
459
+ const recentGenerated = getRecentGeneratedTags(country, 10);
460
+
461
+ const history = { productive, dead, recentGenerated };
282
462
 
283
463
  // 读取历史策略效果(自学习)
284
- const { getDiscoverRound, getDiscoverEffectiveness } =
285
- await import("./db-tags.js");
286
464
  const round = getDiscoverRound(country);
287
465
  const pastStrategies = getDiscoverEffectiveness(country);
288
466
 
289
- // 组装 prompt 并调用 LLM(两阶段:先思考策略,再生成 tag)
467
+ // ====== 自闭环 Step 4: 注入硬规则(反思产出) ======
468
+ const hardRules = getActiveRules(country);
469
+ if (hardRules.length > 0) {
470
+ console.error(
471
+ `[tag-service] ⛔ ${country} 注入 ${hardRules.length} 条硬规则到 prompt`,
472
+ );
473
+ }
474
+
475
+ // 组装 prompt(加入健康指标、niche 推荐、硬规则)
290
476
  const prompt = buildDiscoverPrompt(
291
477
  country,
292
478
  count,
293
479
  history,
294
- userPrompt,
480
+ effectiveUserPrompt,
295
481
  pastStrategies,
482
+ health,
483
+ nicheRecommendation,
484
+ hardRules,
296
485
  );
297
486
  console.error(
298
- `[tag-service] LLM discover ${country} round=${round} (lang=${getLang(country)}, count=${count})`,
487
+ `[tag-service] LLM discover ${country} round=${round} (lang=${getLang(country)}, count=${count}, temp=${temperature}, health=${health?.healthStatus || "unknown"})`,
299
488
  );
300
- const content = await callLLM(prompt);
489
+ const content = await callLLM(prompt, temperature);
301
490
 
302
- // 解析响应:提取 strategy 和 tags,筛选掉数据库中已存在的
303
- const { strategy, tags } = parseDiscoverResponse(content);
491
+ // 解析响应:提取 strategy, niche, tags
492
+ const parsed = parseDiscoverResponse(content);
493
+ const { strategy, tags } = parsed;
494
+ const niche = parsed.niche || null;
304
495
  const unique = [...new Set(tags)]
305
496
  .filter((t) => !allExisting.has(t))
306
497
  .slice(0, count);
@@ -308,6 +499,9 @@ export async function discoverTagsForCountry(
308
499
  console.error(
309
500
  `[tag-service] LLM strategy: ${strategy?.substring(0, 150) || "(none)"}`,
310
501
  );
502
+ if (niche) {
503
+ console.error(`[tag-service] LLM niche focus: ${niche}`);
504
+ }
311
505
  console.error(
312
506
  `[tag-service] LLM generated ${unique.length} tags: ${unique.join(", ")}`,
313
507
  );
@@ -319,7 +513,7 @@ export async function discoverTagsForCountry(
319
513
  if (result.inserted) inserted.push(result.tag);
320
514
  }
321
515
 
322
- // 记录 discover log(策略 + 结果)
516
+ // 记录 discover log(策略 + 结果 + 健康状态 + niche)
323
517
  const { insertDiscoverLog } = await import("./db-tags.js");
324
518
  insertDiscoverLog({
325
519
  country,
@@ -337,13 +531,41 @@ export async function discoverTagsForCountry(
337
531
  dead.length > 0 ? dead.reduce((s, t) => s + t.score, 0) / dead.length : 0,
338
532
  });
339
533
 
534
+ // ====== 自闭环 Step 5: 反思触发(本轮结束后检查是否需要反思) ======
535
+ let reflectionResult = null;
536
+ try {
537
+ reflectionResult = await reflectAndLearn(store, country);
538
+ if (reflectionResult.reflected) {
539
+ console.error(
540
+ `[tag-service] 🔄 ${country} 反思完成: 新增 ${reflectionResult.rulesAdded} 条规则, 分析: ${reflectionResult.analysis?.substring(0, 100)}`,
541
+ );
542
+ }
543
+ } catch (e) {
544
+ console.error(`[tag-service] 反思异常: ${e.message}`);
545
+ reflectionResult = { reflected: false, error: e.message };
546
+ }
547
+
340
548
  return {
341
549
  country,
342
550
  round,
343
551
  strategy,
552
+ niche,
344
553
  added: inserted.length,
345
554
  tags: inserted,
346
555
  total: unique.length,
556
+ // 自闭环信息
557
+ health: {
558
+ status: health?.healthStatus || "unknown",
559
+ deadRatio: health?.deadRatio || "?",
560
+ productiveRatio: health?.productiveRatio || "?",
561
+ saturation: health?.saturation || "?",
562
+ issues: health?.issues || [],
563
+ },
564
+ cleanupActions,
565
+ autoNiche: autoNichePrompt ? nicheRecommendation.uncovered[0].niche : null,
566
+ temperature,
567
+ hardRulesInjected: hardRules.length,
568
+ reflection: reflectionResult,
347
569
  };
348
570
  }
349
571
 
@@ -503,3 +725,267 @@ export async function scoreTag(store, { tag, targetCountries, serverUrl }) {
503
725
 
504
726
  return result;
505
727
  }
728
+
729
+ // ====== 反思闭环:自我学习与改进 ======
730
+
731
+ /**
732
+ * 组装反思 prompt
733
+ * 输入:最近轮次数据、成功/失败模式、当前规则、健康指标
734
+ * 输出期望:JSON 格式的规则列表
735
+ */
736
+ export function buildReflectionPrompt(data) {
737
+ const {
738
+ country,
739
+ langName,
740
+ recentLogs,
741
+ productiveTop,
742
+ deadFrequency,
743
+ errorStats,
744
+ activeRules,
745
+ health,
746
+ } = data;
747
+
748
+ // 最近轮次效果摘要
749
+ const roundsSummary = recentLogs
750
+ .slice(0, 10)
751
+ .map((log) => {
752
+ const tagsGen = JSON.parse(log.tags_generated || "[]");
753
+ return `Round ${log.round}: strategy="${log.strategy?.substring(0, 100) || "(none)"}", generated ${tagsGen.length} tags: ${tagsGen.slice(0, 8).join(", ")}${tagsGen.length > 8 ? "..." : ""}`;
754
+ })
755
+ .join("\n");
756
+
757
+ // 成功模式:productive 标签的特征
758
+ const successPatterns = productiveTop
759
+ .slice(0, 20)
760
+ .map(
761
+ (t) =>
762
+ `${t.tag} (score=${t.score}, authors=${t.matched_authors}, pushed=${t.pushed_users})`,
763
+ )
764
+ .join(", ");
765
+
766
+ // 失败模式:dead 标签的特征
767
+ const failurePatterns = deadFrequency
768
+ .slice(0, 30)
769
+ .map((t) => `${t.tag} (error: ${t.last_error || "no_match"})`)
770
+ .join(", ");
771
+
772
+ // Error 类型统计
773
+ const errorSummary = errorStats
774
+ .map((e) => `${e.error_type}: ${e.count} tags`)
775
+ .join(", ");
776
+
777
+ // 当前活跃规则
778
+ const currentRules =
779
+ activeRules.length > 0
780
+ ? activeRules.map((r) => ` - ${r.rule_text}`).join("\n")
781
+ : " (none yet)";
782
+
783
+ // 健康指标
784
+ const healthInfo = health
785
+ ? `Health: ${health.healthStatus} | deadRatio=${health.deadRatio}% | productiveRatio=${health.productiveRatio}% | saturation=${health.saturation}%`
786
+ : "Health: unknown";
787
+
788
+ return `You are a self-reflection agent analyzing why TikTok hashtag discovery has been failing for ${country} (${langName}).
789
+
790
+ Your goal: identify FAILURE PATTERNS and SUCCESS PATTERNS, then produce HARD RULES that will guide future tag generation to be more effective.
791
+
792
+ ## Recent discovery rounds (all failing)
793
+ ${roundsSummary}
794
+
795
+ ## Success patterns (productive tags that actually work)
796
+ ${successPatterns}
797
+
798
+ ## Failure patterns (dead tags that don't work)
799
+ ${failurePatterns}
800
+
801
+ ## Error type distribution
802
+ ${errorSummary}
803
+
804
+ ## Current health status
805
+ ${healthInfo}
806
+
807
+ ## Current hard rules already in effect
808
+ ${currentRules}
809
+
810
+ ## Analysis task
811
+
812
+ Analyze the data above and answer:
813
+
814
+ 1. **What patterns do productive tags share?** (language, length, word type, structure)
815
+ 2. **What patterns do dead tags share?** (language, length, word type, structure)
816
+ 3. **What specific tags should NEVER be generated again?** (list the most frequently repeated dead tags)
817
+ 4. **What language strategy should be used?** (e.g. "only use ${langName} words", "avoid English-only tags")
818
+ 5. **What word structure works?** (e.g. "single industry words", "city+category combos", "avoid compound words")
819
+
820
+ ## Output format
821
+
822
+ Return ONLY a JSON object:
823
+ {
824
+ "analysis_summary": "2-3 sentence summary of what you learned",
825
+ "success_pattern": "description of what productive tags look like",
826
+ "failure_pattern": "description of what dead tags look like",
827
+ "rules": [
828
+ {
829
+ "rule_type": "hard",
830
+ "rule_text": "NEVER use English-only tags for ${country} — they have 0% match rate",
831
+ "trigger_reason": "dead_ratio > 60% with English tags"
832
+ },
833
+ {
834
+ "rule_type": "hard",
835
+ "rule_text": "NEVER generate these specific tags: hautpflege, bodycare, serum, creme, wellness",
836
+ "trigger_reason": "these tags confirmed dead 5+ times"
837
+ },
838
+ {
839
+ "rule_type": "soft",
840
+ "rule_text": "Prefer ${langName} industry/profession words (like handwerker, schreinerei pattern)",
841
+ "trigger_reason": "productive tags are mostly local-language industry words"
842
+ }
843
+ ]
844
+ }
845
+
846
+ Be specific and actionable. Each rule should be something that can be directly applied to future tag generation prompts.`;
847
+ }
848
+
849
+ /**
850
+ * 解析反思 LLM 的响应
851
+ */
852
+ export function parseReflectionResponse(content) {
853
+ try {
854
+ const parsed = JSON.parse(content);
855
+ return {
856
+ analysisSummary: parsed.analysis_summary || "",
857
+ successPattern: parsed.success_pattern || "",
858
+ failurePattern: parsed.failure_pattern || "",
859
+ rules: (parsed.rules || [])
860
+ .map((r) => ({
861
+ ruleType: r.rule_type || "hard",
862
+ ruleText: r.rule_text || "",
863
+ triggerReason: r.trigger_reason || "",
864
+ }))
865
+ .filter((r) => r.ruleText.length > 10),
866
+ };
867
+ } catch {
868
+ // JSON 解析失败,尝试从文本提取
869
+ const rules = [];
870
+ const lines = content.split("\n");
871
+ for (const line of lines) {
872
+ const match = line.match(/"rule_text"\s*:\s*"([^"]+)"/);
873
+ if (match && match[1].length > 10) {
874
+ rules.push({
875
+ ruleType: line.includes("soft") ? "soft" : "hard",
876
+ ruleText: match[1],
877
+ triggerReason: "",
878
+ });
879
+ }
880
+ }
881
+ return {
882
+ analysisSummary: content.substring(0, 200),
883
+ successPattern: "",
884
+ failurePattern: "",
885
+ rules,
886
+ };
887
+ }
888
+ }
889
+
890
+ /**
891
+ * 执行一次反思:分析数据 → 调用 LLM → 写入规则
892
+ * @param {object} store - data-store 实例
893
+ * @param {string} country - 国家代码
894
+ * @returns {{ reflected: boolean, rulesAdded: number, analysis: string }}
895
+ */
896
+ export async function reflectAndLearn(store, country) {
897
+ if (!COUNTRY_LANG[country]) {
898
+ return { reflected: false, error: `不支持的国家代码: ${country}` };
899
+ }
900
+
901
+ const {
902
+ shouldTriggerReflection,
903
+ getReflectionData,
904
+ insertReflectionRules,
905
+ supersedeRules,
906
+ getActiveRules,
907
+ } = await import("./db-tags.js");
908
+
909
+ // Step 1: 检查是否需要反思
910
+ const triggerCheck = shouldTriggerReflection(country);
911
+ if (!triggerCheck.shouldReflect) {
912
+ console.error(`[reflection] ${country} 不需要反思: ${triggerCheck.reason}`);
913
+ return { reflected: false, reason: triggerCheck.reason };
914
+ }
915
+
916
+ console.error(`[reflection] 🔄 ${country} 触发反思: ${triggerCheck.reason}`);
917
+
918
+ // Step 2: 收集反思数据
919
+ const data = getReflectionData(country);
920
+ if (!data) {
921
+ return { reflected: false, error: "无法获取反思数据" };
922
+ }
923
+
924
+ // Step 3: 组装反思 prompt
925
+ const prompt = buildReflectionPrompt(data);
926
+
927
+ // Step 4: 调用 LLM 进行反思(温度较低,确保分析质量)
928
+ console.error(`[reflection] 🧠 ${country} 正在调用 LLM 进行反思分析...`);
929
+ const content = await callLLM(prompt, 0.3);
930
+
931
+ // Step 5: 解析反思结果
932
+ const reflection = parseReflectionResponse(content);
933
+ if (reflection.rules.length === 0) {
934
+ console.error(
935
+ `[reflection] ${country} LLM 反思未产出有效规则,原始响应: ${content.substring(0, 200)}`,
936
+ );
937
+ return {
938
+ reflected: true,
939
+ rulesAdded: 0,
940
+ analysis: reflection.analysisSummary,
941
+ rawContent: content.substring(0, 500),
942
+ };
943
+ }
944
+
945
+ console.error(
946
+ `[reflection] ${country} LLM 分析: ${reflection.analysisSummary}`,
947
+ );
948
+ console.error(
949
+ `[reflection] ${country} 成功模式: ${reflection.successPattern}`,
950
+ );
951
+ console.error(
952
+ `[reflection] ${country} 失败模式: ${reflection.failurePattern}`,
953
+ );
954
+ console.error(
955
+ `[reflection] ${country} 产出 ${reflection.rules.length} 条规则:`,
956
+ );
957
+ for (const rule of reflection.rules) {
958
+ console.error(` [${rule.ruleType}] ${rule.ruleText}`);
959
+ }
960
+
961
+ // Step 6: 废弃旧规则,写入新规则
962
+ const supersedeResult = supersedeRules(country, "new reflection round");
963
+ console.error(
964
+ `[reflection] ${country} 废弃 ${supersedeResult.superseded} 条旧规则`,
965
+ );
966
+
967
+ const rulesToInsert = reflection.rules.map((r) => ({
968
+ country,
969
+ ruleType: r.ruleType,
970
+ ruleText: r.ruleText,
971
+ triggerReason: r.triggerReason,
972
+ productiveSample: data.productiveTop.slice(0, 10).map((t) => t.tag),
973
+ deadSample: data.deadFrequency.slice(0, 20).map((t) => t.tag),
974
+ analysisSummary: reflection.analysisSummary,
975
+ }));
976
+
977
+ const insertResult = insertReflectionRules(rulesToInsert);
978
+ console.error(
979
+ `[reflection] ${country} 写入 ${insertResult.inserted} 条新规则`,
980
+ );
981
+
982
+ return {
983
+ reflected: true,
984
+ rulesAdded: insertResult.inserted,
985
+ analysis: reflection.analysisSummary,
986
+ successPattern: reflection.successPattern,
987
+ failurePattern: reflection.failurePattern,
988
+ rules: reflection.rules,
989
+ superseded: supersedeResult.superseded,
990
+ };
991
+ }