tt-help-cli-ycl 1.4.3 → 1.4.5

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.
@@ -357,6 +357,37 @@ export function getRecentDiscoverLogs(country, limit = 10) {
357
357
  }));
358
358
  }
359
359
 
360
+ /**
361
+ * 获取最近 N 轮生成的所有标签(去重),用于检测重复生成
362
+ */
363
+ export function getAllExistingTagNames() {
364
+ const db = getDb();
365
+ if (!db) return new Set();
366
+ const rows = db.prepare("SELECT tag FROM tags").all();
367
+ return new Set(rows.map((r) => r.tag));
368
+ }
369
+
370
+ export function getRecentGeneratedTags(country, lookback = 10) {
371
+ const db = getDb();
372
+ if (!db) return [];
373
+
374
+ const logs = db
375
+ .prepare(
376
+ `SELECT tags_generated FROM discover_log
377
+ WHERE country = ?
378
+ ORDER BY round DESC
379
+ LIMIT ?`,
380
+ )
381
+ .all(country, lookback);
382
+
383
+ const allTags = new Set();
384
+ for (const log of logs) {
385
+ const tags = JSON.parse(log.tags_generated || "[]");
386
+ for (const t of tags) allTags.add(t);
387
+ }
388
+ return [...allTags];
389
+ }
390
+
360
391
  /**
361
392
  * 获取某国家的 discover 轮次(用于递增)
362
393
  */
@@ -425,3 +456,762 @@ export function getDiscoverEffectiveness(country) {
425
456
  };
426
457
  });
427
458
  }
