tt-help-cli-ycl 1.4.3 → 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.
- package/package.json +1 -1
- package/src/cli/tag.js +128 -116
- package/src/lib/args.js +0 -8
- package/src/lib/tag-discover.js +218 -10
- package/src/watch/data-store.js +42 -0
- package/src/watch/db-schema.js +19 -0
- package/src/watch/db-tags.js +749 -0
- package/src/watch/public/app.js +27 -0
- package/src/watch/public/index.html +1 -1
- package/src/watch/server.js +244 -0
- package/src/watch/tag-service.js +509 -23
package/src/lib/tag-discover.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 使用 tag-service 的公共函数(LLM 调用、prompt 组装、解析)。
|
|
5
5
|
* 历史 tag 数据通过 API 从服务端获取,不再读写 productive-tags.json。
|
|
6
|
+
* 自闭环:从服务端获取健康数据 → 自动 niche 切换 → 动态温度调整
|
|
6
7
|
*/
|
|
7
8
|
import {
|
|
8
9
|
COUNTRY_LANG,
|
|
@@ -10,7 +11,9 @@ import {
|
|
|
10
11
|
callLLM,
|
|
11
12
|
normalizeTag,
|
|
12
13
|
parseTagsFromResponse,
|
|
14
|
+
parseDiscoverResponse,
|
|
13
15
|
buildDiscoverPrompt,
|
|
16
|
+
reflectAndLearn,
|
|
14
17
|
} from "../watch/tag-service.js";
|
|
15
18
|
|
|
16
19
|
const DEFAULT_SERVER = "http://127.0.0.1:3000";
|
|
@@ -52,7 +55,87 @@ async function fetchTagHistory(serverUrl, country) {
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
|
-
*
|
|
58
|
+
* 从服务端获取某国的 tag 健康指标
|
|
59
|
+
*/
|
|
60
|
+
async function fetchCountryHealth(serverUrl, country) {
|
|
61
|
+
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${baseUrl}/api/tags/health?country=${country}`);
|
|
64
|
+
if (!res.ok) return null;
|
|
65
|
+
return await res.json();
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 从服务端触发数据清理(自闭环)
|
|
73
|
+
*/
|
|
74
|
+
async function triggerCleanup(serverUrl, actions = {}) {
|
|
75
|
+
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(`${baseUrl}/api/tags/cleanup`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify(actions),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) return {};
|
|
83
|
+
return await res.json();
|
|
84
|
+
} catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 从服务端获取某国的活跃反思规则
|
|
91
|
+
*/
|
|
92
|
+
async function fetchActiveRules(serverUrl, country) {
|
|
93
|
+
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${baseUrl}/api/tags/rules?country=${country}`);
|
|
96
|
+
if (!res.ok) return [];
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
return data.rules || [];
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 从服务端触发反思(自闭环)
|
|
106
|
+
*/
|
|
107
|
+
async function triggerReflection(serverUrl, country) {
|
|
108
|
+
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(`${baseUrl}/api/tags/reflect?country=${country}`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) return { reflected: false, error: `HTTP ${res.status}` };
|
|
114
|
+
return await res.json();
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return { reflected: false, error: e.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 从服务端获取最近生成的标签(去重)
|
|
122
|
+
*/
|
|
123
|
+
async function fetchRecentGenerated(serverUrl, country, lookback = 10) {
|
|
124
|
+
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(
|
|
127
|
+
`${baseUrl}/api/tags/recent-generated?country=${country}&lookback=${lookback}`,
|
|
128
|
+
);
|
|
129
|
+
if (!res.ok) return [];
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
return data.tags || [];
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 为单个国家生成 tag(CLI 模式,通过 API 获取历史数据 + 自闭环)
|
|
56
139
|
*/
|
|
57
140
|
async function discoverTagsForCountryCli(
|
|
58
141
|
country,
|
|
@@ -64,24 +147,112 @@ async function discoverTagsForCountryCli(
|
|
|
64
147
|
return { country, error: `不支持的国家代码: ${country}` };
|
|
65
148
|
}
|
|
66
149
|
|
|
150
|
+
// ====== 自闭环 Step 1: 获取健康数据 ======
|
|
151
|
+
const health = await fetchCountryHealth(serverUrl, country);
|
|
152
|
+
let nicheRecommendation = null;
|
|
153
|
+
let autoNichePrompt = null;
|
|
154
|
+
let temperature = 0.7;
|
|
155
|
+
|
|
156
|
+
if (health) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
` 🏥 ${country} health: ${health.healthStatus} | deadRatio=${health.deadRatio}% | productiveRatio=${health.productiveRatio}% | saturation=${health.saturation}%\n`,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// 动态温度调整
|
|
162
|
+
if (health.healthStatus === "critical") {
|
|
163
|
+
temperature = 0.3;
|
|
164
|
+
} else if (health.healthStatus === "warning") {
|
|
165
|
+
temperature = 0.5;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 自动 niche 切换
|
|
169
|
+
nicheRecommendation = health.nicheRecommendation;
|
|
170
|
+
if (
|
|
171
|
+
health.healthStatus !== "healthy" &&
|
|
172
|
+
nicheRecommendation &&
|
|
173
|
+
nicheRecommendation.uncovered &&
|
|
174
|
+
nicheRecommendation.uncovered.length > 0
|
|
175
|
+
) {
|
|
176
|
+
const targetNiche = nicheRecommendation.uncovered[0];
|
|
177
|
+
const nicheDescriptions = {
|
|
178
|
+
fashion:
|
|
179
|
+
"clothing, shoes, accessories, vintage fashion, secondhand clothes",
|
|
180
|
+
food: "bakery, food delivery, local food, restaurant, cooking supplies",
|
|
181
|
+
handmade_crafts:
|
|
182
|
+
"handmade items, DIY crafts, ceramics, candles, resin art",
|
|
183
|
+
home_living:
|
|
184
|
+
"home decoration, furniture, interior design, cleaning services",
|
|
185
|
+
electronics_tech:
|
|
186
|
+
"electronics, gadgets, phones, computers, tech accessories",
|
|
187
|
+
pets_animals: "pet supplies, dog accessories, cat food, animal care",
|
|
188
|
+
beauty_health:
|
|
189
|
+
"beauty products, cosmetics, skincare, hair care, wellness",
|
|
190
|
+
books_collectibles:
|
|
191
|
+
"books, collectibles, vinyl records, trading cards, antiques",
|
|
192
|
+
services_professional:
|
|
193
|
+
"professional services, repair, cleaning, photography, florist",
|
|
194
|
+
marketplace_local:
|
|
195
|
+
"local market, marketplace, shop, boutique, online store",
|
|
196
|
+
};
|
|
197
|
+
autoNichePrompt = `Focus on the "${targetNiche.niche}" niche: ${nicheDescriptions[targetNiche.niche] || targetNiche.niche}. We have only ${targetNiche.currentCoverage} productive tags in this category — explore more hashtags in this direction.`;
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
` 🎯 ${country} auto niche switch → ${targetNiche.niche} (coverage: ${targetNiche.currentCoverage})\n`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 触发服务端数据清理
|
|
204
|
+
const cleanupActions = {};
|
|
205
|
+
if (health.lowScored > health.productive * 2) {
|
|
206
|
+
cleanupActions.lowScoredCleanup = true;
|
|
207
|
+
}
|
|
208
|
+
cleanupActions.staleScoringReset = true;
|
|
209
|
+
cleanupActions.errorReset = true;
|
|
210
|
+
|
|
211
|
+
const cleanupResult = await triggerCleanup(serverUrl, cleanupActions);
|
|
212
|
+
if (cleanupResult && Object.keys(cleanupResult).length > 0) {
|
|
213
|
+
process.stderr.write(` 🧹 Cleanup: ${JSON.stringify(cleanupResult)}\n`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 合并用户 prompt 和自动 niche prompt
|
|
218
|
+
const effectiveUserPrompt = autoNichePrompt || userPrompt;
|
|
219
|
+
|
|
220
|
+
// ====== 原有 discover 流程 ======
|
|
67
221
|
// 从服务端获取历史 tag
|
|
68
222
|
const history = await fetchTagHistory(serverUrl, country);
|
|
69
223
|
const { productive, dead, allExisting: existingSet } = history;
|
|
70
224
|
const existingArr = Array.isArray(existingSet) ? existingSet : [];
|
|
71
225
|
|
|
72
|
-
//
|
|
226
|
+
// 获取最近生成的标签(用于检测重复)
|
|
227
|
+
const recentGenerated = await fetchRecentGenerated(serverUrl, country);
|
|
228
|
+
|
|
229
|
+
// ====== 自闭环 Step 4: 获取硬规则(反思产出) ======
|
|
230
|
+
const hardRules = await fetchActiveRules(serverUrl, country);
|
|
231
|
+
if (hardRules.length > 0) {
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
` ⛔ ${country} 注入 ${hardRules.length} 条硬规则到 prompt\n`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 使用统一的 prompt 组装(加入健康指标、niche 推荐、硬规则)
|
|
73
238
|
const prompt = buildDiscoverPrompt(
|
|
74
239
|
country,
|
|
75
240
|
count,
|
|
76
|
-
{ productive, dead },
|
|
77
|
-
|
|
241
|
+
{ productive, dead, recentGenerated },
|
|
242
|
+
effectiveUserPrompt,
|
|
243
|
+
null, // pastStrategies(CLI 模式暂不传)
|
|
244
|
+
health,
|
|
245
|
+
nicheRecommendation,
|
|
246
|
+
hardRules,
|
|
78
247
|
);
|
|
79
248
|
|
|
80
249
|
process.stderr.write(
|
|
81
|
-
` [LLM] 正在生成标签 (国家: ${country}, 语言: ${getLang(country)}, 目标: ${count}
|
|
250
|
+
` [LLM] 正在生成标签 (国家: ${country}, 语言: ${getLang(country)}, 目标: ${count} 个, temp=${temperature}, health=${health?.healthStatus || "unknown"})...\n`,
|
|
82
251
|
);
|
|
83
|
-
const content = await callLLM(prompt);
|
|
84
|
-
const
|
|
252
|
+
const content = await callLLM(prompt, temperature);
|
|
253
|
+
const parsed = parseDiscoverResponse(content);
|
|
254
|
+
const tags = parsed.tags || parseTagsFromResponse(content);
|
|
255
|
+
const niche = parsed.niche || null;
|
|
85
256
|
// 两阶段筛选:先内部去重,再过滤已存在,最后取 count 个
|
|
86
257
|
const unique = [...new Set(tags)]
|
|
87
258
|
.filter((t) => !existingArr.includes(t))
|
|
@@ -90,7 +261,42 @@ async function discoverTagsForCountryCli(
|
|
|
90
261
|
process.stderr.write(
|
|
91
262
|
` [LLM] 生成 ${unique.length} 个标签: ${unique.join(", ")}\n`,
|
|
92
263
|
);
|
|
93
|
-
|
|
264
|
+
if (niche) {
|
|
265
|
+
process.stderr.write(` [LLM] niche focus: ${niche}\n`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ====== 自闭环 Step 5: 反思触发(本轮结束后检查) ======
|
|
269
|
+
let reflectionResult = null;
|
|
270
|
+
try {
|
|
271
|
+
reflectionResult = await triggerReflection(serverUrl, country);
|
|
272
|
+
if (reflectionResult.reflected) {
|
|
273
|
+
process.stderr.write(
|
|
274
|
+
` 🔄 ${country} 反思完成: 新增 ${reflectionResult.rulesAdded} 条规则\n`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
process.stderr.write(` 反思异常: ${e.message}\n`);
|
|
279
|
+
reflectionResult = { reflected: false, error: e.message };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
country,
|
|
284
|
+
tags: unique,
|
|
285
|
+
niche,
|
|
286
|
+
health: health
|
|
287
|
+
? {
|
|
288
|
+
status: health.healthStatus,
|
|
289
|
+
deadRatio: health.deadRatio,
|
|
290
|
+
productiveRatio: health.productiveRatio,
|
|
291
|
+
saturation: health.saturation,
|
|
292
|
+
issues: health.issues || [],
|
|
293
|
+
}
|
|
294
|
+
: null,
|
|
295
|
+
autoNiche: autoNichePrompt ? nicheRecommendation.uncovered[0].niche : null,
|
|
296
|
+
temperature,
|
|
297
|
+
hardRulesInjected: hardRules.length,
|
|
298
|
+
reflection: reflectionResult,
|
|
299
|
+
};
|
|
94
300
|
}
|
|
95
301
|
|
|
96
302
|
/**
|
|
@@ -108,13 +314,15 @@ export async function discoverTags(countries, options = {}) {
|
|
|
108
314
|
const allTags = [];
|
|
109
315
|
|
|
110
316
|
for (const country of countryList) {
|
|
111
|
-
const
|
|
317
|
+
const result = await discoverTagsForCountryCli(
|
|
112
318
|
country,
|
|
113
319
|
count,
|
|
114
320
|
userPrompt,
|
|
115
321
|
serverUrl,
|
|
116
322
|
);
|
|
117
|
-
|
|
323
|
+
if (result.tags) {
|
|
324
|
+
allTags.push(...result.tags);
|
|
325
|
+
}
|
|
118
326
|
}
|
|
119
327
|
|
|
120
328
|
return allTags;
|
package/src/watch/data-store.js
CHANGED
|
@@ -3203,6 +3203,7 @@ export function createStore(filePath, options = {}) {
|
|
|
3203
3203
|
trackClient,
|
|
3204
3204
|
getActiveClients,
|
|
3205
3205
|
moveSellerJobsToBase, // 将 jobs/raw_jobs 中 tt_seller=1 且 video_count=0 的记录移动到 jobs_base
|
|
3206
|
+
moveRestrictedJobsToBase, // 将 jobs 中 status='restricted' 的记录移动到 jobs_base
|
|
3206
3207
|
registerVideos,
|
|
3207
3208
|
getVideo,
|
|
3208
3209
|
getVideos,
|
|
@@ -3354,4 +3355,45 @@ export function createStore(filePath, options = {}) {
|
|
|
3354
3355
|
availableInBase: available,
|
|
3355
3356
|
};
|
|
3356
3357
|
}
|
|
3358
|
+
|
|
3359
|
+
function moveRestrictedJobsToBase() {
|
|
3360
|
+
const db = getDb();
|
|
3361
|
+
if (!db) return { ok: false, error: "db not ready" };
|
|
3362
|
+
|
|
3363
|
+
const now = Date.now();
|
|
3364
|
+
const countRow = db
|
|
3365
|
+
.prepare("SELECT COUNT(*) as c FROM jobs WHERE status = 'restricted'")
|
|
3366
|
+
.get();
|
|
3367
|
+
if (!countRow?.c) return { ok: true, moved: 0 };
|
|
3368
|
+
|
|
3369
|
+
db.prepare(
|
|
3370
|
+
`
|
|
3371
|
+
INSERT OR REPLACE INTO jobs_base (
|
|
3372
|
+
unique_id, nickname, sources, pinned, no_video, user_update_count,
|
|
3373
|
+
tt_seller, verified, video_count, comment_count, guessed_location,
|
|
3374
|
+
location_created, follower_count, following_count, heart_count,
|
|
3375
|
+
refresh_time, region, signature, sec_uid, status_code,
|
|
3376
|
+
latest_video_time, bio_link, status, restricted, error, claimed_by,
|
|
3377
|
+
claimed_at, processed, processed_at, created_at, updated_at
|
|
3378
|
+
)
|
|
3379
|
+
SELECT
|
|
3380
|
+
unique_id, nickname, sources, pinned, no_video, 0,
|
|
3381
|
+
CASE WHEN tt_seller = 0 THEN NULL ELSE tt_seller END,
|
|
3382
|
+
verified, video_count, comment_count, guessed_location,
|
|
3383
|
+
location_created, follower_count, following_count, heart_count,
|
|
3384
|
+
refresh_time, region, signature, sec_uid, status_code,
|
|
3385
|
+
latest_video_time, bio_link,
|
|
3386
|
+
'pending', 0, NULL, NULL, NULL, 0, NULL,
|
|
3387
|
+
COALESCE(created_at, ?), ?
|
|
3388
|
+
FROM jobs WHERE status = 'restricted'
|
|
3389
|
+
`,
|
|
3390
|
+
).run(now, now);
|
|
3391
|
+
|
|
3392
|
+
const deleted = db
|
|
3393
|
+
.prepare("DELETE FROM jobs WHERE status = 'restricted'")
|
|
3394
|
+
.run();
|
|
3395
|
+
|
|
3396
|
+
markStatsDirty();
|
|
3397
|
+
return { ok: true, moved: deleted.changes || 0 };
|
|
3398
|
+
}
|
|
3357
3399
|
}
|
package/src/watch/db-schema.js
CHANGED
|
@@ -297,6 +297,24 @@ export function initDb(filePath) {
|
|
|
297
297
|
)
|
|
298
298
|
`);
|
|
299
299
|
|
|
300
|
+
// ===== reflection_rules 表(反思闭环:LLM 自我学习规则) =====
|
|
301
|
+
db.exec(`
|
|
302
|
+
CREATE TABLE IF NOT EXISTS reflection_rules (
|
|
303
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
304
|
+
country TEXT NOT NULL,
|
|
305
|
+
rule_type TEXT NOT NULL DEFAULT 'hard',
|
|
306
|
+
rule_text TEXT NOT NULL,
|
|
307
|
+
source TEXT NOT NULL DEFAULT 'reflection',
|
|
308
|
+
trigger_reason TEXT,
|
|
309
|
+
productive_sample TEXT,
|
|
310
|
+
dead_sample TEXT,
|
|
311
|
+
analysis_summary TEXT,
|
|
312
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
313
|
+
superseded_at TEXT,
|
|
314
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
315
|
+
)
|
|
316
|
+
`);
|
|
317
|
+
|
|
300
318
|
// ===== 索引 =====
|
|
301
319
|
const indexes = [
|
|
302
320
|
`CREATE INDEX IF NOT EXISTS idx_jobs_status_video ON jobs(status, video_count DESC)`,
|
|
@@ -315,6 +333,7 @@ export function initDb(filePath) {
|
|
|
315
333
|
`CREATE INDEX IF NOT EXISTS idx_tags_status ON tags(status)`,
|
|
316
334
|
`CREATE INDEX IF NOT EXISTS idx_tags_score ON tags(score DESC)`,
|
|
317
335
|
`CREATE INDEX IF NOT EXISTS idx_discover_log_country ON discover_log(country, round DESC)`,
|
|
336
|
+
`CREATE INDEX IF NOT EXISTS idx_reflection_rules_active ON reflection_rules(country, is_active)`,
|
|
318
337
|
];
|
|
319
338
|
for (const sql of indexes) {
|
|
320
339
|
db.exec(sql);
|