tt-help-cli-ycl 1.4.4 → 1.4.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/tag.js CHANGED
@@ -66,12 +66,28 @@ function getBrowserMemory(port = null) {
66
66
  cmd =
67
67
  'ps aux | grep "[m]sedge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
68
68
  }
69
+ } else if (platform === "win32") {
70
+ // Windows: 用 PowerShell 查 msedge 进程内存(WorkingSet64 → MB)
71
+ // 按端口筛选时匹配命令行参数 --remote-debugging-port=PORT
72
+ if (port) {
73
+ cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'Name LIKE \\\"msedge%\\\"' | Where-Object { $_.CommandLine -match 'remote-debugging-port=${port}' } | ForEach-Object { $_.WorkingSet64 } | Measure-Object -Sum | Select-Object -ExpandProperty Sum"`;
74
+ } else {
75
+ cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'Name LIKE \\\"msedge%\\\"' | ForEach-Object { $_.WorkingSet64 } | Measure-Object -Sum | Select-Object -ExpandProperty Sum"`;
76
+ }
69
77
  } else {
70
78
  return null;
71
79
  }
72
- const result = execSync(cmd, { timeout: 3000 }).toString().trim();
73
- const mb = parseInt(result, 10);
74
- return mb > 0 ? mb : null;
80
+ const result = execSync(cmd, { timeout: 5000 }).toString().trim();
81
+ if (platform === "win32") {
82
+ // PowerShell 返回 WorkingSet64(字节),需除以 1048576 得 MB
83
+ const bytes = parseInt(result, 10);
84
+ const mb = bytes > 0 ? Math.round(bytes / 1048576) : 0;
85
+ return mb > 0 ? mb : null;
86
+ } else {
87
+ // macOS/Linux: ps RSS 是 KB,awk 已除以 1024 得 MB
88
+ const mb = parseInt(result, 10);
89
+ return mb > 0 ? mb : null;
90
+ }
75
91
  } catch {
76
92
  return null;
77
93
  }
@@ -97,6 +97,23 @@ function killEdgeProcesses(targetDir, port) {
97
97
  } else {
98
98
  command = "true";
99
99
  }
