tt-help-cli-ycl 1.3.95 → 1.3.97

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.
@@ -57,6 +57,10 @@
57
57
  <div class="label">目标商家</div>
58
58
  <div class="value target" id="statTarget">0</div>
59
59
  </div>
60
+ <div class="stat-card clickable pending-card" id="statTagsCard" onclick="navigateToTags()" style="cursor:pointer">
61
+ <div class="label">关键词</div>
62
+ <div class="value target" id="statTags">0</div>
63
+ </div>
60
64
  </div>
61
65
  <div id="activeClientsSection" class="active-clients-section" style="display:none">
62
66
  <div class="active-clients-bar" id="activeClientsBar"></div>
@@ -351,6 +355,7 @@
351
355
  <th class="sortable-target" data-sort="topVideoPlayCount">最大播放量 <span class="sort-icon">↕</span></th>
352
356
  <th class="sortable-target" data-sort="latestVideoTime">最近发布 <span class="sort-icon">↕</span></th>
353
357
  <th>最近刷新</th>
358
+ <th>操作</th>
354
359
  </tr>
355
360
  </thead>
356
361
  <tbody id="targetTable"></tbody>
@@ -362,6 +367,77 @@
362
367
  </div>
363
368
  </div>
364
369
  </div>
370
+ <div id="tagsPage">
371
+ <div class="stats" style="margin-bottom:16px">
372
+ <div class="stat-card">
373
+ <div class="label">关键词总数</div>
374
+ <div class="value target" id="tagsPageStatTotal">0</div>
375
+ </div>
376
+ <div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
377
+ <div class="label">← 返回主页面</div>
378
+ </div>
379
+ <div class="stat-card">
380
+ <div class="label">有效</div>
381
+ <div class="value done" id="tagsPageStatProductive">0</div>
382
+ </div>
383
+ <div class="stat-card">
384
+ <div class="label">无效</div>
385
+ <div class="value error" id="tagsPageStatDead">0</div>
386
+ </div>
387
+ <div class="stat-card">
388
+ <div class="label">待打分</div>
389
+ <div class="value pending" id="tagsPageStatNew">0</div>
390
+ </div>
391
+ <div class="stat-card clickable" id="refreshTagsBtn" style="background:rgba(59,130,246,0.12);cursor:pointer">
392
+ <div class="label">🔄 刷新</div>
393
+ </div>
394
+ </div>
395
+ <div class="tags-layout">
396
+ <div class="tags-table-card">
397
+ <h3>关键词列表</h3>
398
+ <div class="controls">
399
+ <button onclick="openAddTagModal()"
400
+ style="padding:6px 12px;border:1px solid #a78bfa;border-radius:6px;background:rgba(167,139,250,0.12);color:#a78bfa;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:all 0.15s">+
401
+ 添加关键词</button>
402
+ <input type="text" id="tagsSearchInput" placeholder="搜索关键词...">
403
+ <button data-tags-filter="all" class="active" onclick="setTagsFilter('all')">全部</button>
404
+ <button data-tags-filter="new" onclick="setTagsFilter('new')">待打分</button>
405
+ <button data-tags-filter="scoring" onclick="setTagsFilter('scoring')">打分中</button>
406
+ <button data-tags-filter="productive" onclick="setTagsFilter('productive')"
407
+ style="background:#166534;color:#fff">有效</button>
408
+ <button data-tags-filter="dead" onclick="setTagsFilter('dead')"
409
+ style="background:#7f1d1d;color:#fff">无效</button>
410
+ <select id="tagsCountryFilter"
411
+ style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
412
+ <option value="">全部国家</option>
413
+ </select>
414
+ </div>
415
+ <div class="table-scroll">
416
+ <table>
417
+ <thead>
418
+ <tr>
419
+ <th style="width:40px">#</th>
420
+ <th style="min-width:120px">关键词</th>
421
+ <th class="sortable-tag" data-sort="score">评分 <span class="sort-icon">↕</span></th>
422
+ <th class="sortable-tag" data-sort="author_count">作者数 <span class="sort-icon">↕</span></th>
423
+ <th class="sortable-tag" data-sort="total_posts">帖子数 <span class="sort-icon">↕</span></th>
424
+ <th class="sortable-tag" data-sort="matched_authors">匹配作者 <span class="sort-icon">↕</span></th>
425
+ <th class="sortable-tag" data-sort="pushed_users">推送用户 <span class="sort-icon">↕</span></th>
426
+ <th>国家</th>
427
+ <th>状态</th>
428
+ <th>来源</th>
429
+ <th>创建时间</th>
430
+ </tr>
431
+ </thead>
432
+ <tbody id="tagsTable"></tbody>
433
+ </table>
434
+ <div id="tagsMoreHint" onclick="loadMoreTags()"
435
+ style="text-align:center;padding:10px 8px;font-size:13px;color:#6b7280;cursor:default;user-select:none;transition:color 0.2s">
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
365
441
  <script src="app.js"></script>
