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 +1 -1
- package/src/cli/tag.js +19 -3
- package/src/lib/browser/cdp.js +19 -0
- package/src/watch/db-tags.js +44 -3
- package/src/watch/tag-service.js +89 -11
package/package.json
CHANGED
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:
|
|
73
|
-
|
|
74
|
-
|
|
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
|
}
|
package/src/lib/browser/cdp.js
CHANGED
|
@@ -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
|
}
|
package/src/watch/db-tags.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
package/src/watch/tag-service.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
437
|
-
|
|
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
|
-
//
|
|
454
|
-
const
|
|
455
|
-
const allExisting =
|
|
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
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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 = [];
|