100
+ } else if (port) {
101
+ // 仅按端口筛选,不误杀其他端口的 Edge 实例
102
+ let pids = "";
103
+ try {
104
+ pids = execSync(
105
+ `ps aux | grep "[M]icrosoft Edge" | grep -v "Helper\\|crashpad" | grep "remote-debugging-port=${port}" | awk '{print $2}'`,
106
+ )
107
+ .toString()
108
+ .trim();
109
+ } catch (e) {
110
+ pids = "";
111
+ }
112
+ if (pids) {
113
+ command = `kill -9 ${pids} 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true`;
114
+ } else {
115
+ command = "true";
116
+ }
100
117
  } else {
101
118
  command =
102
119
  'killall -9 "Microsoft Edge" 2>/dev/null; rm -f ~/Library/Caches/Microsoft\\ Edge/Singleton*; true';
@@ -143,6 +160,8 @@ function killEdgeProcesses(targetDir, port) {
143
160
  if (targetDir) {
144
161
  const escapedDir = targetDir.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
145
162
  command = `ps aux | grep msedge | grep -v Helper | grep -E -- '--user-data-dir=${escapedDir}($|[^A-Za-z0-9_])' | awk '{print $2}' | xargs -r kill -9 2>/dev/null; true`;
163
+ } else if (port) {
164
+ command = `ps aux | grep msedge | grep -v Helper | grep "remote-debugging-port=${port}" | awk '{print $2}' | xargs -r kill -9 2>/dev/null; true`;
146
165
  } else {
147
166
  command = "pkill -9 -f msedge 2>/dev/null; true";
148
167
  }
@@ -360,6 +360,13 @@ export function getRecentDiscoverLogs(country, limit = 10) {
360
360
  /**
361
361
  * 获取最近 N 轮生成的所有标签(去重),用于检测重复生成
362
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
+
363
370
  export function getRecentGeneratedTags(country, lookback = 10) {
364
371
  const db = getDb();
365
372
  if (!db) return [];
@@ -493,6 +500,7 @@ export function getCountryHealth(country) {
493
500
  let recentDeadCount = 0;
494
501
  let recentTotalGenerated = 0;
495
502
  let recentEmptyRounds = 0;
503
+ let consecutiveLowProductiveRounds = 0; // 连续 productive=0 的轮数
496
504
 
497
505
  for (const log of recentLogs) {
498
506
  const tagsGen = JSON.parse(log.tags_generated || "[]");
@@ -504,10 +512,20 @@ export function getCountryHealth(country) {
504
512
  const tagResults = db
505
513
  .prepare(`SELECT status FROM tags WHERE tag IN (${placeholders})`)
506
514
  .all(...tagsGen);
507
- recentProductiveCount += tagResults.filter(
515
+ const productiveInRound = tagResults.filter(
508
516
  (t) => t.status === "productive",
509
517
  ).length;
518
+ recentProductiveCount += productiveInRound;
510
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++;
511
529
  }
512
530
  }
513
531
 
@@ -560,6 +578,14 @@ export function getCountryHealth(country) {
560
578
  );
561
579
  }
562
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
+
563
589
  if (lowScored.length > productive.length * 2) {
564
590
  issues.push(
565
591
  `low_scored=${lowScored.length} vs productive=${productive.length}: 低分tag远多于有效tag,应清理`,
@@ -582,6 +608,7 @@ export function getCountryHealth(country) {
582
608
  recentProductiveRatio: recentProductiveRatio.toFixed(1),
583
609
  recentDeadRatio: recentDeadRatio.toFixed(1),
584
610
  recentEmptyRounds,
611
+ consecutiveLowProductiveRounds,
585
612
  saturation: saturation.toFixed(0),
586
613
  healthStatus,
587
614
  issues,
@@ -1025,11 +1052,13 @@ export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1025
1052
  let totalProductive = 0;
1026
1053
  let totalDead = 0;
1027
1054
  let emptyRounds = 0;
1055
+ let consecutiveLowProductiveRounds = 0; // 连续 productive ≤ 1 的轮数
1028
1056
 
1029
1057
  for (const log of recentLogs) {
1030
1058
  const tagsGen = JSON.parse(log.tags_generated || "[]");
1031
1059
  if (tagsGen.length === 0) {
1032
1060
  emptyRounds++;
1061
+ consecutiveLowProductiveRounds++;
1033
1062
  continue;
1034
1063
  }
1035
1064
 
@@ -1044,21 +1073,33 @@ export function shouldTriggerReflection(country, lookback = 5, threshold = 1) {
1044
1073
  const dead = tagResults.filter((t) => t.status === "dead").length;
1045
1074
  totalProductive += productive;
1046
1075
  totalDead += dead;
1076
+
1077
+ // 连续低效轮计数:productive ≤ 1 则累加,否则重置
1078
+ if (productive <= 1) {
1079
+ consecutiveLowProductiveRounds++;
1080
+ } else {
1081
+ consecutiveLowProductiveRounds = 0;
1082
+ }
1047
1083
  }
1048
1084
 
1049
1085
  const avgProductive = totalProductive / lookback;
1050
- const shouldReflect = avgProductive <= threshold;
1086
+ // 触发条件:平均 productive ≤ threshold,或连续 ≥2 轮 productive ≤ 1
1087
+ const shouldReflect =
1088
+ avgProductive <= threshold || consecutiveLowProductiveRounds >= 2;
1051
1089
 
1052
1090
  return {
1053
1091
  shouldReflect,
1054
1092
  reason: shouldReflect
1055
- ? `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} ≤ ${threshold}`
1093
+ ? consecutiveLowProductiveRounds >= 2
1094
+ ? `连续${consecutiveLowProductiveRounds}轮productive≤1,需要反思换方向`
1095
+ : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} ≤ ${threshold}`
1056
1096
  : `最近${lookback}轮平均productive=${avgProductive.toFixed(1)} > ${threshold}`,
1057
1097
  recentRounds: recentLogs,
1058
1098
  totalProductive,
1059
1099
  totalDead,
1060
1100
  emptyRounds,
1061
1101
  avgProductive,
1102
+ consecutiveLowProductiveRounds,
1062
1103
  };
1063
1104
  }
1064
1105
 
@@ -148,6 +148,8 @@ export function buildDiscoverPrompt(
148
148
  health,
149
149
  nicheRecommendation,
150
150
  hardRules = [],
151
+ existingCount = 0,
152
+ forceNicheSwitch = null,
151
153
  ) {
152
154
  const lang = getLang(country);
153
155
  const langNames = {
@@ -288,6 +290,18 @@ ${nicheRecommendation.uncovered.length > 0 ? `Focus your tags on the UNCOVERED n
288
290
  `;
289
291
  }
290
292
 
293
+ // ====== 自闭环:强制方向切换提示 ======
294
+ let forceNicheSwitchHint = "";
295
+ if (forceNicheSwitch) {
296
+ forceNicheSwitchHint = `
297
+ === 🔄 MANDATORY DIRECTION SWITCH ===
298
+ ⚠️ CRITICAL: The previous direction "${forceNicheSwitch.from}" has been exhausted — ${forceNicheSwitch.reason}.
299
+ You MUST NOT generate tags in the "${forceNicheSwitch.from}" direction anymore.
300
+ You MUST switch to the "${forceNicheSwitch.to}" direction: ${forceNicheSwitch.description}.
301
+ Generate ONLY tags related to this new niche. Do NOT fall back to old patterns.
302
+ `;
303
+ }
304
+
291
305
  return `You are discovering TikTok hashtags used by people who sell things in ${country}.
292
306
 
293
307
  ⛔ CRITICAL RULE: ONLY suggest hashtags that you are CONFIDENT actually exist on TikTok with real posts.
@@ -321,7 +335,7 @@ ${hardRules.map((r) => ` ${r.ruleType === "hard" ? "⛔" : "💡"} ${r.ruleText
321
335
  : ""
322
336
  }
323
337
 
324
- ${productiveHint}${deadHint}${repeatedDeadHint}${langPatternHint}${errorHint}${userHint}${strategyReview}${healthHint}${nicheHint}
338
+ ${productiveHint}${deadHint}${repeatedDeadHint}${langPatternHint}${errorHint}${userHint}${strategyReview}${healthHint}${nicheHint}${forceNicheSwitchHint}
325
339
 
326
340
  ## Quality check before responding
327
341
 
@@ -337,7 +351,7 @@ Return ONLY a JSON object with three fields:
337
351
  }
338
352
 
339
353
  Rules:
340
- - Brainstorm broadly — generate 12-15 distinct, diverse tag ideas covering different categories (selling verbs, product types, community tags, marketplace terms, seasonal trends)
354
+ - Brainstorm broadly — generate at least ${Math.max(count * 3, 20)} distinct, diverse tag ideas covering different categories (selling verbs, product types, community tags, marketplace terms, seasonal trends). We already have ${existingCount} existing tags in our database, so many of your suggestions will be filtered out as duplicates — generate MORE than you think we need to ensure enough survive filtering.
341
355
  - We will filter duplicates and existing tags on our end, so explore freely
342
356
  - QUALITY over creativity — 3 real, commonly used tags beat 4 invented ones
343
357
  - NEVER invent compound words or translate concepts into hashtags
@@ -433,8 +447,57 @@ export async function discoverTagsForCountry(
433
447
  }
434
448
  }
435
449
 
436
- // 合并用户 prompt 和自动 niche prompt
437
- const effectiveUserPrompt = autoNichePrompt || userPrompt;
450
+ // ====== 自闭环 Step 2b: 连续低效轮强制方向切换 ======
451
+ // 商品分类方向定义(用于强制切换)
452
+ const NICHE_CATEGORIES = {
453
+ fashion:
454
+ "clothing, shoes, accessories, vintage, secondhand, thrift, sneakers, jewelry",
455
+ food: "bakery, food delivery, restaurant, cooking, local food, cafe, patisserie, boulangerie",
456
+ handmade_crafts:
457
+ "handmade, DIY, crafts, ceramics, candles, resin art, macrame, pottery, weaving",
458
+ home_living:
459
+ "home decoration, furniture, interior design, cleaning, moving, renovation",
460
+ electronics_tech:
461
+ "electronics, gadgets, phones, computers, audio, gaming, chargers, accessories",
462
+ pets_animals:
463
+ "pet supplies, dog accessories, cat food, animal care, grooming, veterinary",
464
+ beauty_health:
465
+ "beauty, cosmetics, skincare, hair care, wellness, perfume, makeup, spa",
466
+ books_collectibles:
467
+ "books, collectibles, vinyl records, trading cards, antiques, coins, stamps",
468
+ services_professional:
469
+ "repair, cleaning, photography, florist, tutoring, legal, consulting, moving",
470
+ marketplace_local:
471
+ "local market, marketplace, shop, boutique, online store, flea market, bazaar",
472
+ };
473
+
474
+ let forceNicheSwitch = null;
475
+ if (health && health.consecutiveLowProductiveRounds >= 2) {
476
+ // 连续 ≥2 轮 productive=0,强制切换方向
477
+ // 找出当前 productive tag 覆盖最多的 niche(即当前方向)
478
+ const currentNiche =
479
+ nicheRecommendation?.saturated?.[0]?.niche || "marketplace_local";
480
+ // 找出覆盖最少的 niche 作为新方向
481
+ const targetNiche = nicheRecommendation?.uncovered?.[0]?.niche || "food";
482
+ forceNicheSwitch = {
483
+ from: currentNiche,
484
+ to: targetNiche,
485
+ reason: `连续 ${health.consecutiveLowProductiveRounds} 轮无有效产出`,
486
+ description: NICHE_CATEGORIES[targetNiche] || targetNiche,
487
+ };
488
+ console.error(
489
+ `[tag-service] 🔄 ${country} 强制方向切换: ${currentNiche} → ${targetNiche} (原因: 连续${health.consecutiveLowProductiveRounds}轮低效)`,
490
+ );
491
+ }
492
+
493
+ // 合并用户 prompt、自动 niche prompt 和强制方向切换
494
+ let effectiveUserPrompt = userPrompt;
495
+ if (forceNicheSwitch) {
496
+ // 强制方向切换优先级最高,覆盖 autoNichePrompt
497
+ effectiveUserPrompt = `MANDATORY DIRECTION CHANGE: You MUST switch from "${forceNicheSwitch.from}" to "${forceNicheSwitch.to}" niche. ${forceNicheSwitch.reason}. Focus on: ${forceNicheSwitch.description}. Do NOT generate tags in the "${forceNicheSwitch.from}" direction anymore — it has been exhausted with no productive results.`;
498
+ } else if (autoNichePrompt) {
499
+ effectiveUserPrompt = autoNichePrompt;
500
+ }
438
501
 
439
502
  // ====== 自闭环 Step 3: 动态温度调整 ======
440
503
  let temperature = 0.7; // 默认
@@ -444,15 +507,22 @@ export async function discoverTagsForCountry(
444
507
  } else if (health.healthStatus === "warning") {
445
508
  temperature = 0.5; // 较保守
446
509
  }
510
+ // 连续低效轮时进一步降温,让 LLM 更保守
511
+ if (health.consecutiveLowProductiveRounds >= 2) {
512
+ temperature = Math.min(temperature, 0.3);
513
+ }
447
514
  }
448
515
 
449
516
  // ====== 原有 discover 流程 ======
450
517
  // 读取历史打分记录
451
518
  const productive = store.getTagsByCountry(country, 50);
452
519
  const dead = store.getDeadTags(country);
453
- // 获取该国所有已存在的 tag 名(应用层去重用)
454
- const allTags = store.getTagsByCountry(country, 0);
455
- const allExisting = new Set(allTags.map((t) => t.tag));
520
+ // 全局去重:获取所有已存在的 tag 名(不限国家、不限状态),避免跨国家重复入库
521
+ const { getAllExistingTagNames } = await import("./db-tags.js");
522
+ const allExisting = getAllExistingTagNames();
523
+ console.error(
524
+ `[tag-service] ${country} 全局去重集合: ${allExisting.size} 个已存在 tag`,
525
+ );
456
526
 
457
527
  // 获取最近生成的标签(用于检测重复生成)
458
528
  const { getRecentGeneratedTags } = await import("./db-tags.js");
@@ -482,6 +552,8 @@ export async function discoverTagsForCountry(
482
552
  health,
483
553
  nicheRecommendation,
484
554
  hardRules,
555
+ allExisting.size,
556
+ forceNicheSwitch,
485
557
  );
486
558
  console.error(
487
559
  `[tag-service] LLM discover ${country} round=${round} (lang=${getLang(country)}, count=${count}, temp=${temperature}, health=${health?.healthStatus || "unknown"})`,
@@ -492,9 +564,9 @@ export async function discoverTagsForCountry(
492
564
  const parsed = parseDiscoverResponse(content);
493
565
  const { strategy, tags } = parsed;
494
566
  const niche = parsed.niche || null;
495
- const unique = [...new Set(tags)]
496
- .filter((t) => !allExisting.has(t))
497
- .slice(0, count);
567
+ const deduped = [...new Set(tags)]; // LLM 自身去重
568
+ const filtered = deduped.filter((t) => !allExisting.has(t)); // 全局去重
569
+ const unique = filtered.slice(0, count);
498
570
 
499
571
  console.error(
500
572
  `[tag-service] LLM strategy: ${strategy?.substring(0, 150) || "(none)"}`,
@@ -503,8 +575,14 @@ export async function discoverTagsForCountry(
503
575
  console.error(`[tag-service] LLM niche focus: ${niche}`);
504
576
  }
505
577
  console.error(
506
- `[tag-service] LLM generated ${unique.length} tags: ${unique.join(", ")}`,
578
+ `[tag-service] ${country} 生成链路: LLM原始=${tags.length} → 自去重=${deduped.length} → 全局去重=${filtered.length} → 截取=${unique.length}`,
507
579
  );
580
+ if (filtered.length < count) {
581
+ const dupCount = deduped.length - filtered.length;
582
+ console.error(
583
+ `[tag-service] ⚠️ ${country} 去重后仅剩 ${filtered.length} 个(${dupCount} 个与已有 ${allExisting.size} 个 tag 重复),不足目标 ${count} 个`,
584
+ );
585
+ }
508
586
 
509
587
  // 去重写入数据库
510
588
  const inserted = [];