tt-help-cli-ycl 1.4.1 → 1.4.3
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 +77 -25
- package/src/lib/args.js +4 -0
- package/src/lib/constants.js +3 -2
- package/src/lib/tag-discover.js +15 -5
- package/src/watch/db-tags.js +10 -2
- package/src/watch/server.js +11 -2
- package/src/watch/tag-service.js +13 -17
package/package.json
CHANGED
package/src/cli/tag.js
CHANGED
|
@@ -580,6 +580,7 @@ export async function handleScoreAll(parsed) {
|
|
|
580
580
|
countries,
|
|
581
581
|
serverUrl,
|
|
582
582
|
autoDiscover,
|
|
583
|
+
discoverCount = 10,
|
|
583
584
|
port,
|
|
584
585
|
proxy: cliProxy,
|
|
585
586
|
} = tagScoreAll || {};
|
|
@@ -654,7 +655,7 @@ export async function handleScoreAll(parsed) {
|
|
|
654
655
|
);
|
|
655
656
|
const memHeapRecycleMb = Math.max(
|
|
656
657
|
128,
|
|
657
|
-
parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "
|
|
658
|
+
parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "512", 10) || 512,
|
|
658
659
|
);
|
|
659
660
|
const recycleCooldownMs = Math.max(
|
|
660
661
|
0,
|
|
@@ -687,9 +688,9 @@ export async function handleScoreAll(parsed) {
|
|
|
687
688
|
}
|
|
688
689
|
}
|
|
689
690
|
|
|
690
|
-
async function recycleCdpSession(reason) {
|
|
691
|
+
async function recycleCdpSession(reason, { switchPort = true } = {}) {
|
|
691
692
|
const oldPort = cdpPort;
|
|
692
|
-
if (switchPortOnRecycle) {
|
|
693
|
+
if (switchPort && switchPortOnRecycle) {
|
|
693
694
|
cdpPort = pickNextPort(cdpPort);
|
|
694
695
|
cdpOpts.port = cdpPort;
|
|
695
696
|
clientMeta.port = cdpPort;
|
|
@@ -722,6 +723,7 @@ export async function handleScoreAll(parsed) {
|
|
|
722
723
|
}
|
|
723
724
|
await recycleCdpSession(
|
|
724
725
|
`mem rss=${rssMB.toFixed(0)}MB heap=${heapMB.toFixed(0)}MB (threshold rss=${memRssRecycleMb} heap=${memHeapRecycleMb})`,
|
|
726
|
+
{ switchPort: false },
|
|
725
727
|
);
|
|
726
728
|
return;
|
|
727
729
|
}
|
|
@@ -730,15 +732,21 @@ export async function handleScoreAll(parsed) {
|
|
|
730
732
|
totalScored > 0 &&
|
|
731
733
|
totalScored % periodicRecycleEvery === 0
|
|
732
734
|
) {
|
|
733
|
-
await recycleCdpSession(`periodic every ${totalScored} tasks
|
|
735
|
+
await recycleCdpSession(`periodic every ${totalScored} tasks`, {
|
|
736
|
+
switchPort: false,
|
|
737
|
+
});
|
|
734
738
|
}
|
|
735
739
|
}
|
|
736
740
|
|
|
737
741
|
let totalScored = 0;
|
|
738
742
|
let emptyRounds = 0; // 连续无任务的轮数
|
|
739
743
|
let consecutive403 = 0; // 连续 HTTP 403 次数,达 3 次认为被封
|
|
740
|
-
const DISCOVER_AFTER_EMPTY = 3; // 连续 3 轮无任务时触发 discover
|
|
744
|
+
const DISCOVER_AFTER_EMPTY = 3; // 连续 3 轮无任务时触发 discover(兜底)
|
|
741
745
|
const CONSECUTIVE_403_BLOCKED = 3; // 连续 3 次 403 认为账号被封
|
|
746
|
+
const DISCOVER_LOW_THRESHOLD = 5; // 剩余 new 标签 < 5 时提前触发发现
|
|
747
|
+
const DISCOVER_COOLDOWN_MS = 5 * 60 * 1000; // 发现冷却时间 5 分钟,防止频繁调用 LLM
|
|
748
|
+
let discoverPromise = null; // 正在后台执行的发现 Promise
|
|
749
|
+
let lastDiscoverTime = 0; // 上次发现完成的时间戳
|
|
742
750
|
|
|
743
751
|
// 生成客户端 ID,用于服务端追踪
|
|
744
752
|
const clientId = randomUUID();
|
|
@@ -763,38 +771,82 @@ export async function handleScoreAll(parsed) {
|
|
|
763
771
|
};
|
|
764
772
|
process.on("SIGINT", cleanup);
|
|
765
773
|
|
|
774
|
+
// 异步发现函数:后台为所有目标国家生成标签,不阻塞打分循环
|
|
775
|
+
async function runDiscoverInBackground() {
|
|
776
|
+
if (discoverPromise) return; // 已在执行中,不重复触发
|
|
777
|
+
const elapsed = Date.now() - lastDiscoverTime;
|
|
778
|
+
if (elapsed < DISCOVER_COOLDOWN_MS) {
|
|
779
|
+
const remainMin = Math.ceil((DISCOVER_COOLDOWN_MS - elapsed) / 60000);
|
|
780
|
+
log(`🔍 发现冷却中,还需 ${remainMin} 分钟后才能再次触发`);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
log(`🔍 剩余标签不足,后台为 ${targetCountries.length} 个国家生成标签...`);
|
|
784
|
+
discoverPromise = (async () => {
|
|
785
|
+
let totalInserted = 0;
|
|
786
|
+
for (const country of targetCountries) {
|
|
787
|
+
try {
|
|
788
|
+
const discRes = await fetch(
|
|
789
|
+
`${baseUrl}/api/tags/discover?country=${country}&count=${discoverCount}`,
|
|
790
|
+
{ headers: buildClientHeaders(clientId, clientMeta) },
|
|
791
|
+
);
|
|
792
|
+
const discData = await discRes.json();
|
|
793
|
+
if (discData.added) {
|
|
794
|
+
totalInserted += discData.added;
|
|
795
|
+
log(` ${country}: 新增 ${discData.added} 个`);
|
|
796
|
+
}
|
|
797
|
+
} catch (e) {
|
|
798
|
+
log(` ${country}: 请求失败 (${e.message})`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
log(` ✅ 发现完成,共入库 ${totalInserted} 个新标签`);
|
|
802
|
+
lastDiscoverTime = Date.now(); // 记录完成时间,开始冷却
|
|
803
|
+
discoverPromise = null;
|
|
804
|
+
})();
|
|
805
|
+
}
|
|
806
|
+
|
|
766
807
|
try {
|
|
767
808
|
while (true) {
|
|
768
809
|
try {
|
|
769
|
-
// 从服务端取下一个 new
|
|
810
|
+
// 从服务端取下一个 new 标签(同时查剩余数量)
|
|
770
811
|
const tagsRes = await fetch(`${baseUrl}/api/tags?status=new&limit=1`, {
|
|
771
812
|
headers: buildClientHeaders(clientId, clientMeta),
|
|
772
813
|
});
|
|
773
814
|
const tagsData = await tagsRes.json();
|
|
815
|
+
|
|
816
|
+
// 提前触发发现:剩余 new 标签低于阈值且不在冷却期时后台生成
|
|
817
|
+
if (
|
|
818
|
+
autoDiscover &&
|
|
819
|
+
!discoverPromise &&
|
|
820
|
+
Date.now() - lastDiscoverTime >= DISCOVER_COOLDOWN_MS &&
|
|
821
|
+
(tagsData.total || 0) < DISCOVER_LOW_THRESHOLD
|
|
822
|
+
) {
|
|
823
|
+
runDiscoverInBackground();
|
|
824
|
+
}
|
|
825
|
+
|
|
774
826
|
if (!tagsData.tags || tagsData.tags.length === 0) {
|
|
775
827
|
emptyRounds++;
|
|
776
828
|
|
|
777
|
-
//
|
|
829
|
+
// 兜底发现:连续 N 轮无任务(提前发现可能没生效)
|
|
778
830
|
if (autoDiscover && emptyRounds >= DISCOVER_AFTER_EMPTY) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
831
|
+
// 等后台发现完成(如果正在跑)
|
|
832
|
+
if (discoverPromise) {
|
|
833
|
+
log(` ⏳ 等待后台发现完成...`);
|
|
834
|
+
await discoverPromise;
|
|
835
|
+
} else if (Date.now() - lastDiscoverTime >= DISCOVER_COOLDOWN_MS) {
|
|
836
|
+
// 冷却期已过,触发一次同步发现
|
|
837
|
+
log(`🔍 连续 ${emptyRounds} 轮无待打分标签,同步生成标签...`);
|
|
838
|
+
await runDiscoverInBackground();
|
|
839
|
+
await discoverPromise;
|
|
840
|
+
} else {
|
|
841
|
+
const remainMin = Math.ceil(
|
|
842
|
+
(DISCOVER_COOLDOWN_MS - (Date.now() - lastDiscoverTime)) /
|
|
843
|
+
60000,
|
|
844
|
+
);
|
|
845
|
+
log(
|
|
846
|
+
`🔍 连续 ${emptyRounds} 轮空,但发现冷却中(还需 ${remainMin} 分钟),继续等待...`,
|
|
847
|
+
);
|
|
795
848
|
}
|
|
796
|
-
emptyRounds = 0;
|
|
797
|
-
// 等 3 秒让服务端处理完
|
|
849
|
+
emptyRounds = 0;
|
|
798
850
|
await new Promise((r) => setTimeout(r, 3000));
|
|
799
851
|
continue;
|
|
800
852
|
}
|
package/src/lib/args.js
CHANGED
|
@@ -740,6 +740,7 @@ function parseTagArgs(args) {
|
|
|
740
740
|
let isScore = false;
|
|
741
741
|
let isScoreAll = false;
|
|
742
742
|
let scoreAllPort = 7222;
|
|
743
|
+
let discoverCountScoreAll = 10;
|
|
743
744
|
let scoreProxy = null;
|
|
744
745
|
let scoreTag = null;
|
|
745
746
|
let scoreCountries = null;
|
|
@@ -781,6 +782,8 @@ function parseTagArgs(args) {
|
|
|
781
782
|
discoverCount = parseInt(args[++i]) || 4;
|
|
782
783
|
} else if (arg === "--auto-discover") {
|
|
783
784
|
autoDiscover = true;
|
|
785
|
+
} else if (arg === "--discover-count") {
|
|
786
|
+
discoverCountScoreAll = parseInt(args[++i]) || 10;
|
|
784
787
|
} else if (arg === "--countries") {
|
|
785
788
|
scoreCountries = args[++i]
|
|
786
789
|
.split(",")
|
|
@@ -879,6 +882,7 @@ function parseTagArgs(args) {
|
|
|
879
882
|
countries: scoreCountries,
|
|
880
883
|
serverUrl,
|
|
881
884
|
autoDiscover,
|
|
885
|
+
discoverCount: discoverCountScoreAll,
|
|
882
886
|
port: scoreAllPort,
|
|
883
887
|
proxy: scoreProxy,
|
|
884
888
|
},
|
package/src/lib/constants.js
CHANGED
|
@@ -261,13 +261,14 @@ const HELP_TEXT = [
|
|
|
261
261
|
" --countries <CSV> 目标国家,逗号分隔(默认 13 个欧洲国家)",
|
|
262
262
|
" -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
|
|
263
263
|
" --port <N> 起始 CDP 端口,端口池默认为 N~N+9(默认 7222)",
|
|
264
|
-
" --discover
|
|
264
|
+
" --discover 开启自动发现:剩余 new 标签 < 5 时后台异步生成,连续 3 轮空兜底",
|
|
265
|
+
" --discover-count <N> 每次发现每个国家生成标签数(默认 10,上限 20)",
|
|
265
266
|
" 行为:",
|
|
266
267
|
" 连续 3 次 HTTP 403 → 判断账号被封 → 等 2 分钟后自动切换到池中另一端口",
|
|
267
268
|
" 环境变量:",
|
|
268
269
|
" TAG_SCOREALL_PORT_POOL_SIZE=N 端口池大小(默认 10)",
|
|
269
270
|
" TAG_SCOREALL_RECYCLE_RSS_MB=N 内存回收阈值 RSS MB(默认 900)",
|
|
270
|
-
" TAG_SCOREALL_RECYCLE_HEAP_MB=N 内存回收阈值 Heap MB(默认
|
|
271
|
+
" TAG_SCOREALL_RECYCLE_HEAP_MB=N 内存回收阈值 Heap MB(默认 512)",
|
|
271
272
|
" 示例: tt-help tag score-all",
|
|
272
273
|
" tt-help tag score-all --countries ES -s http://127.0.0.1:3001",
|
|
273
274
|
" tt-help tag score-all --port 9222 -s http://127.0.0.1:3001 --discover",
|
package/src/lib/tag-discover.js
CHANGED
|
@@ -66,16 +66,26 @@ async function discoverTagsForCountryCli(
|
|
|
66
66
|
|
|
67
67
|
// 从服务端获取历史 tag
|
|
68
68
|
const history = await fetchTagHistory(serverUrl, country);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const { productive, dead, allExisting: existingSet } = history;
|
|
70
|
+
const existingArr = Array.isArray(existingSet) ? existingSet : [];
|
|
71
|
+
|
|
72
|
+
// 使用统一的 prompt 组装(不传 allExisting,由应用层去重)
|
|
73
|
+
const prompt = buildDiscoverPrompt(
|
|
74
|
+
country,
|
|
75
|
+
count,
|
|
76
|
+
{ productive, dead },
|
|
77
|
+
userPrompt,
|
|
78
|
+
);
|
|
72
79
|
|
|
73
80
|
process.stderr.write(
|
|
74
|
-
` [LLM]
|
|
81
|
+
` [LLM] 正在生成标签 (国家: ${country}, 语言: ${getLang(country)}, 目标: ${count} 个)...\n`,
|
|
75
82
|
);
|
|
76
83
|
const content = await callLLM(prompt);
|
|
77
84
|
const tags = parseTagsFromResponse(content);
|
|
78
|
-
|
|
85
|
+
// 两阶段筛选:先内部去重,再过滤已存在,最后取 count 个
|
|
86
|
+
const unique = [...new Set(tags)]
|
|
87
|
+
.filter((t) => !existingArr.includes(t))
|
|
88
|
+
.slice(0, count);
|
|
79
89
|
|
|
80
90
|
process.stderr.write(
|
|
81
91
|
` [LLM] 生成 ${unique.length} 个标签: ${unique.join(", ")}\n`,
|
package/src/watch/db-tags.js
CHANGED
|
@@ -22,11 +22,19 @@ export function insertTag(tag, countries, source = "llm") {
|
|
|
22
22
|
return { inserted: false, error: "invalid tag" };
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
|
+
// ON CONFLICT: 合并 countries 数组,避免跨国家共享 tag 时信息丢失
|
|
25
26
|
const result = db
|
|
26
27
|
.prepare(
|
|
27
|
-
|
|
28
|
+
`INSERT INTO tags (tag, countries, source) VALUES (@tag, @countries, @source)
|
|
29
|
+
ON CONFLICT(tag) DO UPDATE SET countries = (
|
|
30
|
+
SELECT json_group_array(DISTINCT value) FROM (
|
|
31
|
+
SELECT value FROM json_each(countries)
|
|
32
|
+
UNION
|
|
33
|
+
SELECT value FROM json_each(@countries)
|
|
34
|
+
)
|
|
35
|
+
)`,
|
|
28
36
|
)
|
|
29
|
-
.run(normalized, JSON.stringify(countries), source);
|
|
37
|
+
.run({ tag: normalized, countries: JSON.stringify(countries), source });
|
|
30
38
|
return { inserted: result.changes > 0, tag: normalized };
|
|
31
39
|
} catch (e) {
|
|
32
40
|
return { inserted: false, error: e.message };
|
package/src/watch/server.js
CHANGED
|
@@ -1169,9 +1169,18 @@ export function startWatchServer(
|
|
|
1169
1169
|
tags = store.getAllTags(limit, offset, upperCountry);
|
|
1170
1170
|
}
|
|
1171
1171
|
|
|
1172
|
-
//
|
|
1172
|
+
// 获取总数(带国家过滤,按 status 返回对应计数)
|
|
1173
1173
|
const stats = store.getTagStats(upperCountry);
|
|
1174
|
-
|
|
1174
|
+
let total;
|
|
1175
|
+
if (stats) {
|
|
1176
|
+
if (status === "new") total = stats.new;
|
|
1177
|
+
else if (status === "productive") total = stats.productive;
|
|
1178
|
+
else if (status === "dead") total = stats.dead;
|
|
1179
|
+
else if (status === "scoring") total = stats.scoring;
|
|
1180
|
+
else total = stats.total;
|
|
1181
|
+
} else {
|
|
1182
|
+
total = tags.length;
|
|
1183
|
+
}
|
|
1175
1184
|
sendJSON(res, 200, { tags, total, offset, limit });
|
|
1176
1185
|
return;
|
|
1177
1186
|
}
|
package/src/watch/tag-service.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function callLLM(prompt) {
|
|
|
45
45
|
model: LLM_MODEL,
|
|
46
46
|
messages: [{ role: "user", content: prompt }],
|
|
47
47
|
max_tokens: 1024,
|
|
48
|
-
temperature: 0.
|
|
48
|
+
temperature: 0.7,
|
|
49
49
|
}),
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -182,13 +182,6 @@ export function buildDiscoverPrompt(
|
|
|
182
182
|
? `\nWhy previous tags failed: ${errorPatterns.join("; ")}. Avoid tags likely to have same issues.`
|
|
183
183
|
: "";
|
|
184
184
|
|
|
185
|
-
// 已存在的所有 tag(防止重复生成)
|
|
186
|
-
const allExisting = history.allExisting || [];
|
|
187
|
-
const existingHint =
|
|
188
|
-
allExisting.length > 0
|
|
189
|
-
? `\nTags already in database (DO NOT generate these again): ${allExisting.slice(0, 50).join(", ")}.`
|
|
190
|
-
: "";
|
|
191
|
-
|
|
192
185
|
const userHint = userPrompt
|
|
193
186
|
? `\nAdditional focus: ${userPrompt}. Generate tags specifically for this niche.`
|
|
194
187
|
: "";
|
|
@@ -244,7 +237,7 @@ Think about the MOST COMMON hashtags that real sellers in ${country} use on TikT
|
|
|
244
237
|
- Common seller identities (shop, boutique, small business, creator...)
|
|
245
238
|
- Well-known community tags (support local, marketplace, second round...)
|
|
246
239
|
|
|
247
|
-
${qualityWarning}${productiveHint}${deadHint}${errorHint}${
|
|
240
|
+
${qualityWarning}${productiveHint}${deadHint}${errorHint}${userHint}${strategyReview}
|
|
248
241
|
|
|
249
242
|
## Quality check before responding
|
|
250
243
|
|
|
@@ -255,15 +248,16 @@ For each tag you consider, ask yourself: "Have I actually seen this hashtag used
|
|
|
255
248
|
Return ONLY a JSON object with two fields:
|
|
256
249
|
{
|
|
257
250
|
"strategy": "Your analysis (2-3 sentences). Acknowledge what worked/failed before and explain why your chosen tags are likely real and commonly used.",
|
|
258
|
-
"tags": ["tag1", "tag2", "tag3", "tag4"]
|
|
251
|
+
"tags": ["tag1", "tag2", "tag3", "tag4", "..."]
|
|
259
252
|
}
|
|
260
253
|
|
|
261
254
|
Rules:
|
|
255
|
+
- Brainstorm broadly — generate 12-15 distinct, diverse tag ideas covering different categories (selling verbs, product types, community tags, marketplace terms, seasonal trends)
|
|
256
|
+
- We will filter duplicates and existing tags on our end, so explore freely
|
|
262
257
|
- QUALITY over creativity — 3 real, commonly used tags beat 4 invented ones
|
|
263
258
|
- NEVER invent compound words or translate concepts into hashtags
|
|
264
259
|
- Prefer simpler, more common tags over hyper-specific ones
|
|
265
|
-
- Look for tags with broad appeal (many potential posters)
|
|
266
|
-
- Do NOT generate tags that already exist`;
|
|
260
|
+
- Look for tags with broad appeal (many potential posters)`;
|
|
267
261
|
}
|
|
268
262
|
|
|
269
263
|
// ====== discover: 单国家标签发现 ======
|
|
@@ -281,10 +275,10 @@ export async function discoverTagsForCountry(
|
|
|
281
275
|
// 读取历史打分记录
|
|
282
276
|
const productive = store.getTagsByCountry(country, 50);
|
|
283
277
|
const dead = store.getDeadTags(country);
|
|
284
|
-
// 获取该国所有已存在的 tag
|
|
278
|
+
// 获取该国所有已存在的 tag 名(应用层去重用)
|
|
285
279
|
const allTags = store.getTagsByCountry(country, 0);
|
|
286
|
-
const allExisting = allTags.map((t) => t.tag);
|
|
287
|
-
const history = { productive, dead
|
|
280
|
+
const allExisting = new Set(allTags.map((t) => t.tag));
|
|
281
|
+
const history = { productive, dead };
|
|
288
282
|
|
|
289
283
|
// 读取历史策略效果(自学习)
|
|
290
284
|
const { getDiscoverRound, getDiscoverEffectiveness } =
|
|
@@ -305,9 +299,11 @@ export async function discoverTagsForCountry(
|
|
|
305
299
|
);
|
|
306
300
|
const content = await callLLM(prompt);
|
|
307
301
|
|
|
308
|
-
// 解析响应:提取 strategy 和 tags
|
|
302
|
+
// 解析响应:提取 strategy 和 tags,筛选掉数据库中已存在的
|
|
309
303
|
const { strategy, tags } = parseDiscoverResponse(content);
|
|
310
|
-
const unique = [...new Set(tags)]
|
|
304
|
+
const unique = [...new Set(tags)]
|
|
305
|
+
.filter((t) => !allExisting.has(t))
|
|
306
|
+
.slice(0, count);
|
|
311
307
|
|
|
312
308
|
console.error(
|
|
313
309
|
`[tag-service] LLM strategy: ${strategy?.substring(0, 150) || "(none)"}`,
|