tt-help-cli-ycl 1.4.22 → 1.4.24
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/scripts/migrate-raw-jobs.mjs +243 -0
- package/scripts/sync-raw-jobs.mjs +281 -0
- package/src/cli/raw-sync.js +41 -0
- package/src/cli/refresh.js +1 -1
- package/src/lib/args.js +50 -0
- package/src/lib/constants.js +14 -0
- package/src/main.js +3 -0
- package/src/scraper/explore-core.js +14 -4
- package/src/watch/data-store.js +256 -259
- package/src/watch/db-columns.js +2 -0
- package/src/watch/db-schema.js +2 -0
- package/src/watch/db-stats.js +18 -10
- package/src/watch/llm-config.js +66 -16
- package/src/watch/public/app.js +53 -16
- package/src/watch/server.js +122 -19
|
@@ -42,7 +42,7 @@ async function processExplore(page, username, options, log) {
|
|
|
42
42
|
keepFollow: false,
|
|
43
43
|
locationCreated: null,
|
|
44
44
|
latestVideoTime: null,
|
|
45
|
-
|
|
45
|
+
recentEcVideos: null,
|
|
46
46
|
noVideo: false,
|
|
47
47
|
restricted: false,
|
|
48
48
|
error: null,
|
|
@@ -97,6 +97,19 @@ async function processExplore(page, username, options, log) {
|
|
|
97
97
|
nowSeconds - v.createTime <= SEVEN_DAYS_SECONDS,
|
|
98
98
|
);
|
|
99
99
|
if (recentVideos.length > 0) {
|
|
100
|
+
result.recentEcVideos = recentVideos.map((v) => ({
|
|
101
|
+
id: v.id,
|
|
102
|
+
href: v.href,
|
|
103
|
+
playCount: v.playCount,
|
|
104
|
+
createTime: v.createTime,
|
|
105
|
+
isAd: v.isAd,
|
|
106
|
+
}));
|
|
107
|
+
const maxPlay = recentVideos.reduce(
|
|
108
|
+
(max, v) => (v.playCount > max ? v.playCount : max),
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
111
|
+
log(` 7天内 EC视频: ${recentVideos.length} 个, 最高播放 ${maxPlay}`);
|
|
112
|
+
// 兼容旧字段 topRecentVideo
|
|
100
113
|
const topVideo = recentVideos.reduce((max, v) =>
|
|
101
114
|
v.playCount > max.playCount ? v : max,
|
|
102
115
|
);
|
|
@@ -107,9 +120,6 @@ async function processExplore(page, username, options, log) {
|
|
|
107
120
|
createTime: topVideo.createTime,
|
|
108
121
|
isAd: topVideo.isAd,
|
|
109
122
|
};
|
|
110
|
-
log(
|
|
111
|
-
` 7天内 EC视频最高播放: ${topVideo.playCount} 次播放 (${recentVideos.length} 个EC候选)`,
|
|
112
|
-
);
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
if (videoArray.length <= 0) {
|
package/src/watch/data-store.js
CHANGED
|
@@ -320,7 +320,9 @@ function getTargetUsersFromDb(targetLocations = []) {
|
|
|
320
320
|
tt_seller, verified, follower_count, following_count,
|
|
321
321
|
location_created, latest_video_time, refresh_time,
|
|
322
322
|
guessed_location, pinned, processed_at, video_count,
|
|
323
|
-
no_video, claimed_by, claimed_at, created_at, updated_at
|
|
323
|
+
no_video, claimed_by, claimed_at, created_at, updated_at,
|
|
324
|
+
top_video_play_count, top_video_href, top_video_is_ad,
|
|
325
|
+
recent_ec_videos
|
|
324
326
|
FROM jobs
|
|
325
327
|
WHERE tt_seller = 1
|
|
326
328
|
AND verified = 0
|
|
@@ -349,6 +351,8 @@ function getTargetUsersByCountryFromDb(targetLocations = [], options = {}) {
|
|
|
349
351
|
search,
|
|
350
352
|
limit,
|
|
351
353
|
offset,
|
|
354
|
+
sort,
|
|
355
|
+
sortOrder,
|
|
352
356
|
} = options;
|
|
353
357
|
const placeholders = targetLocations.map(() => "?").join(", ");
|
|
354
358
|
const baseParams = [...targetLocations];
|
|
@@ -401,6 +405,7 @@ function getTargetUsersByCountryFromDb(targetLocations = [], options = {}) {
|
|
|
401
405
|
top_video_play_count,
|
|
402
406
|
top_video_href,
|
|
403
407
|
top_video_is_ad,
|
|
408
|
+
recent_ec_videos,
|
|
404
409
|
status,
|
|
405
410
|
sources
|
|
406
411
|
FROM jobs
|
|
@@ -441,6 +446,50 @@ function getTargetUsersByCountryFromDb(targetLocations = [], options = {}) {
|
|
|
441
446
|
.prepare(countSql)
|
|
442
447
|
.get(...params)?.cnt || 0;
|
|
443
448
|
|
|
449
|
+
// 服务端排序:当 sort=topVideoPlayCount 时,全量取出后按 EC 最大播放量排序再分页
|
|
450
|
+
if (sort === "topVideoPlayCount") {
|
|
451
|
+
const allRows = getDb()
|
|
452
|
+
.prepare(sql)
|
|
453
|
+
.all(...params)
|
|
454
|
+
.map(mapJobRow);
|
|
455
|
+
|
|
456
|
+
const getMaxPlay = (u) => {
|
|
457
|
+
try {
|
|
458
|
+
const ec =
|
|
459
|
+
typeof u.recentEcVideos === "string"
|
|
460
|
+
? JSON.parse(u.recentEcVideos)
|
|
461
|
+
: Array.isArray(u.recentEcVideos)
|
|
462
|
+
? u.recentEcVideos
|
|
463
|
+
: [];
|
|
464
|
+
if (ec.length > 0) {
|
|
465
|
+
return ec.reduce(
|
|
466
|
+
(max, v) => (v.playCount > max ? v.playCount : max),
|
|
467
|
+
0,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {}
|
|
471
|
+
return 0;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const isAsc = sortOrder === "asc";
|
|
475
|
+
allRows.sort((a, b) => {
|
|
476
|
+
const va = getMaxPlay(a);
|
|
477
|
+
const vb = getMaxPlay(b);
|
|
478
|
+
return isAsc ? va - vb : vb - va;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const safeLimit = Math.min(Math.floor(limit), 10000);
|
|
482
|
+
const safeOffset = Math.max(Math.floor(offset), 0);
|
|
483
|
+
const rows = allRows.slice(safeOffset, safeOffset + safeLimit);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
total,
|
|
487
|
+
limit: safeLimit,
|
|
488
|
+
offset: safeOffset,
|
|
489
|
+
users: rows,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
444
493
|
sql += ` LIMIT ? OFFSET ?`;
|
|
445
494
|
const safeLimit = Math.min(Math.floor(limit), 10000);
|
|
446
495
|
const safeOffset = Math.max(Math.floor(offset), 0);
|
|
@@ -1514,291 +1563,211 @@ export function createStore(filePath, options = {}) {
|
|
|
1514
1563
|
.filter(Boolean)
|
|
1515
1564
|
: [];
|
|
1516
1565
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (tier2List.length > 0)
|
|
1534
|
-
groups.push({ type: "include", values: tier2List });
|
|
1535
|
-
if (selected) {
|
|
1536
|
-
if (otherList.length > 0)
|
|
1537
|
-
groups.push({ type: "include", values: otherList });
|
|
1538
|
-
} else {
|
|
1539
|
-
groups.push({ type: "exclude", values: [...tier1, ...tier2] });
|
|
1540
|
-
}
|
|
1541
|
-
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;
|
|
1542
1582
|
}
|
|
1543
1583
|
|
|
1544
|
-
|
|
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;
|
|
1545
1601
|
|
|
1546
|
-
|
|
1547
|
-
if (!
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
+
};
|
|
1560
1625
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
} else {
|
|
1569
|
-
// 登录:可以领取 status_code 为空、0、或 209002 的任务
|
|
1570
|
-
where.push(
|
|
1571
|
-
"(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
|
|
1572
|
-
);
|
|
1573
|
-
}
|
|
1574
|
-
// 其他 status_code 值的任务不被领取
|
|
1575
|
-
if (requireVideo) {
|
|
1576
|
-
where.push("COALESCE(video_count, 0) > 0");
|
|
1577
|
-
}
|
|
1578
|
-
applyLocationGroup(where, args, group);
|
|
1579
|
-
for (const filter of filters) {
|
|
1580
|
-
where.push(filter);
|
|
1581
|
-
}
|
|
1582
|
-
return getDb()
|
|
1583
|
-
.prepare(
|
|
1584
|
-
`
|
|
1585
|
-
SELECT *
|
|
1586
|
-
FROM jobs
|
|
1587
|
-
WHERE ${where.join(" AND ")}
|
|
1588
|
-
ORDER BY follower_count DESC, created_at ASC, unique_id ASC
|
|
1589
|
-
LIMIT 1
|
|
1590
|
-
`,
|
|
1591
|
-
)
|
|
1592
|
-
.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);
|
|
1593
1633
|
}
|
|
1594
1634
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
for (const location of group.values) {
|
|
1598
|
-
const row = queryPendingOne({
|
|
1599
|
-
requireVideo,
|
|
1600
|
-
group: { type: "include", values: [location] },
|
|
1601
|
-
filters,
|
|
1602
|
-
});
|
|
1603
|
-
if (row) return row;
|
|
1604
|
-
}
|
|
1605
|
-
return null;
|
|
1606
|
-
}
|
|
1607
|
-
return queryPendingOne({ requireVideo, group, filters });
|
|
1608
|
-
}
|
|
1635
|
+
const sqlParts = [];
|
|
1636
|
+
const allParams = [];
|
|
1609
1637
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (requireVideo) {
|
|
1617
|
-
where.push("COALESCE(video_count, 0) > 0");
|
|
1618
|
-
}
|
|
1619
|
-
if (normalizedLocations.length > 0) {
|
|
1620
|
-
where.push(
|
|
1621
|
-
`(UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")}) OR COALESCE(guessed_location, '') = '')`,
|
|
1622
|
-
);
|
|
1623
|
-
args.push(...normalizedLocations);
|
|
1624
|
-
}
|
|
1625
|
-
return getDb()
|
|
1626
|
-
.prepare(
|
|
1627
|
-
`
|
|
1628
|
-
SELECT *
|
|
1629
|
-
FROM jobs
|
|
1630
|
-
WHERE ${where.join(" AND ")}
|
|
1631
|
-
ORDER BY created_at ASC, unique_id ASC
|
|
1632
|
-
LIMIT 1
|
|
1633
|
-
`,
|
|
1634
|
-
)
|
|
1635
|
-
.get(...args);
|
|
1636
|
-
}
|
|
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
|
+
};
|
|
1637
1644
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
}
|
|
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
|
+
);
|
|
1650
1659
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
+
}
|
|
1665
1675
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (follow) return follow;
|
|
1680
|
-
}
|
|
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
|
+
];
|
|
1681
1689
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}
|
|
1688
|
-
|
|
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++;
|
|
1689
1718
|
}
|
|
1719
|
+
basePrio += 2;
|
|
1720
|
+
}
|
|
1690
1721
|
|
|
1691
|
-
|
|
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);
|
|
1692
1730
|
}
|
|
1693
1731
|
|
|
1694
|
-
|
|
1695
|
-
if (!row) return null;
|
|
1732
|
+
if (selectedRow) {
|
|
1696
1733
|
getDb()
|
|
1697
1734
|
.prepare(
|
|
1698
1735
|
"UPDATE jobs SET status = 'processing', claimed_at = ?, claimed_by = ? WHERE unique_id = ?",
|
|
1699
1736
|
)
|
|
1700
|
-
.run(now, userId,
|
|
1737
|
+
.run(now, userId, selectedRow.unique_id);
|
|
1701
1738
|
markStatsDirty();
|
|
1702
1739
|
return {
|
|
1703
|
-
uniqueId:
|
|
1704
|
-
nickname:
|
|
1740
|
+
uniqueId: selectedRow.unique_id,
|
|
1741
|
+
nickname: selectedRow.nickname,
|
|
1705
1742
|
claimedAt: now,
|
|
1706
1743
|
claimedBy: userId,
|
|
1707
1744
|
};
|
|
1708
1745
|
}
|
|
1709
1746
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
)
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
status: "pending",
|
|
1733
|
-
claimed_at: null,
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
for (const requireVideo of [true, false]) {
|
|
1738
|
-
const pinned = findPinnedPending(requireVideo);
|
|
1739
|
-
if (pinned) {
|
|
1740
|
-
return claimRow(pinned);
|
|
1741
|
-
}
|
|
1742
|
-
if (expiredCandidate) {
|
|
1743
|
-
return claimRow({
|
|
1744
|
-
unique_id: expiredCandidate.uniqueId,
|
|
1745
|
-
nickname: expiredCandidate.nickname,
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
const ranked = findPrioritizedPending(requireVideo);
|
|
1749
|
-
if (ranked) {
|
|
1750
|
-
return claimRow(ranked);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
// 尝试从 raw_jobs 毛料库补充任务(使用 createStore 时配置的 LLM 打分)
|
|
1754
|
-
// 使用锁防止多个请求同时触发 LLM refill
|
|
1755
|
-
if (refillLock) {
|
|
1756
|
-
// 已有 refill 在进行中,等待完成后重新尝试领取
|
|
1757
|
-
await refillLock;
|
|
1758
|
-
for (const requireVideo of [true, false]) {
|
|
1759
|
-
const pinned = findPinnedPending(requireVideo);
|
|
1760
|
-
if (pinned) {
|
|
1761
|
-
return claimRow(pinned);
|
|
1762
|
-
}
|
|
1763
|
-
const ranked = findPrioritizedPending(requireVideo);
|
|
1764
|
-
if (ranked) {
|
|
1765
|
-
return claimRow(ranked);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
return null;
|
|
1769
|
-
}
|
|
1770
|
-
const refillPromise = (async () => {
|
|
1771
|
-
const result = refillJobsFromRaw(
|
|
1772
|
-
normalizedLocations.length ? normalizedLocations : null,
|
|
1773
|
-
500,
|
|
1774
|
-
refillLlmConfig,
|
|
1775
|
-
);
|
|
1776
|
-
// refillJobsFromRaw 在 LLM 模式下返回 Promise
|
|
1777
|
-
if (result && typeof result.then === "function") {
|
|
1778
|
-
return await result;
|
|
1779
|
-
}
|
|
1780
|
-
return result;
|
|
1781
|
-
})();
|
|
1782
|
-
// 让并发请求等待同一个 refill,并且无论成功/失败都释放锁
|
|
1783
|
-
refillLock = refillPromise.finally(() => {
|
|
1784
|
-
refillLock = null;
|
|
1785
|
-
});
|
|
1786
|
-
|
|
1787
|
-
const refillResult = await refillLock;
|
|
1788
|
-
if (refillResult.moved > 0) {
|
|
1789
|
-
console.error(
|
|
1790
|
-
`[data-store] 从 raw_jobs 补充了 ${refillResult.moved} 条任务到 jobs`,
|
|
1791
|
-
);
|
|
1792
|
-
for (const requireVideo of [true, false]) {
|
|
1793
|
-
const pinned = findPinnedPending(requireVideo);
|
|
1794
|
-
if (pinned) {
|
|
1795
|
-
return claimRow(pinned);
|
|
1796
|
-
}
|
|
1797
|
-
const ranked = findPrioritizedPending(requireVideo);
|
|
1798
|
-
if (ranked) {
|
|
1799
|
-
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;
|
|
1800
1769
|
}
|
|
1801
|
-
}
|
|
1770
|
+
})();
|
|
1802
1771
|
}
|
|
1803
1772
|
|
|
1804
1773
|
return null;
|
|
@@ -2414,6 +2383,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2414
2383
|
"uniqueId",
|
|
2415
2384
|
"sources",
|
|
2416
2385
|
"topRecentVideo", // 单独处理,不进入通用循环
|
|
2386
|
+
"recentEcVideos", // 单独处理,不进入通用循环
|
|
2417
2387
|
];
|
|
2418
2388
|
for (const key of Object.keys(result)) {
|
|
2419
2389
|
if (extraFields.includes(key)) continue;
|
|
@@ -2431,6 +2401,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2431
2401
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2432
2402
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2433
2403
|
}
|
|
2404
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2405
|
+
if (
|
|
2406
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2407
|
+
result.recentEcVideos.length > 0
|
|
2408
|
+
) {
|
|
2409
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2410
|
+
} else if (result.recentEcVideos === null) {
|
|
2411
|
+
user.recentEcVideos = null;
|
|
2412
|
+
}
|
|
2434
2413
|
user.sources = [...new Set([...(user.sources || []), "processed"])];
|
|
2435
2414
|
}
|
|
2436
2415
|
if (user.status !== oldStatus) markStatsDirty();
|
|
@@ -2658,6 +2637,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2658
2637
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2659
2638
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2660
2639
|
}
|
|
2640
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2641
|
+
if (
|
|
2642
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2643
|
+
result.recentEcVideos.length > 0
|
|
2644
|
+
) {
|
|
2645
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2646
|
+
} else if (result.recentEcVideos === null) {
|
|
2647
|
+
user.recentEcVideos = null;
|
|
2648
|
+
}
|
|
2661
2649
|
const newUsers = processDiscoveredUsers(result);
|
|
2662
2650
|
const ret = updateJobInfo(uniqueId, user, false);
|
|
2663
2651
|
if (ret.error) return { saved: false, error: ret.error };
|
|
@@ -2684,6 +2672,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2684
2672
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2685
2673
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2686
2674
|
}
|
|
2675
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2676
|
+
if (
|
|
2677
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2678
|
+
result.recentEcVideos.length > 0
|
|
2679
|
+
) {
|
|
2680
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2681
|
+
} else if (result.recentEcVideos === null) {
|
|
2682
|
+
user.recentEcVideos = null;
|
|
2683
|
+
}
|
|
2687
2684
|
const newUsers = processDiscoveredUsers(result);
|
|
2688
2685
|
return { saved: true, newUsers };
|
|
2689
2686
|
}
|
package/src/watch/db-columns.js
CHANGED
|
@@ -49,6 +49,7 @@ export const JOB_EXT_COLUMNS = [
|
|
|
49
49
|
"top_video_play_count",
|
|
50
50
|
"top_video_href",
|
|
51
51
|
"top_video_is_ad",
|
|
52
|
+
"recent_ec_videos",
|
|
52
53
|
];
|
|
53
54
|
|
|
54
55
|
// 完整的 jobs 列 = 核心列 + 扩展列
|
|
@@ -91,6 +92,7 @@ export const WRITABLE_JOB_COLUMNS = new Set([
|
|
|
91
92
|
"top_video_href",
|
|
92
93
|
"top_video_is_ad",
|
|
93
94
|
"user_create_time",
|
|
95
|
+
"recent_ec_videos",
|
|
94
96
|
]);
|
|
95
97
|
|
|
96
98
|
// ===== Boolean 列(存储时 0/1,读取时转为 true/false/null) =====
|
package/src/watch/db-schema.js
CHANGED
|
@@ -129,6 +129,7 @@ export function initDb(filePath) {
|
|
|
129
129
|
top_video_href: "TEXT",
|
|
130
130
|
top_video_is_ad: "INTEGER",
|
|
131
131
|
user_create_time: "INTEGER",
|
|
132
|
+
recent_ec_videos: "TEXT",
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
// ===== jobs_base 表 =====
|
|
@@ -424,6 +425,7 @@ export function initDb(filePath) {
|
|
|
424
425
|
`CREATE INDEX IF NOT EXISTS idx_cross_country_source ON cross_country_learnings(source_country, result_status)`,
|
|
425
426
|
`CREATE INDEX IF NOT EXISTS idx_cross_country_target ON cross_country_learnings(target_country)`,
|
|
426
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)`,
|
|
427
429
|
];
|
|
428
430
|
for (const sql of indexes) {
|
|
429
431
|
db.exec(sql);
|