366
442
  </body>
367
443
 
@@ -1389,3 +1389,153 @@ td.user-id:hover {
1389
1389
  height: 140px;
1390
1390
  }
1391
1391
  }
1392
+
1393
+ /* 自定义国家输入行 */
1394
+ .custom-loc-row {
1395
+ margin-top: 12px;
1396
+ }
1397
+
1398
+ .custom-loc-input {
1399
+ width: 100%;
1400
+ padding: 10px 12px;
1401
+ border: 1px solid #333;
1402
+ border-radius: 6px;
1403
+ background: #0f0f13;
1404
+ color: #e0e0e0;
1405
+ font-size: 13px;
1406
+ font-weight: 600;
1407
+ outline: none;
1408
+ text-transform: uppercase;
1409
+ transition: border-color 0.15s;
1410
+ box-sizing: border-box;
1411
+ }
1412
+
1413
+ .custom-loc-input:focus {
1414
+ border-color: #a78bfa;
1415
+ }
1416
+
1417
+ .custom-loc-input::placeholder {
1418
+ color: #555;
1419
+ font-weight: 400;
1420
+ text-transform: none;
1421
+ }
1422
+
1423
+ /* 非商家按钮 */
1424
+ .btn-non-seller {
1425
+ padding: 4px 10px;
1426
+ border: 1px solid #f87171;
1427
+ border-radius: 4px;
1428
+ background: transparent;
1429
+ color: #f87171;
1430
+ font-size: 11px;
1431
+ font-weight: 600;
1432
+ cursor: pointer;
1433
+ transition: all 0.15s;
1434
+ white-space: nowrap;
1435
+ }
1436
+
1437
+ .btn-non-seller:hover {
1438
+ background: rgba(248, 113, 113, 0.12);
1439
+ border-color: #ef4444;
1440
+ color: #ef4444;
1441
+ }
1442
+
1443
+ /* ===== 关键词页面 ===== */
1444
+ #tagsPage {
1445
+ display: none;
1446
+ padding-bottom: 40px;
1447
+ }
1448
+
1449
+ #tagsPage.active {
1450
+ display: block;
1451
+ }
1452
+
1453
+ .tags-layout {
1454
+ display: flex;
1455
+ flex-direction: column;
1456
+ gap: 16px;
1457
+ }
1458
+
1459
+ .tags-table-card {
1460
+ background: #1c1c26;
1461
+ border-radius: 10px;
1462
+ padding: 16px;
1463
+ }
1464
+
1465
+ .tags-table-card h3 {
1466
+ font-size: 14px;
1467
+ color: #e0e0e0;
1468
+ margin-bottom: 12px;
1469
+ }
1470
+
1471
+ #tagsPage .controls {
1472
+ margin-bottom: 12px;
1473
+ }
1474
+
1475
+ #tagsPage .controls button {
1476
+ padding: 5px 10px;
1477
+ border: 1px solid #333;
1478
+ border-radius: 4px;
1479
+ background: #2a2a3a;
1480
+ color: #ccc;
1481
+ font-size: 11px;
1482
+ cursor: pointer;
1483
+ transition: all 0.15s;
1484
+ }
1485
+
1486
+ #tagsPage .controls button.active {
1487
+ background: #7c3aed;
1488
+ color: #fff;
1489
+ border-color: #7c3aed;
1490
+ }
1491
+
1492
+ #tagsPage .controls button:hover:not(.active) {
1493
+ border-color: #7c3aed;
1494
+ color: #a78bfa;
1495
+ }
1496
+
1497
+ #tagsTable td {
1498
+ font-size: 12px;
1499
+ }
1500
+
1501
+ #tagsTable td:nth-child(3),
1502
+ #tagsTable td:nth-child(4),
1503
+ #tagsTable td:nth-child(5),
1504
+ #tagsTable td:nth-child(6),
1505
+ #tagsTable td:nth-child(7) {
1506
+ font-family: monospace;
1507
+ text-align: right;
1508
+ color: #9ca3af;
1509
+ }
1510
+
1511
+ .form-row {
1512
+ margin-top: 12px;
1513
+ }
1514
+
1515
+ .form-row label {
1516
+ display: block;
1517
+ margin-bottom: 4px;
1518
+ }
1519
+
1520
+ .checkbox-label {
1521
+ display: inline-flex;
1522
+ align-items: center;
1523
+ gap: 4px;
1524
+ padding: 4px 10px;
1525
+ border: 1px solid #333;
1526
+ border-radius: 4px;
1527
+ background: #0f0f13;
1528
+ color: #ccc;
1529
+ font-size: 12px;
1530
+ cursor: pointer;
1531
+ transition: all 0.15s;
1532
+ user-select: none;
1533
+ }
1534
+
1535
+ .checkbox-label:hover {
1536
+ border-color: #a78bfa;
1537
+ }
1538
+
1539
+ .checkbox-label input[type="checkbox"] {
1540
+ accent-color: #a78bfa;
1541
+ }
@@ -561,6 +561,26 @@ export function startWatchServer(
561
561
  return;
562
562
  }
