tt-help-cli-ycl 1.3.80 → 1.3.82

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.
@@ -961,6 +961,7 @@ let currentTargetTotal = 0;
961
961
  let currentTargetLoading = false;
962
962
  let currentTargetAllLoaded = false;
963
963
  let currentTargetSeq = 0;
964
+ let currentTargetSort = { key: null, asc: true };
964
965
  const TARGET_PAGE_SIZE = 200;
965
966
 
966
967
  async function fetchTargetByCountry() {
@@ -1087,16 +1088,36 @@ function renderTargetTable() {
1087
1088
  const el = document.getElementById("targetTable");
1088
1089
  const moreHint = document.getElementById("targetMoreHint");
1089
1090
 
1090
- if (currentTargetUsers.length === 0) {
1091
+ // 应用本地排序
1092
+ let displayUsers = currentTargetUsers;
1093
+ if (currentTargetSort.key) {
1094
+ displayUsers = [...currentTargetUsers].sort((a, b) => {
1095
+ let va = a[currentTargetSort.key];
1096
+ let vb = b[currentTargetSort.key];
1097
+ if (va == null && vb == null) return 0;
1098
+ if (va == null) return 1;
1099
+ if (vb == null) return -1;
1100
+ if (typeof va === "number" && typeof vb === "number") {
1101
+ return currentTargetSort.asc ? va - vb : vb - va;
1102
+ }
1103
+ va = String(va).toLowerCase();
1104
+ vb = String(vb).toLowerCase();
1105
+ return currentTargetSort.asc
1106
+ ? va.localeCompare(vb)
1107
+ : vb.localeCompare(va);
1108
+ });
1109
+ }
1110
+
1111
+ if (displayUsers.length === 0) {
1091
1112
  el.innerHTML =
1092
- '<tr><td colspan="9" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
1113
+ '<tr><td colspan="10" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
1093
1114
  if (moreHint) {
1094
1115
  moreHint.style.display = "none";
1095
1116
  }
1096
1117
  return;
1097
1118
  }
1098
1119
 
1099
- el.innerHTML = currentTargetUsers
1120
+ el.innerHTML = displayUsers
1100
1121
  .map((u, i) => {
1101
1122
  const nick = (u.nickname || "")
1102
1123
  .replace(/</g, "&lt;")
@@ -1113,6 +1134,13 @@ function renderTargetTable() {
1113
1134
  ? formatTime(u.latestVideoTime * 1000)
1114
1135
  : "-";
1115
1136
  const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
1137
+ const topPlayCount =
1138
+ u.topVideoPlayCount != null && u.topVideoPlayCount > 0
1139
+ ? formatNum(u.topVideoPlayCount)
1140
+ : "-";
1141
+ const topPlayCountCell = u.topVideoHref
1142
+ ? `<td data-label="最大播放量" style="font-size:11px;color:#888"><a href="${u.topVideoHref}" target="_blank" style="color:#3b82f6;text-decoration:none" title="点击查看视频">${topPlayCount}</a></td>`
1143
+ : `<td data-label="最大播放量" style="font-size:11px;color:#888">${topPlayCount}</td>`;
1116
1144
 
1117
1145
  const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
1118
1146
  const locationCell = u.modifiedAt
@@ -1140,6 +1168,7 @@ function renderTargetTable() {
1140
1168
  <td data-label="视频">${videos}</td>
1141
1169
  ${locationCell}
1142
1170
  <td data-label="确认国家" style="font-size:11px">${confirmedLocation}</td>
1171
+ ${topPlayCountCell}
1143
1172
  <td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
1144
1173
  <td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
1145
1174
  </tr>`;
@@ -1325,7 +1354,7 @@ function renderPendingCountryGrid(countries) {
1325
1354
  <div class="pending-country-item${isUnknown ? "" : " has-target"}"
1326
1355
  onclick="filterByPendingCountry('${safeCountry}')">
1327
1356
  <div class="country-action-btns">
1328
- <button class="country-action-btn restore" title="重置为需要预处理" onclick="event.stopPropagation(); resetPendingByCountry('${safeCountry}', ${c.count})">↺</button>
1357
+ <!-- <button class="country-action-btn restore" title="重置为需要预处理" onclick="event.stopPropagation(); resetPendingByCountry('${safeCountry}', ${c.count})">↺</button> -->
1329
1358
  <button class="country-action-btn" title="移到毛料库,暂不处理" onclick="event.stopPropagation(); moveCountryJobsToRaw('pending', '${safeCountry}', ${c.count})">✕</button>
1330
1359
  </div>
1331
1360
  <div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
@@ -1836,11 +1865,37 @@ function initTableSorting() {
1836
1865
  });
1837
1866
  }
1838
1867
 
1868
+ function initTargetTableSorting() {
1869
+ document.querySelectorAll("th.sortable-target").forEach((th) => {
1870
+ th.addEventListener("click", () => {
1871
+ const key = th.dataset.sort;
1872
+ if (!key) return;
1873
+ if (currentTargetSort.key === key) {
1874
+ currentTargetSort.asc = !currentTargetSort.asc;
1875
+ } else {
1876
+ currentTargetSort.key = key;
1877
+ currentTargetSort.asc = true;
1878
+ }
1879
+ // 更新排序指示器
1880
+ document.querySelectorAll("th.sortable-target").forEach((h) => {
1881
+ h.classList.remove("sort-asc", "sort-desc");
1882
+ const icon = h.querySelector(".sort-icon");
1883
+ if (icon) icon.textContent = "↕";
1884
+ });
1885
+ th.classList.add(currentTargetSort.asc ? "sort-asc" : "sort-desc");
1886
+ const icon = th.querySelector(".sort-icon");
1887
+ if (icon) icon.textContent = currentTargetSort.asc ? "↑" : "↓";
1888
+ renderTargetTable();
1889
+ });
1890
+ });
1891
+ }
1892
+
1839
1893
  // 初始化
1840
1894
  fetchStats();
1841
1895
  fetchUsers();
1842
1896
  fetchClientErrors();
1843
1897
  initTableSorting();
1898
+ initTargetTableSorting();
1844
1899
 
1845
1900
  setInterval(fetchStats, 10000);
1846
1901
  setInterval(fetchUsers, 10000);
@@ -327,7 +327,8 @@
327
327
  <th>视频</th>
328
328
  <th>国家</th>
329
329
  <th>确认国家</th>
330
- <th>最近发布</th>
330
+ <th class="sortable-target" data-sort="topVideoPlayCount">最大播放量 <span class="sort-icon">↕</span></th>
331
+ <th class="sortable-target" data-sort="latestVideoTime">最近发布 <span class="sort-icon">↕</span></th>
331
332
  <th>最近刷新</th>
332
333
  </tr>
333
334
  </thead>
@@ -619,6 +619,31 @@ th.sortable.sort-desc .sort-icon {
619
619
  color: #fe2c55;
620
620
  }
621
621
 
622
+ th.sortable-target {
623
+ cursor: pointer;
624
+ user-select: none;
625
+ }
626
+
627
+ th.sortable-target:hover {
628
+ color: #fe2c55;
629
+ }
630
+
631
+ th.sortable-target .sort-icon {
632
+ font-size: 10px;
633
+ opacity: 0.4;
634
+ margin-left: 2px;
635
+ }
636
+
637
+ th.sortable-target.sort-asc .sort-icon {
638
+ opacity: 1;
639
+ color: #fe2c55;
640
+ }
641
+
642
+ th.sortable-target.sort-desc .sort-icon {
643
+ opacity: 1;
644
+ color: #fe2c55;
645
+ }
646
+
622
647
  td {
623
648
  padding: 6px 10px;
624
649
  border-bottom: 1px solid #1f1f2a;
@@ -89,9 +89,14 @@ function sendCSV(res, columns, rows) {
89
89
  res.end(body);
90
90
  }
91
91
 
92
- export function startWatchServer(dataAnchor, port = 3000, existingStore) {
92
+ export function startWatchServer(
93
+ dataAnchor,
94
+ port = 3000,
95
+ existingStore,
96
+ options = {},
97
+ ) {
93
98
  return new Promise((_resolve, reject) => {
94
- const store = existingStore || createStore(dataAnchor);
99
+ const store = existingStore || createStore(dataAnchor, options);
95
100
 
96
101
  function logJob(action, detail) {
97
102
  const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
@@ -172,7 +177,7 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
172
177
  .filter(Boolean)
173
178
  : null;
174
179
  const loggedIn = params.loggedIn === "true";
175
- const job = store.claimNextJob(
180
+ const job = await store.claimNextJob(
176
181
  userId,
177
182
  5 * 60 * 1000,
178
183
  locations,
@@ -377,6 +382,7 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
377
382
  const stats = computeStatsIncremental(store);
378
383
  stats.targetLocations = DEFAULT_TARGET_LOCATIONS;
379
384
  stats.clientLoginStatus = store.getClientLoginStatus();
385
+ stats.llmSampleOffsets = store.getLlmSampleOffsets(); // 添加偏移量状态
380
386
  sendJSON(res, 200, stats);
381
387
  return;
382
388
  }
@@ -570,6 +576,8 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
570
576
  uniqueId: u.uniqueId,
571
577
  nickname: u.nickname || "",
572
578
  followerCount: u.followerCount || 0,
579
+ topVideoPlayCount: u.topVideoPlayCount || null,
580
+ topVideoHref: u.topVideoHref || null,
573
581
  }));
574
582
  sendJSON(res, 200, { total: targets.length, users });
575
583
  }
@@ -657,12 +665,32 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
657
665
  offset: parseInt(params.offset || "0", 10),
658
666
  },
659
667
  );
668
+ // 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
669
+ if (result.users && Array.isArray(result.users)) {
670
+ result.users = result.users.map((u) => ({
671
+ ...u,
672
+ topVideoPlayCount: u.topVideoPlayCount || null,
673
+ topVideoHref: u.topVideoHref || null,
674
+ }));
675
+ }
660
676
  sendJSON(res, 200, result);
661
677
  return;
662
678
  }
663
679
 
664
680
  // 默认:全量(兼容旧调用)
665
681
  const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
682
+ // 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
683
+ if (result.countries && Array.isArray(result.countries)) {
684
+ for (const country of result.countries) {
685
+ if (country.users && Array.isArray(country.users)) {
686
+ country.users = country.users.map((u) => ({
687
+ ...u,
688
+ topVideoPlayCount: u.topVideoPlayCount || null,
689
+ topVideoHref: u.topVideoHref || null,
690
+ }));
691
+ }
692
+ }
693
+ }
666
694
  sendJSON(res, 200, result);
667
695
  return;
668
696
  }
@@ -764,6 +792,41 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
764
792
  return;
765
793
  }
766
794
 
795
+ // 手动触发从 raw_jobs 补充任务到 jobs
796
+ if (req.method === "POST" && routePath === "/api/raw-jobs/refill") {
797
+ try {
798
+ const body = await readBody(req);
799
+ const locationsParam = body.locations || "";
800
+ const locations = locationsParam
801
+ ? locationsParam
802
+ .split(",")
803
+ .map((s) => s.trim().toUpperCase())
804
+ .filter(Boolean)
805
+ : null;
806
+ const limit = body.limit || 500;
807
+ const options = {
808
+ llmScore: !!body.llmScore,
809
+ llmMinScore: body.llmMinScore ?? 60,
810
+ llmSampleSize: body.llmSampleSize ?? 100,
811
+ llmMinReturn: body.llmMinReturn ?? 60,
812
+ llmMaxBatches: body.llmMaxBatches ?? 10,
813
+ };
814
+ const result = await store.refillJobsFromRaw(
815
+ locations,
816
+ limit,
817
+ options,
818
+ );
819
+ if (result.error) {
820
+ sendJSON(res, 400, result);
821
+ return;
822
+ }
823
+ sendJSON(res, 200, result);
824
+ } catch (e) {
825
+ sendJSON(res, 400, { error: e.message });
826
+ }
827
+ return;
828
+ }
829
+
767
830
  if (req.method === "POST" && routePath === "/api/attach-stuck/restore") {
768
831
  try {
769
832
  const body = await readBody(req);