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