563
563
 
564
+ const nonSellerMatch = routePath.match(
565
+ /^\/api\/user-non-seller\/([^/]+)$/,
566
+ );
567
+ if (req.method === "PUT" && nonSellerMatch) {
568
+ const uniqueId = nonSellerMatch[1];
569
+ try {
570
+ const ret = store.setNonSeller(uniqueId);
571
+ if (ret.error) {
572
+ sendJSON(res, 404, { error: ret.error });
573
+ return;
574
+ }
575
+ const ts = new Date().toISOString().slice(11, 19);
576
+ console.error(`[JOB ${ts}] NON-SELLER: ${uniqueId} → ttSeller=false`);
577
+ sendJSON(res, 200, ret);
578
+ } catch (e) {
579
+ sendJSON(res, 400, { error: e.message });
580
+ }
581
+ return;
582
+ }
583
+
564
584
  if (req.method === "GET" && routePath === "/api/comment-tasks") {
565
585
  const limit = parseInt(params.limit) || 1;
566
586
  const tasks = store.getPendingCommentTasks(limit);
@@ -1127,26 +1147,32 @@ export function startWatchServer(
1127
1147
  return;
1128
1148
  }
1129
1149
 
1130
- // GET /api/tags?status=&country=&limit=
1150
+ // GET /api/tags/stats — 关键词统计(总数/各状态/国家列表)
1151
+ if (req.method === "GET" && routePath === "/api/tags/stats") {
1152
+ const stats = store.getTagStats();
1153
+ sendJSON(res, 200, stats || { total: 0, countries: [] });
1154
+ return;
1155
+ }
1156
+
1157
+ // GET /api/tags?status=&country=&limit=&offset=
1131
1158
  if (req.method === "GET" && routePath === "/api/tags") {
1132
1159
  const status = params.status || null;
1133
1160
  const country = params.country || null;
1134
- const limit = Math.min(parseInt(params.limit) || 100, 500);
1161
+ const limit = Math.min(parseInt(params.limit) || 200, 500);
1162
+ const offset = parseInt(params.offset) || 0;
1163
+ const upperCountry = country ? country.toUpperCase() : null;
1135
1164
 
1136
1165
  let tags;
1137
1166
  if (status) {
1138
- tags = store.getTagsByStatus(status, limit);
1167
+ tags = store.getTagsByStatus(status, limit, offset, upperCountry);
1139
1168
  } else {
1140
- tags = store.getAllTags(limit);
1169
+ tags = store.getAllTags(limit, offset, upperCountry);
1141
1170
  }
1142
1171
 
1143
- if (country) {
1144
- tags = tags.filter((t) =>
1145
- t.countries.includes(country.toUpperCase()),
1146
- );
1147
- }
1148
-
1149
- sendJSON(res, 200, { tags, total: tags.length });
1172
+ // 获取总数(带国家过滤)
1173
+ const stats = store.getTagStats(upperCountry);
1174
+ const total = stats ? stats.total : tags.length;
1175
+ sendJSON(res, 200, { tags, total, offset, limit });
1150
1176
  return;
1151
1177
  }
1152
1178
 
@@ -1173,6 +1199,33 @@ export function startWatchServer(
1173
1199
  return;
1174
1200
  }
1175
1201
 
1202
+ // POST /api/tags/batch-add — 批量添加关键词
1203
+ if (req.method === "POST" && routePath === "/api/tags/batch-add") {
1204
+ try {
1205
+ const body = await readBody(req);
1206
+ const { tags, countries } = body || {};
1207
+ if (!Array.isArray(tags) || tags.length === 0) {
1208
+ sendJSON(res, 400, { error: "tags 不能为空" });
1209
+ return;
1210
+ }
1211
+ if (!Array.isArray(countries) || countries.length === 0) {
1212
+ sendJSON(res, 400, { error: "countries 不能为空" });
1213
+ return;
1214
+ }
1215
+ let added = 0;
1216
+ for (const tag of tags) {
1217
+ for (const c of countries) {
1218
+ const ret = store.insertTag(tag, [c]);
1219
+ if (ret.inserted) added++;
1220
+ }
1221
+ }
1222
+ sendJSON(res, 200, { ok: true, added });
1223
+ } catch (e) {
1224
+ sendJSON(res, 500, { error: e.message });
1225
+ }
1226
+ return;
1227
+ }
1228
+
1176
1229
  // POST /api/tags/productive — CLI 模式上报 productive tag
1177
1230
  if (req.method === "POST" && routePath === "/api/tags/productive") {
1178
1231
  try {
@@ -1266,6 +1319,22 @@ export function startWatchServer(
1266
1319
  console.error(`Watch 监控服务已启动:`);
1267
1320
  console.error(` 本地访问: http://127.0.0.1:${port}`);
1268
1321
  console.error(` 局域网访问: http://${localIP}:${port}`);
1322
+
1323
+ // 启动时清理超时的 scoring 标签
1324
+ try {
1325
+ const { resetStaleScoringTags } = store;
1326
+ if (resetStaleScoringTags) {
1327
+ const result = resetStaleScoringTags(30);
1328
+ if (result.reset > 0) {
1329
+ console.error(
1330
+ `[启动] 已重置 ${result.reset} 个超时的 scoring 标签`,
1331
+ );
1332
+ }
1333
+ }
1334
+ } catch (e) {
1335
+ console.error(`[启动] 清理 scoring 标签失败: ${e.message}`);
1336
+ }
1337
+
1269
1338
  _resolve({ server, port });
1270
1339
  });
1271
1340
 
@@ -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.7,
48
+ temperature: 0.3,
49
49
  }),
