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.
@@ -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
- * 为单个国家生成 tag(CLI 模式,通过 API 获取历史数据)
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
- // 使用统一的 prompt 组装(不传 allExisting,由应用层去重)
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
- userPrompt,
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} 个)...\n`,
250
+ ` [LLM] 正在生成标签 (国家: ${country}, 语言: ${getLang(country)}, 目标: ${count} 个, temp=${temperature}, health=${health?.healthStatus || "unknown"})...\n`,
82
251
  );
83
- const content = await callLLM(prompt);
84
- const tags = parseTagsFromResponse(content);
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
- return unique;
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 tags = await discoverTagsForCountryCli(
317
+ const result = await discoverTagsForCountryCli(
112
318
  country,
113
319
  count,
114
320
  userPrompt,
115
321
  serverUrl,
116
322
  );
117
- allTags.push(...tags);
323
+ if (result.tags) {
324
+ allTags.push(...result.tags);
325
+ }
118
326
  }
119
327
 
120
328
  return allTags;
@@ -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
  }
@@ -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);