tt-help-cli-ycl 1.3.93 → 1.3.94
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/comments.js +49 -24
- package/src/cli/tag.js +239 -91
- package/src/lib/args.js +23 -0
- package/src/lib/browser/cdp.js +4 -1
- package/src/lib/constants.js +15 -0
- package/src/lib/tag-fetcher.js +69 -63
- package/src/watch/data-store.js +631 -2399
- package/src/watch/data-store.js.bak +5091 -0
- package/src/watch/data-store.js.bak2 +5019 -0
- package/src/watch/db-columns.js +160 -0
- package/src/watch/db-crud.js +458 -0
- package/src/watch/db-mappers.js +128 -0
- package/src/watch/db-raw-jobs.js +235 -0
- package/src/watch/db-schema.js +367 -0
- package/src/watch/db-stats.js +235 -0
- package/src/watch/db-tags.js +348 -0
- package/src/watch/llm-scoring.js +235 -0
- package/src/watch/public/app.js +47 -0
- package/src/watch/public/index.html +6 -0
- package/src/watch/server.js +24 -0
- package/src/watch/tag-service.js +142 -11
package/src/watch/tag-service.js
CHANGED
|
@@ -79,9 +79,66 @@ export function parseTagsFromResponse(content) {
|
|
|
79
79
|
return tags;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* 解析 LLM 的两阶段响应(strategy + tags)
|
|
84
|
+
* 期望格式: {"strategy": "...", "tags": [...]}
|
|
85
|
+
* 兼容旧格式: 纯数组或纯文本
|
|
86
|
+
*/
|
|
87
|
+
export function parseDiscoverResponse(content) {
|
|
88
|
+
let strategy = null;
|
|
89
|
+
let tags = [];
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(content);
|
|
93
|
+
|
|
94
|
+
// 新格式: { strategy, tags }
|
|
95
|
+
if (parsed.strategy && Array.isArray(parsed.tags)) {
|
|
96
|
+
strategy = parsed.strategy;
|
|
97
|
+
tags = parsed.tags.map(normalizeTag).filter((t) => t && t.length >= 2);
|
|
98
|
+
return { strategy, tags };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 旧格式: 纯数组
|
|
102
|
+
if (Array.isArray(parsed)) {
|
|
103
|
+
tags = parsed.map(normalizeTag).filter((t) => t && t.length >= 2);
|
|
104
|
+
return { strategy, tags };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 旧格式: { tags: [...] }
|
|
108
|
+
if (Array.isArray(parsed.tags)) {
|
|
109
|
+
tags = parsed.tags.map(normalizeTag).filter((t) => t && t.length >= 2);
|
|
110
|
+
return { strategy, tags };
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// JSON 解析失败,尝试从文本中提取
|
|
114
|
+
// 先尝试提取 strategy(可能在 JSON 块之前)
|
|
115
|
+
const strategyMatch = content.match(/"strategy"\s*:\s*"([^"]+)"/);
|
|
116
|
+
if (strategyMatch) {
|
|
117
|
+
strategy = strategyMatch[1];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 降级:从文本行中提取 tags
|
|
122
|
+
const lines = content.split(/[\n,]+/);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const cleaned = normalizeTag(line.replace(/^[-\d.\s]+/, ""));
|
|
125
|
+
if (cleaned && /^[a-z0-9_]+$/.test(cleaned) && cleaned.length >= 2) {
|
|
126
|
+
tags.push(cleaned);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { strategy, tags };
|
|
131
|
+
}
|
|
132
|
+
|
|
82
133
|
// ====== Prompt 组装 ======
|
|
83
134
|
|
|
84
|
-
export function buildDiscoverPrompt(
|
|
135
|
+
export function buildDiscoverPrompt(
|
|
136
|
+
country,
|
|
137
|
+
count,
|
|
138
|
+
history,
|
|
139
|
+
userPrompt,
|
|
140
|
+
pastStrategies,
|
|
141
|
+
) {
|
|
85
142
|
const lang = getLang(country);
|
|
86
143
|
const langNames = {
|
|
87
144
|
cs: "Czech",
|
|
@@ -98,11 +155,15 @@ export function buildDiscoverPrompt(country, count, history, userPrompt) {
|
|
|
98
155
|
};
|
|
99
156
|
const langName = langNames[lang] || lang;
|
|
100
157
|
|
|
101
|
-
// 正样本:该国高分 tag
|
|
158
|
+
// 正样本:该国高分 tag(带分数,让 LLM 知道哪些方向最有效)
|
|
102
159
|
const productive = history.productive || [];
|
|
103
160
|
const productiveHint =
|
|
104
161
|
productive.length > 0
|
|
105
|
-
? `\nTags that already worked well for ${country}: ${productive
|
|
162
|
+
? `\nTags that already worked well for ${country} (with scores): ${productive
|
|
163
|
+
.map((t) => `${t.tag}(score:${Math.round(t.score)})`)
|
|
164
|
+
.join(
|
|
165
|
+
", ",
|
|
166
|
+
)}. These are examples of what works — explore DIFFERENT directions, not variations of these.`
|
|
106
167
|
: "";
|
|
107
168
|
|
|
108
169
|
// 负样本:该国 dead tag
|
|
@@ -132,6 +193,23 @@ export function buildDiscoverPrompt(country, count, history, userPrompt) {
|
|
|
132
193
|
? `\nAdditional focus: ${userPrompt}. Generate tags specifically for this niche.`
|
|
133
194
|
: "";
|
|
134
195
|
|
|
196
|
+
// 历史策略反馈:让 LLM 知道之前的策略效果如何
|
|
197
|
+
let strategyReview = "";
|
|
198
|
+
if (pastStrategies && pastStrategies.length > 0) {
|
|
199
|
+
const strategyLines = pastStrategies
|
|
200
|
+
.map(
|
|
201
|
+
(s) =>
|
|
202
|
+
`Round ${s.round}: strategy="${s.strategy?.substring(0, 200)}", added ${s.tags_added} tags, avg_score=${(s.avg_score || 0).toFixed(1)}, productive=${s.productive_count || 0}, dead=${s.dead_count || 0}`,
|
|
203
|
+
)
|
|
204
|
+
.join("\n");
|
|
205
|
+
strategyReview = `
|
|
206
|
+
=== Previous discovery rounds (learn from these results) ===
|
|
207
|
+
${strategyLines}
|
|
208
|
+
|
|
209
|
+
Based on the above, which strategies produced high-scoring tags? Which failed?
|
|
210
|
+
Use this analysis to decide your strategy for this round.`;
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
return `You are discovering TikTok hashtags used by people who sell things in ${country}.
|
|
136
214
|
|
|
137
215
|
Your goal: Find hashtags that real sellers in ${country} actually use — any kind of tag they might use. Think broadly:
|
|
@@ -139,16 +217,32 @@ Your goal: Find hashtags that real sellers in ${country} actually use — any ki
|
|
|
139
217
|
- What they sell (shoes, clothes, jewelry, food, pets, furniture...)
|
|
140
218
|
- How they sell (online, handmade, second-hand, local pickup...)
|
|
141
219
|
- Product-specific tags (sneakers, dresses, cakes, necklaces...)
|
|
220
|
+
- Niche categories: beauty, fitness, pets, plants, books, toys, music, art, photography, gardening, DIY, automotive, real estate...
|
|
142
221
|
|
|
143
222
|
All tags must be in ${langName} language (or widely used in ${country}).
|
|
144
|
-
Generate ${count} tags that are ALL DIFFERENT from each other and from any existing tags
|
|
223
|
+
Generate ${count} tags that are ALL DIFFERENT from each other and from any existing tags.${productiveHint}${deadHint}${errorHint}${existingHint}${userHint}${strategyReview}
|
|
224
|
+
|
|
225
|
+
## IMPORTANT: Think first, then generate
|
|
226
|
+
|
|
227
|
+
Before generating tags, analyze:
|
|
228
|
+
1. Which tag directions scored highest? What makes them work?
|
|
229
|
+
2. Which directions completely failed? Why?
|
|
230
|
+
3. What seller niches are NOT yet covered? (e.g., if we have "shop" but no "bakery", "petstore", "bookshop"...)
|
|
231
|
+
4. What specific direction will YOU explore this round? Be concrete.
|
|
232
|
+
|
|
233
|
+
## Output format
|
|
234
|
+
|
|
235
|
+
Return ONLY a JSON object with two fields:
|
|
236
|
+
{
|
|
237
|
+
"strategy": "Your analysis and plan for this round (2-4 sentences). Explain what direction you're exploring and why.",
|
|
238
|
+
"tags": ["tag1", "tag2", "tag3", "tag4"]
|
|
239
|
+
}
|
|
145
240
|
|
|
146
241
|
Rules:
|
|
147
242
|
- Each tag should explore a DIFFERENT angle — don't just swap country suffixes
|
|
148
243
|
- Prefer specific and niche tags over generic ones (e.g., "vendozapatos" beats "vender")
|
|
149
|
-
- Do NOT generate tags that already exist
|
|
150
|
-
|
|
151
|
-
Return ONLY a JSON array of tag strings, nothing else. Example: ["ventas","tiendaonline","vender"]`;
|
|
244
|
+
- Do NOT generate tags that already exist
|
|
245
|
+
- Your strategy should be different from previous rounds — if a direction worked, go deeper; if it failed, try something new`;
|
|
152
246
|
}
|
|
153
247
|
|
|
154
248
|
// ====== discover: 单国家标签发现 ======
|
|
@@ -171,15 +265,32 @@ export async function discoverTagsForCountry(
|
|
|
171
265
|
const allExisting = allTags.map((t) => t.tag);
|
|
172
266
|
const history = { productive, dead, allExisting };
|
|
173
267
|
|
|
174
|
-
//
|
|
175
|
-
const
|
|
268
|
+
// 读取历史策略效果(自学习)
|
|
269
|
+
const { getDiscoverRound, getDiscoverEffectiveness } =
|
|
270
|
+
await import("./db-tags.js");
|
|
271
|
+
const round = getDiscoverRound(country);
|
|
272
|
+
const pastStrategies = getDiscoverEffectiveness(country);
|
|
273
|
+
|
|
274
|
+
// 组装 prompt 并调用 LLM(两阶段:先思考策略,再生成 tag)
|
|
275
|
+
const prompt = buildDiscoverPrompt(
|
|
276
|
+
country,
|
|
277
|
+
count,
|
|
278
|
+
history,
|
|
279
|
+
userPrompt,
|
|
280
|
+
pastStrategies,
|
|
281
|
+
);
|
|
176
282
|
console.error(
|
|
177
|
-
`[tag-service] LLM discover ${country} (lang=${getLang(country)}, count=${count})`,
|
|
283
|
+
`[tag-service] LLM discover ${country} round=${round} (lang=${getLang(country)}, count=${count})`,
|
|
178
284
|
);
|
|
179
285
|
const content = await callLLM(prompt);
|
|
180
|
-
|
|
286
|
+
|
|
287
|
+
// 解析响应:提取 strategy 和 tags
|
|
288
|
+
const { strategy, tags } = parseDiscoverResponse(content);
|
|
181
289
|
const unique = [...new Set(tags)].slice(0, count);
|
|
182
290
|
|
|
291
|
+
console.error(
|
|
292
|
+
`[tag-service] LLM strategy: ${strategy?.substring(0, 150) || "(none)"}`,
|
|
293
|
+
);
|
|
183
294
|
console.error(
|
|
184
295
|
`[tag-service] LLM generated ${unique.length} tags: ${unique.join(", ")}`,
|
|
185
296
|
);
|
|
@@ -191,8 +302,28 @@ export async function discoverTagsForCountry(
|
|
|
191
302
|
if (result.inserted) inserted.push(result.tag);
|
|
192
303
|
}
|
|
193
304
|
|
|
305
|
+
// 记录 discover log(策略 + 结果)
|
|
306
|
+
const { insertDiscoverLog } = await import("./db-tags.js");
|
|
307
|
+
insertDiscoverLog({
|
|
308
|
+
country,
|
|
309
|
+
round,
|
|
310
|
+
strategy,
|
|
311
|
+
tagsGenerated: unique,
|
|
312
|
+
tagsAdded: inserted.length,
|
|
313
|
+
productiveSample: productive.slice(0, 20).map((t) => t.tag),
|
|
314
|
+
deadSample: dead.slice(0, 20).map((t) => t.tag),
|
|
315
|
+
avgProductiveScore:
|
|
316
|
+
productive.length > 0
|
|
317
|
+
? productive.reduce((s, t) => s + t.score, 0) / productive.length
|
|
318
|
+
: 0,
|
|
319
|
+
avgDeadScore:
|
|
320
|
+
dead.length > 0 ? dead.reduce((s, t) => s + t.score, 0) / dead.length : 0,
|
|
321
|
+
});
|
|
322
|
+
|
|
194
323
|
return {
|
|
195
324
|
country,
|
|
325
|
+
round,
|
|
326
|
+
strategy,
|
|
196
327
|
added: inserted.length,
|
|
197
328
|
tags: inserted,
|
|
198
329
|
total: unique.length,
|