tt-help-cli-ycl 1.4.23 → 1.4.25

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.
@@ -1563,291 +1563,211 @@ export function createStore(filePath, options = {}) {
1563
1563
  .filter(Boolean)
1564
1564
  : [];
1565
1565
 
1566
- function getLocationGroups() {
1567
- const selected = normalizedLocations.length
1568
- ? normalizedLocations
1569
- : null;
1570
- const tier1List = selected
1571
- ? selected.filter((loc) => tier1.has(loc))
1572
- : [...tier1];
1573
- const tier2List = selected
1574
- ? selected.filter((loc) => tier2.has(loc))
1575
- : [...tier2];
1576
- const otherList = selected
1577
- ? selected.filter((loc) => !tier1.has(loc) && !tier2.has(loc))
1578
- : null;
1579
- const groups = [];
1580
- if (tier1List.length > 0)
1581
- groups.push({ type: "include", values: tier1List });
1582
- if (tier2List.length > 0)
1583
- groups.push({ type: "include", values: tier2List });
1584
- if (selected) {
1585
- if (otherList.length > 0)
1586
- groups.push({ type: "include", values: otherList });
1587
- } else {
1588
- groups.push({ type: "exclude", values: [...tier1, ...tier2] });
1589
- }
1590
- return groups;
1566
+ // ---- 过期 processing 回收 ----
1567
+ const expiredRow = getDb()
1568
+ .prepare(
1569
+ `SELECT * FROM jobs WHERE status = 'processing'
1570
+ AND claimed_at IS NOT NULL AND ? - claimed_at > ?
1571
+ ORDER BY claimed_at ASC LIMIT 1`,
1572
+ )
1573
+ .get(now, expireMs);
1574
+ let expiredCandidate = null;
1575
+ if (expiredRow) {
1576
+ getDb()
1577
+ .prepare(
1578
+ "UPDATE jobs SET status = 'pending', claimed_at = NULL WHERE unique_id = ?",
1579
+ )
1580
+ .run(expiredRow.unique_id);
1581
+ expiredCandidate = expiredRow;
1591
1582
  }
1592
1583
 
1593
- const locationGroups = getLocationGroups();
1584
+ // ---- 构建统一优先级查询(合并原 20+ 次独立 SELECT 为 1 次 UNION) ----
1585
+ const statusCodeWhere = loggedIn
1586
+ ? "(status_code IS NULL OR status_code = 0 OR status_code = 209002)"
1587
+ : "(status_code IS NULL OR status_code = 0)";
1588
+ const notSellerWhere = loggedIn ? "" : "AND COALESCE(tt_seller, 0) != 1";
1589
+
1590
+ const tier1List = normalizedLocations.length
1591
+ ? normalizedLocations.filter((loc) => tier1.has(loc))
1592
+ : [...tier1];
1593
+ const tier2List = normalizedLocations.length
1594
+ ? normalizedLocations.filter((loc) => tier2.has(loc))
1595
+ : [...tier2];
1596
+ const otherFiltered = normalizedLocations.length
1597
+ ? normalizedLocations.filter(
1598
+ (loc) => !tier1.has(loc) && !tier2.has(loc),
1599
+ )
1600
+ : null;
1594
1601
 
1595
- function applyLocationGroup(where, args, group) {
1596
- if (!group) return;
1597
- if (group.type === "include") {
1598
- where.push(
1599
- `(UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1600
- );
1601
- args.push(...group.values);
1602
- return;
1603
- }
1604
- where.push(
1605
- `UPPER(COALESCE(guessed_location, '')) NOT IN (${group.values.map(() => "?").join(", ")})`,
1606
- );
1607
- args.push(...group.values);
1608
- }
1602
+ const locIn = (list) => {
1603
+ if (!list || list.length === 0) return null;
1604
+ const ph = list.map(() => "?").join(", ");
1605
+ return {
1606
+ sql: `(UPPER(COALESCE(guessed_location, '')) IN (${ph}) OR COALESCE(guessed_location, '') = '')`,
1607
+ params: [...list],
1608
+ };
1609
+ };
1610
+ const locNotIn = (list) => {
1611
+ if (!list || list.length === 0) return null;
1612
+ const ph = list.map(() => "?").join(", ");
1613
+ return {
1614
+ sql: `UPPER(COALESCE(guessed_location, '')) NOT IN (${ph})`,
1615
+ params: [...list],
1616
+ };
1617
+ };
1618
+ const locOther = () => {
1619
+ if (otherFiltered && otherFiltered.length > 0)
1620
+ return locIn(otherFiltered);
1621
+ if (normalizedLocations.length === 0)
1622
+ return locNotIn([...tier1, ...tier2]);
1623
+ return null;
1624
+ };
1609
1625
 
1610
- function queryPendingOne({ requireVideo, group, filters = [] }) {
1611
- const where = ["status = 'pending'"];
1612
- const args = [];
1613
- if (!loggedIn) {
1614
- where.push("COALESCE(tt_seller, 0) != 1");
1615
- // 未登录:只能领取 status_code 为空或 0 的任务
1616
- where.push("(status_code IS NULL OR status_code = 0)");
1617
- } else {
1618
- // 登录:可以领取 status_code 为空、0、或 209002 的任务
1619
- where.push(
1620
- "(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
1621
- );
1622
- }
1623
- // 其他 status_code 值的任务不被领取
1624
- if (requireVideo) {
1625
- where.push("COALESCE(video_count, 0) > 0");
1626
- }
1627
- applyLocationGroup(where, args, group);
1628
- for (const filter of filters) {
1629
- where.push(filter);
1630
- }
1631
- return getDb()
1632
- .prepare(
1633
- `
1634
- SELECT *
1635
- FROM jobs
1636
- WHERE ${where.join(" AND ")}
1637
- ORDER BY follower_count DESC, created_at ASC, unique_id ASC
1638
- LIMIT 1
1639
- `,
1640
- )
1641
- .get(...args);
1626
+ // 全局 location 过滤(pinned 查询用)
1627
+ let pinnedLocSql = "";
1628
+ const pinnedLocParams = [];
1629
+ if (normalizedLocations.length > 0) {
1630
+ const ph = normalizedLocations.map(() => "?").join(", ");
1631
+ pinnedLocSql = `AND (UPPER(COALESCE(guessed_location, '')) IN (${ph}) OR COALESCE(guessed_location, '') = '')`;
1632
+ pinnedLocParams.push(...normalizedLocations);
1642
1633
  }
1643
1634
 
1644
- function queryPendingByGroup({ requireVideo, group, filters = [] }) {
1645
- if (group?.type === "include" && group.values.length > 1) {
1646
- for (const location of group.values) {
1647
- const row = queryPendingOne({
1648
- requireVideo,
1649
- group: { type: "include", values: [location] },
1650
- filters,
1651
- });
1652
- if (row) return row;
1653
- }
1654
- return null;
1655
- }
1656
- return queryPendingOne({ requireVideo, group, filters });
1657
- }
1635
+ const sqlParts = [];
1636
+ const allParams = [];
1658
1637
 
1659
- function findPinnedPending(requireVideo) {
1660
- const where = ["status = 'pending'", "COALESCE(pinned, 0) = 1"];
1661
- const args = [];
1662
- if (!loggedIn) {
1663
- where.push("COALESCE(tt_seller, 0) != 1");
1664
- }
1665
- if (requireVideo) {
1666
- where.push("COALESCE(video_count, 0) > 0");
1667
- }
1668
- if (normalizedLocations.length > 0) {
1669
- where.push(
1670
- `(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
1671
- );
1672
- args.push(...normalizedLocations);
1673
- }
1674
- return getDb()
1675
- .prepare(
1676
- `
1677
- SELECT *
1678
- FROM jobs
1679
- WHERE ${where.join(" AND ")}
1680
- ORDER BY created_at ASC, unique_id ASC
1681
- LIMIT 1
1682
- `,
1683
- )
1684
- .get(...args);
1685
- }
1638
+ const addPart = (prio, subPrio, conditions, params) => {
1639
+ sqlParts.push(
1640
+ `SELECT *, ${prio} as _prio, ${subPrio} as _sub_prio FROM jobs WHERE ${conditions}`,
1641
+ );
1642
+ allParams.push(...params);
1643
+ };
1686
1644
 
1687
- function findPrioritizedPending(requireVideo) {
1688
- for (const group of locationGroups) {
1689
- const seed = queryPendingByGroup({
1690
- requireVideo,
1691
- group,
1692
- filters: [
1693
- "COALESCE(pinned, 0) = 0",
1694
- `instr(COALESCE(sources, ''), '"seed"') > 0`,
1695
- ],
1696
- });
1697
- if (seed) return seed;
1698
- }
1645
+ // prio 0: pinned + hasVideo
1646
+ addPart(
1647
+ 0,
1648
+ 0,
1649
+ `status = 'pending' AND pinned = 1 AND video_count > 0 ${pinnedLocSql} AND ${statusCodeWhere} ${notSellerWhere}`,
1650
+ pinnedLocParams,
1651
+ );
1652
+ // prio 1: pinned (no video req)
1653
+ addPart(
1654
+ 1,
1655
+ 0,
1656
+ `status = 'pending' AND pinned = 1 ${pinnedLocSql} AND ${statusCodeWhere} ${notSellerWhere}`,
1657
+ pinnedLocParams,
1658
+ );
1699
1659
 
1700
- if (loggedIn) {
1701
- for (const group of locationGroups) {
1702
- const seller = queryPendingByGroup({
1703
- requireVideo,
1704
- group,
1705
- filters: [
1706
- "COALESCE(pinned, 0) = 0",
1707
- "tt_seller = 1",
1708
- "verified = 0",
1709
- ],
1710
- });
1711
- if (seller) return seller;
1712
- }
1713
- }
1660
+ // prio 2-3: expired candidate(回收的超时任务优先)
1661
+ if (expiredCandidate) {
1662
+ addPart(
1663
+ 2,
1664
+ 0,
1665
+ `unique_id = ? AND video_count > 0 AND ${statusCodeWhere} ${notSellerWhere}`,
1666
+ [expiredCandidate.unique_id],
1667
+ );
1668
+ addPart(
1669
+ 3,
1670
+ 0,
1671
+ `unique_id = ? AND ${statusCodeWhere} ${notSellerWhere}`,
1672
+ [expiredCandidate.unique_id],
1673
+ );
1674
+ }
1714
1675
 
1715
- for (const group of locationGroups) {
1716
- const follow = queryPendingByGroup({
1717
- requireVideo,
1718
- group,
1719
- filters: [
1720
- "COALESCE(pinned, 0) = 0",
1721
- `(
1722
- instr(COALESCE(sources, ''), '"following"') > 0
1723
- OR instr(COALESCE(sources, ''), '"follower"') > 0
1724
- OR instr(COALESCE(sources, ''), '"comment"') > 0
1725
- )`,
1726
- ],
1727
- });
1728
- if (follow) return follow;
1729
- }
1676
+ // 非 pinned 优先级:seed → seller → follow → other,每级 × 3 个 location tier × (hasVideo / noVideoReq)
1677
+ const priorities = [
1678
+ {
1679
+ filter: `instr(COALESCE(sources, ''), '"seed"') > 0`,
1680
+ loggedInOnly: false,
1681
+ },
1682
+ { filter: `tt_seller = 1 AND verified = 0`, loggedInOnly: true },
1683
+ {
1684
+ filter: `(instr(COALESCE(sources, ''), '"following"') > 0 OR instr(COALESCE(sources, ''), '"follower"') > 0 OR instr(COALESCE(sources, ''), '"comment"') > 0)`,
1685
+ loggedInOnly: false,
1686
+ },
1687
+ { filter: `1 = 1`, loggedInOnly: false },
1688
+ ];
1730
1689
 
1731
- for (const group of locationGroups) {
1732
- const other = queryPendingByGroup({
1733
- requireVideo,
1734
- group,
1735
- filters: ["COALESCE(pinned, 0) = 0"],
1736
- });
1737
- if (other) return other;
1690
+ let basePrio = 4;
1691
+ for (const p of priorities) {
1692
+ if (p.loggedInOnly && !loggedIn) continue;
1693
+ const tierLocations = [
1694
+ { list: tier1List, fn: locIn },
1695
+ { list: tier2List, fn: locIn },
1696
+ { list: null, fn: locOther },
1697
+ ];
1698
+ let subPrio = 0;
1699
+ for (const tl of tierLocations) {
1700
+ const loc = tl.fn(tl.list);
1701
+ if (!loc) continue;
1702
+ // hasVideo
1703
+ addPart(
1704
+ basePrio,
1705
+ subPrio,
1706
+ `status = 'pending' AND pinned = 0 AND ${p.filter} AND video_count > 0 AND ${loc.sql} AND ${statusCodeWhere} ${notSellerWhere}`,
1707
+ loc.params,
1708
+ );
1709
+ subPrio++;
1710
+ // noVideoReq
1711
+ addPart(
1712
+ basePrio + 1,
1713
+ subPrio,
1714
+ `status = 'pending' AND pinned = 0 AND ${p.filter} AND ${loc.sql} AND ${statusCodeWhere} ${notSellerWhere}`,
1715
+ loc.params,
1716
+ );
1717
+ subPrio++;
1738
1718
  }
1719
+ basePrio += 2;
1720
+ }
1739
1721
 
1740
- return null;
1722
+ let selectedRow = null;
1723
+ if (sqlParts.length > 0) {
1724
+ const unionSql =
1725
+ sqlParts.join(" UNION ALL ") +
1726
+ " ORDER BY _prio, _sub_prio, follower_count DESC, created_at ASC, unique_id ASC LIMIT 1";
1727
+ selectedRow = getDb()
1728
+ .prepare(unionSql)
1729
+ .get(...allParams);
1741
1730
  }
1742
1731
 
1743
- function claimRow(row) {
1744
- if (!row) return null;
1732
+ if (selectedRow) {
1745
1733
  getDb()
1746
1734
  .prepare(
1747
1735
  "UPDATE jobs SET status = 'processing', claimed_at = ?, claimed_by = ? WHERE unique_id = ?",
1748
1736
  )
1749
- .run(now, userId, row.unique_id);
1737
+ .run(now, userId, selectedRow.unique_id);
1750
1738
  markStatsDirty();
1751
1739
  return {
1752
- uniqueId: row.unique_id,
1753
- nickname: row.nickname,
1740
+ uniqueId: selectedRow.unique_id,
1741
+ nickname: selectedRow.nickname,
1754
1742
  claimedAt: now,
1755
1743
  claimedBy: userId,
1756
1744
  };
1757
1745
  }
1758
1746
 
1759
- const expiredRow = getDb()
1760
- .prepare(
1761
- `
1762
- SELECT *
1763
- FROM jobs
1764
- WHERE status = 'processing'
1765
- AND claimed_at IS NOT NULL
1766
- AND ? - claimed_at > ?
1767
- ORDER BY claimed_at ASC
1768
- LIMIT 1
1769
- `,
1770
- )
1771
- .get(now, expireMs);
1772
- let expiredCandidate = null;
1773
- if (expiredRow) {
1774
- getDb()
1775
- .prepare(
1776
- "UPDATE jobs SET status = 'pending', claimed_at = NULL WHERE unique_id = ?",
1777
- )
1778
- .run(expiredRow.unique_id);
1779
- expiredCandidate = mapJobRow({
1780
- ...expiredRow,
1781
- status: "pending",
1782
- claimed_at: null,
1783
- });
1784
- }
1785
-
1786
- for (const requireVideo of [true, false]) {
1787
- const pinned = findPinnedPending(requireVideo);
1788
- if (pinned) {
1789
- return claimRow(pinned);
1790
- }
1791
- if (expiredCandidate) {
1792
- return claimRow({
1793
- unique_id: expiredCandidate.uniqueId,
1794
- nickname: expiredCandidate.nickname,
1795
- });
1796
- }
1797
- const ranked = findPrioritizedPending(requireVideo);
1798
- if (ranked) {
1799
- return claimRow(ranked);
1800
- }
1801
- }
1802
- // 尝试从 raw_jobs 毛料库补充任务(使用 createStore 时配置的 LLM 打分)
1803
- // 使用锁防止多个请求同时触发 LLM refill
1804
- if (refillLock) {
1805
- // 已有 refill 在进行中,等待完成后重新尝试领取
1806
- await refillLock;
1807
- for (const requireVideo of [true, false]) {
1808
- const pinned = findPinnedPending(requireVideo);
1809
- if (pinned) {
1810
- return claimRow(pinned);
1811
- }
1812
- const ranked = findPrioritizedPending(requireVideo);
1813
- if (ranked) {
1814
- return claimRow(ranked);
1815
- }
1816
- }
1817
- return null;
1818
- }
1819
- const refillPromise = (async () => {
1820
- const result = refillJobsFromRaw(
1821
- normalizedLocations.length ? normalizedLocations : null,
1822
- 500,
1823
- refillLlmConfig,
1824
- );
1825
- // refillJobsFromRaw 在 LLM 模式下返回 Promise
1826
- if (result && typeof result.then === "function") {
1827
- return await result;
1828
- }
1829
- return result;
1830
- })();
1831
- // 让并发请求等待同一个 refill,并且无论成功/失败都释放锁
1832
- refillLock = refillPromise.finally(() => {
1833
- refillLock = null;
1834
- });
1835
-
1836
- const refillResult = await refillLock;
1837
- if (refillResult.moved > 0) {
1838
- console.error(
1839
- `[data-store] 从 raw_jobs 补充了 ${refillResult.moved} 条任务到 jobs`,
1840
- );
1841
- for (const requireVideo of [true, false]) {
1842
- const pinned = findPinnedPending(requireVideo);
1843
- if (pinned) {
1844
- return claimRow(pinned);
1845
- }
1846
- const ranked = findPrioritizedPending(requireVideo);
1847
- if (ranked) {
1848
- return claimRow(ranked);
1747
+ // refill 异步化:不阻塞当前请求
1748
+ if (!refillLock) {
1749
+ refillLock = (async () => {
1750
+ try {
1751
+ const result = refillJobsFromRaw(
1752
+ normalizedLocations.length ? normalizedLocations : null,
1753
+ 500,
1754
+ refillLlmConfig,
1755
+ );
1756
+ const r =
1757
+ result && typeof result.then === "function"
1758
+ ? await result
1759
+ : result;
1760
+ if (r.moved > 0) {
1761
+ console.error(
1762
+ `[data-store] 从 raw_jobs 补充了 ${r.moved} 条任务到 jobs`,
1763
+ );
1764
+ }
1765
+ } catch (e) {
1766
+ console.error(`[data-store] refill 异常: ${e.message}`);
1767
+ } finally {
1768
+ refillLock = null;
1849
1769
  }
1850
- }
1770
+ })();
1851
1771
  }
1852
1772
 
1853
1773
  return null;
@@ -425,6 +425,7 @@ export function initDb(filePath) {
425
425
  `CREATE INDEX IF NOT EXISTS idx_cross_country_source ON cross_country_learnings(source_country, result_status)`,
426
426
  `CREATE INDEX IF NOT EXISTS idx_cross_country_target ON cross_country_learnings(target_country)`,
427
427
  `CREATE INDEX IF NOT EXISTS idx_tags_dead_reason ON tags(dead_reason)`,
428
+ `CREATE INDEX IF NOT EXISTS idx_jobs_recheck ON jobs(status, updated_at) WHERE status = 'done' AND (tt_seller IS NULL OR tt_seller != 1)`,
428
429
  ];
429
430
  for (const sql of indexes) {
430
431
  db.exec(sql);
@@ -7,10 +7,11 @@
7
7
  import { getDb } from "./db-schema.js";
8
8
  import { getRawJobsCount, getUserDbCount } from "./db-crud.js";
9
9
 
10
- export function getDashboardStatsFromDb(targetLocations = []) {
10
+ export function getDashboardStatsFromDb(targetLocations = [], options = {}) {
11
11
  const db = getDb();
12
12
  if (!db) return null;
13
13
 
14
+ const skipSlow = !!options.skipSlowCounts;
14
15
  const targetPlaceholders = targetLocations.map(() => "?").join(", ");
15
16
  const targetParams = targetLocations.length ? targetLocations : [];
16
17
 
@@ -89,21 +90,28 @@ export function getDashboardStatsFromDb(targetLocations = []) {
89
90
  .prepare("SELECT COUNT(*) as total FROM jobs_base")
90
91
  .get().total;
91
92
 
92
- const tagCount = db.prepare("SELECT COUNT(*) as total FROM tags").get().total;
93
+ const tagCount = skipSlow
94
+ ? 0
95
+ : db.prepare("SELECT COUNT(*) as total FROM tags").get().total;
93
96
 
94
- const commentTaskCount = db
95
- .prepare(
96
- `SELECT COUNT(*) as total FROM videos
97
- WHERE user_update_count IS NULL OR user_update_count <= 0`,
98
- )
99
- .get().total;
97
+ const commentTaskCount = skipSlow
98
+ ? 0
99
+ : db
100
+ .prepare(
101
+ `SELECT COUNT(*) as total FROM videos
102
+ WHERE user_update_count IS NULL OR user_update_count <= 0`,
103
+ )
104
+ .get().total;
105
+
106
+ const rawJobsCount = skipSlow ? 0 : getRawJobsCount();
107
+ const dbUsersCount = skipSlow ? 0 : getUserDbCount();
100
108
 
101
109
  return {
102
110
  totalUsers: aggregateRow.total,
103
111
  tagCount,
104
112
  commentTaskCount,
105
- rawJobs: getRawJobsCount(),
106
- dbTotalUsers: getUserDbCount(),
113
+ rawJobs: rawJobsCount,
114
+ dbTotalUsers: dbUsersCount,
107
115
  jobsTotal: aggregateRow.total,
108
116
  jobsBaseTotal: jobsBaseCount,
109
117
  jobsPending: aggregateRow.pending,
@@ -3356,7 +3356,7 @@ fetchClientErrors();
3356
3356
  initTableSorting();
3357
3357
  initTargetTableSorting();
3358
3358
 
3359
- // 错峰轮询,避免同时发 4 个请求阻塞服务端
3359
+ // 错峰轮询(服务端已缓存,瞬时响应无压力)
3360
3360
  setInterval(fetchStats, 10000);
3361
3361
  setTimeout(() => setInterval(fetchUsers, 10000), 2500);
3362
3362
  setTimeout(() => setInterval(fetchClientErrors, 10000), 5000);