50
50
  });
51
51
 
@@ -163,7 +163,7 @@ export function buildDiscoverPrompt(
163
163
  .map((t) => `${t.tag}(score:${Math.round(t.score)})`)
164
164
  .join(
165
165
  ", ",
166
- )}. These are examples of what works — explore DIFFERENT directions, not variations of these.`
166
+ )}. Use these as reference for the STYLE and TYPE of tag that works — prefer commonly used words like these.`
167
167
  : "";
168
168
 
169
169
  // 负样本:该国 dead tag
@@ -186,7 +186,7 @@ export function buildDiscoverPrompt(
186
186
  const allExisting = history.allExisting || [];
187
187
  const existingHint =
188
188
  allExisting.length > 0
189
- ? `\nTags already in database (DO NOT generate these again): ${allExisting.slice(-50).join(", ")}.`
189
+ ? `\nTags already in database (DO NOT generate these again): ${allExisting.slice(0, 50).join(", ")}.`
190
190
  : "";
191
191
 
192
192
  const userHint = userPrompt
@@ -210,39 +210,60 @@ Based on the above, which strategies produced high-scoring tags? Which failed?
210
210
  Use this analysis to decide your strategy for this round.`;
211
211
  }
212
212
 
213
+ const deadRatio =
214
+ history.dead.length > 0 && history.productive.length > 0
215
+ ? (
216
+ (history.dead.length /
217
+ (history.dead.length + history.productive.length)) *
218
+ 100
219
+ ).toFixed(0)
220
+ : null;
221
+ const qualityWarning =
222
+ deadRatio && Number(deadRatio) > 40
223
+ ? `\n⚠️ WARNING: Currently ${deadRatio}% of our generated tags fail (no real TikTok posts). This is critically high. You MUST be more conservative — only suggest hashtags you have HIGH CONFIDENCE actually exist on TikTok.`
224
+ : "";
225
+
213
226
  return `You are discovering TikTok hashtags used by people who sell things in ${country}.
214
227
 
215
- Your goal: Find hashtags that real sellers in ${country} actually use any kind of tag they might use. Think broadly:
216
- - Who they are (seller, shop owner, entrepreneur, artisan...)
217
- - What they sell (shoes, clothes, jewelry, food, pets, furniture...)
218
- - How they sell (online, handmade, second-hand, local pickup...)
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...
228
+ CRITICAL RULE: ONLY suggest hashtags that you are CONFIDENT actually exist on TikTok with real posts.
229
+ Never invent, construct, or guess compound words. If you aren't sure a hashtag is real, DO NOT suggest it.
230
+
231
+ Good examples of REAL TikTok hashtags:
232
+ - Simple, common words: "verkaufen", "handmade", "secondhand", "bakery", "vintage"
233
+ - Common category tags: "shoplocal", "onlineshopping", "handwerker", "trödel"
234
+ - Established brand/community tags: "smallbusiness", "supportlocal", "vendita"
235
+ ❌ BAD examples (INVENTED, will fail):
236
+ - Novel compound words: "sourdoughschaleverkauf", "predavamdruhouseoblečenípraha"
237
+ - Hyper-specific location+product: "dublinvintagelamp", "canalistalisboa"
238
+ - Rare technical terms: "briefmarkensammlungankauf", "aquascapingzubehör"
239
+ - Underscore-connected constructs: "mtg_cardhu", "epoxigyanta_alkotás_eladó"
240
+
241
+ Think about the MOST COMMON hashtags that real sellers in ${country} use on TikTok:
242
+ - Common selling verbs in ${langName} (sell, buy, offer, clearance...)
243
+ - Common product categories in ${langName} (shoes, clothes, furniture, food, pets...)
244
+ - Common seller identities (shop, boutique, small business, creator...)
245
+ - Well-known community tags (support local, marketplace, second round...)
221
246
 
222
- All tags must be in ${langName} language (or widely used in ${country}).
223
- Generate ${count} tags that are ALL DIFFERENT from each other and from any existing tags.${productiveHint}${deadHint}${errorHint}${existingHint}${userHint}${strategyReview}
247
+ ${qualityWarning}${productiveHint}${deadHint}${errorHint}${existingHint}${userHint}${strategyReview}
224
248
 
225
- ## IMPORTANT: Think first, then generate
249
+ ## Quality check before responding
226
250
 
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.
251
+ For each tag you consider, ask yourself: "Have I actually seen this hashtag used on TikTok, or am I just translating a concept?" If you're translating/constructing — DON'T include it. Only include if you're genuinely confident it's a real, used hashtag.
232
252
 
233
253
  ## Output format
234
254
 
235
255
  Return ONLY a JSON object with two fields:
236
256
  {
237
- "strategy": "Your analysis and plan for this round (2-4 sentences). Explain what direction you're exploring and why.",
257
+ "strategy": "Your analysis (2-3 sentences). Acknowledge what worked/failed before and explain why your chosen tags are likely real and commonly used.",
238
258
  "tags": ["tag1", "tag2", "tag3", "tag4"]
239
259
  }
240
260
 
241
261
  Rules:
242
- - Each tag should explore a DIFFERENT angle don't just swap country suffixes
243
- - Prefer specific and niche tags over generic ones (e.g., "vendozapatos" beats "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`;
262
+ - QUALITY over creativity 3 real, commonly used tags beat 4 invented ones
263
+ - NEVER invent compound words or translate concepts into hashtags
264
+ - 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`;
246
267
  }
247
268
 
248
269
  // ====== discover: 单国家标签发现 ======