459
+
460
+ // ====== 自闭环:健康指标 & 数据清理 ======
461
+
462
+ /**
463
+ * 获取某国家的 tag 健康指标
464
+ * 返回:总数、各状态数、dead比率、productive比率、饱和度、最近轮次效果
465
+ */
466
+ export function getCountryHealth(country) {
467
+ const db = getDb();
468
+ if (!db) return null;
469
+
470
+ // 基础统计
471
+ const allTags = db
472
+ .prepare(`SELECT tag, status, score FROM tags WHERE countries LIKE ?`)
473
+ .all(`%"${country}"%`);
474
+
475
+ const total = allTags.length;
476
+ const productive = allTags.filter((t) => t.status === "productive");
477
+ const dead = allTags.filter((t) => t.status === "dead");
478
+ const scored = allTags.filter((t) => t.status === "scored");
479
+ const lowScored = allTags.filter(
480
+ (t) => t.status === "scored" && t.score >= 10 && t.score < 20,
481
+ );
482
+
483
+ const deadRatio = total > 0 ? (dead.length / total) * 100 : 0;
484
+ const productiveRatio = total > 0 ? (productive.length / total) * 100 : 0;
485
+ const avgProductiveScore =
486
+ productive.length > 0
487
+ ? productive.reduce((s, t) => s + t.score, 0) / productive.length
488
+ : 0;
489
+
490
+ // 最近5轮 discover 效果
491
+ const recentLogs = db
492
+ .prepare(
493
+ `SELECT round, tags_added, tags_generated FROM discover_log
494
+ WHERE country = ? ORDER BY round DESC LIMIT 5`,
495
+ )
496
+ .all(country);
497
+
498
+ // 计算最近5轮的 productive 和 dead 比例
499
+ let recentProductiveCount = 0;
500
+ let recentDeadCount = 0;
501
+ let recentTotalGenerated = 0;
502
+ let recentEmptyRounds = 0;
503
+ let consecutiveLowProductiveRounds = 0; // 连续 productive=0 的轮数
504
+
505
+ for (const log of recentLogs) {
506
+ const tagsGen = JSON.parse(log.tags_generated || "[]");
507
+ recentTotalGenerated += tagsGen.length;
508
+ if (tagsGen.length === 0) recentEmptyRounds++;
509
+
510
+ if (tagsGen.length > 0) {
511
+ const placeholders = tagsGen.map(() => "?").join(",");
512
+ const tagResults = db
513
+ .prepare(`SELECT status FROM tags WHERE tag IN (${placeholders})`)
514
+ .all(...tagsGen);
515
+ const productiveInRound = tagResults.filter(
516
+ (t) => t.status === "productive",
517
+ ).length;
518
+ recentProductiveCount += productiveInRound;
519
+ recentDeadCount += tagResults.filter((t) => t.status === "dead").length;
520
+ // 连续低效轮计数:productive=0 则累加,否则重置
521
+ if (productiveInRound === 0) {
522
+ consecutiveLowProductiveRounds++;
523
+ } else {
524
+ consecutiveLowProductiveRounds = 0;
525
+ }
526
+ } else {
527
+ // 空轮也算低效轮
528
+ consecutiveLowProductiveRounds++;
529
+ }
530
+ }
531
+
532
+ const recentProductiveRatio =
533
+ recentTotalGenerated > 0
534
+ ? (recentProductiveCount / recentTotalGenerated) * 100
535
+ : 0;
536
+ const recentDeadRatio =
537
+ recentTotalGenerated > 0
538
+ ? (recentDeadCount / recentTotalGenerated) * 100
539
+ : 0;
540
+
541
+ // 饱和度:最近轮次生成空或极少 tag 的比例
542
+ const saturation =
543
+ recentLogs.length > 0 ? (recentEmptyRounds / recentLogs.length) * 100 : 0;
544
+
545
+ // 判断健康状态
546
+ let healthStatus = "healthy";
547
+ const issues = [];
548
+
549
+ if (deadRatio > 60) {
550
+ healthStatus = "critical";
551
+ issues.push(
552
+ `dead_ratio=${deadRatio.toFixed(1)}% (>60%): 太多无效tag,需要降低生成激进度`,
553
+ );
554
+ } else if (deadRatio > 40) {
555
+ healthStatus = "warning";
556
+ issues.push(
557
+ `dead_ratio=${deadRatio.toFixed(1)}% (>40%): 无效tag偏多,需要更保守`,
558
+ );
559
+ }
560
+
561
+ if (productiveRatio < 8) {
562
+ healthStatus = healthStatus === "critical" ? "critical" : "warning";
563
+ issues.push(
564
+ `productive_ratio=${productiveRatio.toFixed(1)}% (<8%): 有效tag太少,可能需要切换策略方向`,
565
+ );
566
+ }
567
+
568
+ if (saturation >= 40) {
569
+ healthStatus = healthStatus === "critical" ? "critical" : "warning";
570
+ issues.push(
571
+ `saturation=${saturation.toFixed(0)}%: tag空间接近饱和,需要切换到新niche`,
572
+ );
573
+ }
574
+
575
+ if (recentEmptyRounds > 0) {
576
+ issues.push(
577
+ `recent_empty_rounds=${recentEmptyRounds}: LLM无法生成新不重复tag`,
578
+ );
579
+ }
580
+
581
+ // 连续低效轮警告:连续多轮 productive=0,需要强制换方向
582
+ if (consecutiveLowProductiveRounds >= 2) {
583
+ healthStatus = healthStatus === "critical" ? "critical" : "warning";
584
+ issues.push(
585
+ `consecutive_low_productive_rounds=${consecutiveLowProductiveRounds}: 连续${consecutiveLowProductiveRounds}轮无有效产出,必须切换商品分类方向`,
586
+ );
587
+ }
588
+
589
+ if (lowScored.length > productive.length * 2) {
590
+ issues.push(
591
+ `low_scored=${lowScored.length} vs productive=${productive.length}: 低分tag远多于有效tag,应清理`,
592
+ );
593
+ }
594
+
595
+ // 推荐的 niche 方向(基于现有 productive tag 的分类分析)
596
+ const nicheRecommendation = recommendNiche(country, productive);
597
+
598
+ return {
599
+ country,
600
+ total,
601
+ productive: productive.length,
602
+ dead: dead.length,
603
+ scored: scored.length,
604
+ lowScored: lowScored.length,
605
+ deadRatio: deadRatio.toFixed(1),
606
+ productiveRatio: productiveRatio.toFixed(1),
607
+ avgProductiveScore: avgProductiveScore.toFixed(1),
608
+ recentProductiveRatio: recentProductiveRatio.toFixed(1),
609
+ recentDeadRatio: recentDeadRatio.toFixed(1),
610
+ recentEmptyRounds,
611
+ consecutiveLowProductiveRounds,
612
+ saturation: saturation.toFixed(0),
613
+ healthStatus,
614
+ issues,
615
+ nicheRecommendation,
616
+ };
617
+ }
618
+
619
+ /**
620
+ * 基于现有 productive tag 推荐新的 niche 方向
621
+ * 分析已有 tag 的类别分布,找出未覆盖或覆盖不足的方向
622
+ */
623
+ function recommendNiche(country, productiveTags) {
624
+ // 定义 niche 类别及其关键词模式
625
+ const niches = {
626
+ fashion: [
627
+ "clothes",
628
+ "kleidung",
629
+ "kleding",
630
+ "vestiti",
631
+ "ropa",
632
+ "outfit",
633
+ "mode",
634
+ "shoes",
635
+ "sneakers",
636
+ "jurken",
637
+ "obleceni",
638
+ "ruha",
639
+ "zapatos",
640
+ "chaussures",
641
+ "vintage",
642
+ "secondhand",
643
+ "thrift",
644
+ "friperie",
645
+ "tweedehands",
646
+ "hasznalt",
647
+ "segundamano",
648
+ "secondamano",
649
+ ],
650
+ food: [
651
+ "food",
652
+ "bakery",
653
+ "brot",
654
+ "brood",
655
+ "pane",
656
+ "comida",
657
+ "restaurant",
658
+ "cafe",
659
+ "kitchen",
660
+ "konyha",
661
+ "cukrar",
662
+ "patisserie",
663
+ "boulangerie",
664
+ "wypieki",
665
+ "sauerteig",
666
+ "etterm",
667
+ "kaviarnia",
668
+ ],
669
+ handmade_crafts: [
670
+ "handmade",
671
+ "handwerk",
672
+ "handgemaakt",
673
+ "artigianato",
674
+ "hechoamano",
675
+ "kézműves",
676
+ "kreativ",
677
+ "craft",
678
+ "diy",
679
+ "knutselen",
680
+ "ceramic",
681
+ "keramiek",
682
+ "kerzen",
683
+ "gyertya",
684
+ "harskunst",
685
+ "resin",
686
+ "macrame",
687
+ "pottery",
688
+ "weaving",
689
+ ],
690
+ home_living: [
691
+ "home",
692
+ "wohnung",
693
+ "huis",
694
+ "casa",
695
+ "casa",
696
+ "lakberendezes",
697
+ "interior",
698
+ "deco",
699
+ "decoration",
700
+ "einrichten",
701
+ "furniture",
702
+ "möbel",
703
+ "meubilair",
704
+ "epipla",
705
+ "butor",
706
+ "nabytek",
707
+ "tapic",
708
+ "reinigung",
709
+ "cleaning",
710
+ ],
711
+ electronics_tech: [
712
+ "tech",
713
+ "elektronik",
714
+ "electronica",
715
+ "gadget",
716
+ "phone",
717
+ "smartphone",
718
+ "laptop",
719
+ "computer",
720
+ "audio",
721
+ "cuffie",
722
+ "auricolari",
723
+ "headphones",
724
+ "caricatore",
725
+ "charger",
726
+ "usb",
727
+ "playstation",
728
+ "gaming",
729
+ ],
730
+ pets_animals: [
731
+ "pet",
732
+ "hund",
733
+ "dog",
734
+ "cat",
735
+ "kat",
736
+ "kuty",
737
+ "konia",
738
+ "pets",
739
+ "tier",
740
+ "dieren",
741
+ "animale",
742
+ "animal",
743
+ "kutyakonzerv",
744
+ "akcesoriadlapsow",
745
+ ],
746
+ beauty_health: [
747
+ "beauty",
748
+ "kosmetik",
749
+ "cosmetica",
750
+ "beaute",
751
+ "belleza",
752
+ "szepseg",
753
+ "kapper",
754
+ "hair",
755
+ "haj",
756
+ "skincare",
757
+ "makeup",
758
+ "parfum",
759
+ "perfume",
760
+ "wellness",
761
+ "gesundheit",
762
+ "salud",
763
+ "sante",
764
+ ],
765
+ books_collectibles: [
766
+ "book",
767
+ "buch",
768
+ "boek",
769
+ "libro",
770
+ "knihy",
771
+ "konyv",
772
+ "collectible",
773
+ "sammlung",
774
+ "verzameling",
775
+ "collezionismo",
776
+ "coleccion",
777
+ "antique",
778
+ "antiquariat",
779
+ "vintage",
780
+ "vinyl",
781
+ "schallplatten",
782
+ "pokemon",
783
+ "kaarten",
784
+ "moedas",
785
+ "mince",
786
+ ],
787
+ services_professional: [
788
+ "service",
789
+ "dienst",
790
+ "servizio",
791
+ "servicio",
792
+ "szolgaltatas",
793
+ "handwerker",
794
+ "craftsman",
795
+ "repair",
796
+ "reparatur",
797
+ "reparatie",
798
+ "riparazione",
799
+ "reparacion",
800
+ "cleaning",
801
+ "reinigung",
802
+ "moving",
803
+ "umzug",
804
+ "verhuur",
805
+ "mudanca",
806
+ "florist",
807
+ "photography",
808
+ "fotograf",
809
+ ],
810
+ marketplace_local: [
811
+ "market",
812
+ "markt",
813
+ "mercato",
814
+ "mercado",
815
+ "piac",
816
+ "trh",
817
+ "flohmarkt",
818
+ "rommelmarkt",
819
+ "brocante",
820
+ "bazar",
821
+ "mercadillo",
822
+ "vlooienmarkt",
823
+ "braderie",
824
+ "feira",
825
+ "local",
826
+ "lokal",
827
+ "locale",
828
+ "winkel",
829
+ "boutique",
830
+ "shop",
831
+ "tienda",
832
+ "negozio",
833
+ "bol",
834
+ "webshop",
835
+ "eshop",
836
+ ],
837
+ };
838
+
839
+ // 统计每个 niche 在 productive tag 中的覆盖数
840
+ const nicheCoverage = {};
841
+ for (const [niche, keywords] of Object.entries(niches)) {
842
+ const matched = productiveTags.filter((t) =>
843
+ keywords.some((kw) => t.tag.includes(kw)),
844
+ ).length;
845
+ nicheCoverage[niche] = matched;
846
+ }
847
+
848
+ // 找出覆盖不足的 niche(productive < 5)作为推荐方向
849
+ const uncovered = Object.entries(nicheCoverage)
850
+ .filter(([, count]) => count < 5)
851
+ .sort(([, a], [, b]) => a - b)
852
+ .map(([niche, count]) => ({ niche, currentCoverage: count }));
853
+
854
+ // 找出覆盖良好的 niche(productive >= 10)作为已饱和方向
855
+ const saturated = Object.entries(nicheCoverage)
856
+ .filter(([, count]) => count >= 10)
857
+ .sort(([, a], [, b]) => b - a)
858
+ .map(([niche, count]) => ({ niche, currentCoverage: count }));
859
+
860
+ return { uncovered, saturated, nicheCoverage };
861
+ }
862
+
863
+ /**
864
+ * 清理低效 tag:将 score < 20 的 scored tag 降级为 dead
865
+ * @param {number} minScore - 低于此分数的 scored tag 降级为 dead(默认 20)
866
+ * @returns {object} { cleaned: number }
867
+ */
868
+ export function cleanupLowScoredTags(minScore = 20) {
869
+ const db = getDb();
870
+ if (!db) return { cleaned: 0, error: "db not ready" };
871
+
872
+ const result = db
873
+ .prepare(
874
+ `UPDATE tags SET status = 'dead', last_error = 'auto_cleanup: scored<20'
875
+ WHERE status = 'scored' AND score < ?`,
876
+ )
877
+ .run(minScore);
878
+
879
+ if (result.changes > 0) {
880
+ console.error(
881
+ `[tags] 自闭环清理: ${result.changes} 个低分scored tag降级为dead (score<${minScore})`,
882
+ );
883
+ }
884
+ return { cleaned: result.changes };
885
+ }
886
+
887
+ /**
888
+ * 清理长时间卡在 scoring 状态的 tag(超时未打分)
889
+ * @param {number} minutes - 超时分钟数(默认 60)
890
+ * @returns {object} { reset: number }
891
+ */
892
+ export function cleanupStaleScoringTags(minutes = 60) {
893
+ const db = getDb();
894
+ if (!db) return { reset: 0, error: "db not ready" };
895
+
896
+ const result = db
897
+ .prepare(
898
+ `UPDATE tags SET status = 'new', scored_at = NULL
899
+ WHERE status = 'scoring' AND scored_at IS NOT NULL
900
+ AND scored_at < datetime('now', '-${minutes} minutes')`,
901
+ )
902
+ .run();
903
+
904
+ if (result.changes > 0) {
905
+ console.error(
906
+ `[tags] 自闭环清理: ${result.changes} 个超时scoring tag重置为new (>${minutes}min)`,
907
+ );
908
+ }
909
+ return { reset: result.changes };
910
+ }
911
+
912
+ /**
913
+ * 批量清理 error 状态的 tag(重置为 new 以重新打分)
914
+ * @returns {object} { reset: number }
915
+ */
916
+ export function cleanupErrorTags() {
917
+ const db = getDb();
918
+ if (!db) return { reset: 0, error: "db not ready" };
919
+
920
+ const result = db
921
+ .prepare(
922
+ `UPDATE tags SET status = 'new', scored_at = NULL, last_error = NULL
923
+ WHERE status = 'error'`,
924
+ )
925
+ .run();
926
+
927
+ if (result.changes > 0) {
928
+ console.error(`[tags] 自闭环清理: ${result.changes} 个error tag重置为new`);
929
+ }
930
+ return { reset: result.changes };
931
+ }
932
+
933
+ // ====== 反思闭环:reflection_rules CRUD ======
934
+
935
+ /**
936
+ * 获取某国家的所有活跃硬规则
937
+ */
938
+ export function getActiveRules(country) {
939
+ const db = getDb();
940
+ if (!db) return [];
941
+
942
+ return db
943
+ .prepare(
944
+ `SELECT id, country, rule_type, rule_text, trigger_reason, created_at
945
+ FROM reflection_rules
946
+ WHERE country = ? AND is_active = 1
947
+ ORDER BY created_at DESC`,
948
+ )
949
+ .all(country);
950
+ }
951
+
952
+ /**
953
+ * 插入一条反思规则
954
+ */
955
+ export function insertReflectionRule({
956
+ country,
957
+ ruleType = "hard",
958
+ ruleText,
959
+ triggerReason,
960
+ productiveSample,
961
+ deadSample,
962
+ analysisSummary,
963
+ }) {
964
+ const db = getDb();
965
+ if (!db) return { ok: false, error: "db not ready" };
966
+
967
+ try {
968
+ const result = db
969
+ .prepare(
970
+ `INSERT INTO reflection_rules
971
+ (country, rule_type, rule_text, source, trigger_reason,
972
+ productive_sample, dead_sample, analysis_summary)
973
+ VALUES (?, ?, ?, 'reflection', ?, ?, ?, ?)`,
974
+ )
975
+ .run(
976
+ country,
977
+ ruleType,
978
+ ruleText,
979
+ triggerReason || null,
980
+ JSON.stringify(productiveSample || []),
981
+ JSON.stringify(deadSample || []),
982
+ analysisSummary || null,
983
+ );
984
+ return { ok: true, id: result.lastInsertRowid };
985
+ } catch (e) {
986
+ return { ok: false, error: e.message };
987
+ }
988
+ }
989
+
990
+ /**
991
+ * 批量插入反思规则(一次反思产出多条规则)
992
+ */
993
+ export function insertReflectionRules(rules) {
994
+ const db = getDb();
995
+ if (!db) return { ok: false, error: "db not ready" };
996
+
997
+ const inserted = [];
998
+ for (const rule of rules) {
999
+ const result = insertReflectionRule(rule);
1000
+ if (result.ok) inserted.push(result.id);
1001
+ }
1002
+ return { ok: true, inserted: inserted.length };
1003
+ }
1004
+
1005
+ /**
1006
+ * 废弃旧规则(当新反思覆盖旧规则时)
1007
+ */
1008
+ export function supersedeRules(country, reason = "new reflection") {
1009
+ const db = getDb();
1010
+ if (!db) return { ok: false, error: "db not ready" };
1011
+
1012
+ const result = db
1013
+ .prepare(
1014
+ `UPDATE reflection_rules
1015
+ SET is_active = 0, superseded_at = datetime('now')
1016
+ WHERE country = ? AND is_active = 1`,
1017
+ )
1018
+ .run(country);
1019
+
1020
+ return { ok: true, superseded: result.changes };
1021
+ }
1022
+
1023
+ /**
1024
+ * 检查是否需要触发反思
1025
+ * 条件:最近 N 轮 productive_cnt ≤ threshold
1026
+ * @returns {{ shouldReflect: boolean, reason: string, recentRounds: array }}
1027
+ */
1028
+ export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1029
+ const db = getDb();
1030
+ if (!db) return { shouldReflect: false, reason: "db not ready" };
1031
+
1032
+ // 获取最近 lookback 轮的 discover_log
1033
+ const recentLogs = db
1034
+ .prepare(
1035
+ `SELECT round, tags_generated, tags_added, strategy, created_at
1036
+ FROM discover_log
1037
+ WHERE country = ?
1038
+ ORDER BY round DESC
1039
+ LIMIT ?`,
1040
+ )
1041
+ .all(country, lookback);
1042
+
1043
+ if (recentLogs.length < lookback) {
1044
+ return {
1045
+ shouldReflect: false,
1046
+ reason: `only ${recentLogs.length} rounds, need ${lookback}`,
1047
+ recentRounds: recentLogs,
1048
+ };
1049
+ }
1050
+
1051
+ // 计算每轮的 productive 数量
1052
+ let totalProductive = 0;
1053
+ let totalDead = 0;
1054
+ let emptyRounds = 0;
1055
+ let consecutiveLowProductiveRounds = 0; // 连续 productive ≤ 1 的轮数
1056
+
1057
+ for (const log of recentLogs) {
1058
+ const tagsGen = JSON.parse(log.tags_generated || "[]");
1059
+ if (tagsGen.length === 0) {
1060
+ emptyRounds++;
1061
+ consecutiveLowProductiveRounds++;
1062
+ continue;
1063
+ }
1064
+
1065
+ const placeholders = tagsGen.map(() => "?").join(",");
1066
+ const tagResults = db
1067
+ .prepare(`SELECT status FROM tags WHERE tag IN (${placeholders})`)
1068
+ .all(...tagsGen);
1069
+
1070
+ const productive = tagResults.filter(
1071
+ (t) => t.status === "productive",
1072
+ ).length;
1073
+ const dead = tagResults.filter((t) => t.status === "dead").length;
1074
+ totalProductive += productive;
1075
+ totalDead += dead;
1076
+
1077
+ // 连续低效轮计数:productive ≤ 1 则累加,否则重置
1078
+ if (productive <= 1) {
1079
+ consecutiveLowProductiveRounds++;
1080
+ } else {
1081
+ consecutiveLowProductiveRounds = 0;
1082
+ }
1083
+ }
1084
+
1085
+ const avgProductive = totalProductive / lookback;
1086
+ // 触发条件:平均 productive ≤ threshold,或连续 ≥2 轮 productive ≤ 1
1087
+ const shouldReflect =
1088
+ avgProductive <= threshold || consecutiveLowProductiveRounds >= 2;
1089
+
1090
+ return {
1091
+ shouldReflect,
1092
+ reason: shouldReflect
1093
+ ? consecutiveLowProductiveRounds >= 2
1094
+ ? `连续${consecutiveLowProductiveRounds}轮productive≤1,需要反思换方向`
1095
+ : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} ≤ ${threshold}`
1096
+ : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} > ${threshold}`,
1097
+ recentRounds: recentLogs,
1098
+ totalProductive,
1099
+ totalDead,
1100
+ emptyRounds,
1101
+ avgProductive,
1102
+ consecutiveLowProductiveRounds,
1103
+ };
1104
+ }
1105
+
1106
+ /**
1107
+ * 获取反思所需的数据摘要(供 LLM 分析)
1108
+ */
1109
+ export function getReflectionData(country) {
1110
+ const db = getDb();
1111
+ if (!db) return null;
1112
+
1113
+ // 1. 最近 10 轮 discover_log
1114
+ const recentLogs = db
1115
+ .prepare(
1116
+ `SELECT round, strategy, tags_generated, tags_added,
1117
+ avg_productive_score, avg_dead_score, created_at
1118
+ FROM discover_log
1119
+ WHERE country = ?
1120
+ ORDER BY round DESC
1121
+ LIMIT 10`,
1122
+ )
1123
+ .all(country);
1124
+
1125
+ // 2. productive top 20(成功模式)
1126
+ const productiveTop = db
1127
+ .prepare(
1128
+ `SELECT tag, score, matched_authors, pushed_users
1129
+ FROM tags
1130
+ WHERE status = 'productive' AND countries LIKE ?
1131
+ ORDER BY pushed_users DESC
1132
+ LIMIT 20`,
1133
+ )
1134
+ .all(`%"${country}"%`);
1135
+
1136
+ // 3. dead 标签频次统计(哪些标签被反复生成且 dead)
1137
+ const deadFrequency = db
1138
+ .prepare(
1139
+ `SELECT tag, score, last_error, created_at
1140
+ FROM tags
1141
+ WHERE status = 'dead' AND countries LIKE ?
1142
+ ORDER BY created_at DESC
1143
+ LIMIT 50`,
1144
+ )
1145
+ .all(`%"${country}"%`);
1146
+
1147
+ // 4. dead 标签的 error 类型统计
1148
+ const errorStats = db
1149
+ .prepare(
1150
+ `SELECT
1151
+ COALESCE(last_error, 'no_match') as error_type,
1152
+ COUNT(*) as count
1153
+ FROM tags
1154
+ WHERE status = 'dead' AND countries LIKE ?
1155
+ GROUP BY COALESCE(last_error, 'no_match')
1156
+ ORDER BY count DESC`,
1157
+ )
1158
+ .all(`%"${country}"%`);
1159
+
1160
+ // 5. 语言分布:dead 标签中英语 vs 本地语言的比例
1161
+ const lang = getLang(country);
1162
+ const langNames = {
1163
+ cs: "Czech",
1164
+ el: "Greek",
1165
+ hu: "Hungarian",
1166
+ pt: "Portuguese",
1167
+ es: "Spanish",
1168
+ pl: "Polish",
1169
+ nl: "Dutch",
1170
+ de: "German",
1171
+ fr: "French",
1172
+ it: "Italian",
1173
+ en: "English",
1174
+ };
1175
+ const langName = langNames[lang] || lang;
1176
+
1177
+ // 6. 当前活跃的硬规则
1178
+ const activeRules = getActiveRules(country);
1179
+
1180
+ // 7. 健康指标
1181
+ const health = getCountryHealth(country);
1182
+
1183
+ return {
1184
+ country,
1185
+ lang,
1186
+ langName,
1187
+ recentLogs,
1188
+ productiveTop,
1189
+ deadFrequency,
1190
+ errorStats,
1191
+ activeRules,
1192
+ health,
1193
+ };
1194
+ }
1195
+
1196
+ /**
1197
+ * 获取某语言判断函数(用于分析 dead 标签的语言)
1198
+ * 简单判断:是否包含该国本地语言的特殊字符
1199
+ */
1200
+ function getLang(country) {
1201
+ const COUNTRY_LANG = {
1202
+ CZ: "cs",
1203
+ GR: "el",
1204
+ HU: "hu",
1205
+ PT: "pt",
1206
+ ES: "es",
1207
+ PL: "pl",
1208
+ NL: "nl",
1209
+ DE: "de",
1210
+ FR: "fr",
1211
+ IT: "it",
1212
+ IE: "en",
1213
+ AT: "de",
1214
+ BE: "nl",
1215
+ };
1216
+ return COUNTRY_LANG[country] || "en";
1217
+ }