tt-help-cli-ycl 1.4.142 → 1.4.145
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/sync-raw-jobs.mjs +13 -7
- package/scripts/test-api-interceptor-429.mjs +122 -0
- package/scripts/test-daemon-failover.mjs +61 -4
- package/src/clash/checker.js +41 -0
- package/src/clash/daemon.js +26 -0
- package/src/cli/comments.js +5 -5
- package/src/cli/precheck.js +2 -2
- package/src/cli/refresh-mini.js +1 -1
- package/src/cli/tag.js +110 -353
- package/src/lib/api-interceptor.js +26 -8
- package/src/lib/app-config.js +119 -0
- package/src/lib/args.js +3 -1
- package/src/lib/constants.js +9 -6
- package/src/lib/target-locations.js +1 -0
- package/src/scraper/core.js +2 -2
- package/src/scraper/explore-core.js +2 -2
- package/src/watch/data-store.js +94 -8
- package/src/watch/public/app.js +412 -26
- package/src/watch/public/index.html +74 -1
- package/src/watch/public/style.css +218 -0
- package/src/watch/server.js +144 -40
- package/src/watchdog/process-manager.js +11 -6
|
@@ -112,6 +112,12 @@ body {
|
|
|
112
112
|
color: #f472b6;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/* 精准商家卡片文字改为红色 */
|
|
116
|
+
#statPreciseCard .label,
|
|
117
|
+
#statPreciseCard .value {
|
|
118
|
+
color: #ef4444;
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
.stat-card .value-sub {
|
|
116
122
|
font-size: 12px;
|
|
117
123
|
font-weight: 400;
|
|
@@ -878,6 +884,14 @@ td.user-id:hover {
|
|
|
878
884
|
display: block;
|
|
879
885
|
}
|
|
880
886
|
|
|
887
|
+
#precisePage {
|
|
888
|
+
display: none;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#precisePage.active {
|
|
892
|
+
display: block;
|
|
893
|
+
}
|
|
894
|
+
|
|
881
895
|
#backToTop {
|
|
882
896
|
position: fixed;
|
|
883
897
|
bottom: 32px;
|
|
@@ -1834,6 +1848,22 @@ td.user-id:hover {
|
|
|
1834
1848
|
background: #e61944;
|
|
1835
1849
|
}
|
|
1836
1850
|
|
|
1851
|
+
.settings-form .btn-cancel {
|
|
1852
|
+
padding: 10px 20px;
|
|
1853
|
+
border: 1px solid #333;
|
|
1854
|
+
border-radius: 8px;
|
|
1855
|
+
background: #2a2a3a;
|
|
1856
|
+
color: #ccc;
|
|
1857
|
+
font-size: 14px;
|
|
1858
|
+
font-weight: 600;
|
|
1859
|
+
cursor: pointer;
|
|
1860
|
+
transition: background 0.2s;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
.settings-form .btn-cancel:hover {
|
|
1864
|
+
background: #333;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1837
1867
|
/* ── 看门狗页面 ── */
|
|
1838
1868
|
|
|
1839
1869
|
#watchdogPage {
|
|
@@ -1876,3 +1906,191 @@ td.user-id:hover {
|
|
|
1876
1906
|
color: #888;
|
|
1877
1907
|
font-weight: 600;
|
|
1878
1908
|
}
|
|
1909
|
+
|
|
1910
|
+
/* ===== 精准商家:视频卡片视图 ===== */
|
|
1911
|
+
.precise-list-header {
|
|
1912
|
+
display: flex;
|
|
1913
|
+
align-items: center;
|
|
1914
|
+
justify-content: space-between;
|
|
1915
|
+
gap: 10px;
|
|
1916
|
+
margin-bottom: 8px;
|
|
1917
|
+
}
|
|
1918
|
+
.precise-list-header h3 {
|
|
1919
|
+
margin: 0;
|
|
1920
|
+
}
|
|
1921
|
+
.view-toggle {
|
|
1922
|
+
display: inline-flex;
|
|
1923
|
+
align-items: center;
|
|
1924
|
+
gap: 2px;
|
|
1925
|
+
border-bottom: 2px solid #2f2f3f;
|
|
1926
|
+
}
|
|
1927
|
+
.view-toggle button {
|
|
1928
|
+
padding: 7px 18px;
|
|
1929
|
+
border: none;
|
|
1930
|
+
border-bottom: 2px solid transparent;
|
|
1931
|
+
border-radius: 6px 6px 0 0;
|
|
1932
|
+
background: transparent;
|
|
1933
|
+
color: #9aa;
|
|
1934
|
+
font-size: 13px;
|
|
1935
|
+
cursor: pointer;
|
|
1936
|
+
transition: all 0.2s;
|
|
1937
|
+
}
|
|
1938
|
+
.view-toggle button:hover {
|
|
1939
|
+
color: #ccc;
|
|
1940
|
+
}
|
|
1941
|
+
.view-toggle button.active {
|
|
1942
|
+
color: #fff;
|
|
1943
|
+
border-bottom-color: #7c3aed;
|
|
1944
|
+
background: rgba(124, 58, 237, 0.14);
|
|
1945
|
+
font-weight: 600;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
.precise-video-wrap {
|
|
1949
|
+
display: flex;
|
|
1950
|
+
flex-direction: column;
|
|
1951
|
+
gap: 14px;
|
|
1952
|
+
}
|
|
1953
|
+
.precise-merchant-card {
|
|
1954
|
+
background: #1a1a24;
|
|
1955
|
+
border: 1px solid #2a2a3a;
|
|
1956
|
+
border-radius: 10px;
|
|
1957
|
+
padding: 12px 14px;
|
|
1958
|
+
}
|
|
1959
|
+
.precise-merchant-head {
|
|
1960
|
+
display: flex;
|
|
1961
|
+
flex-wrap: wrap;
|
|
1962
|
+
align-items: baseline;
|
|
1963
|
+
gap: 10px;
|
|
1964
|
+
margin-bottom: 10px;
|
|
1965
|
+
}
|
|
1966
|
+
.precise-merchant-head .pm-user {
|
|
1967
|
+
font-weight: 700;
|
|
1968
|
+
color: #60a5fa;
|
|
1969
|
+
text-decoration: none;
|
|
1970
|
+
font-size: 15px;
|
|
1971
|
+
}
|
|
1972
|
+
.precise-merchant-head .pm-user:hover {
|
|
1973
|
+
text-decoration: underline;
|
|
1974
|
+
}
|
|
1975
|
+
.precise-merchant-head .pm-nick {
|
|
1976
|
+
color: #ccc;
|
|
1977
|
+
font-size: 13px;
|
|
1978
|
+
}
|
|
1979
|
+
.precise-merchant-head .pm-country {
|
|
1980
|
+
font-size: 11px;
|
|
1981
|
+
background: #2a2a3a;
|
|
1982
|
+
color: #a78bfa;
|
|
1983
|
+
padding: 1px 7px;
|
|
1984
|
+
border-radius: 10px;
|
|
1985
|
+
}
|
|
1986
|
+
.precise-merchant-head .pm-stats {
|
|
1987
|
+
color: #888;
|
|
1988
|
+
font-size: 12px;
|
|
1989
|
+
margin-left: auto;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
.precise-video-grid {
|
|
1993
|
+
display: grid;
|
|
1994
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
1995
|
+
gap: 10px;
|
|
1996
|
+
}
|
|
1997
|
+
.precise-video-item {
|
|
1998
|
+
background: #20202e;
|
|
1999
|
+
border: 1px solid #2c2c3c;
|
|
2000
|
+
border-radius: 8px;
|
|
2001
|
+
overflow: hidden;
|
|
2002
|
+
position: relative;
|
|
2003
|
+
display: flex;
|
|
2004
|
+
flex-direction: column;
|
|
2005
|
+
}
|
|
2006
|
+
.precise-video-tile {
|
|
2007
|
+
display: block;
|
|
2008
|
+
position: relative;
|
|
2009
|
+
aspect-ratio: 9 / 14;
|
|
2010
|
+
background: linear-gradient(160deg, #2a2a3a 0%, #1c1c28 100%);
|
|
2011
|
+
color: #e0e0e0;
|
|
2012
|
+
text-decoration: none;
|
|
2013
|
+
overflow: hidden;
|
|
2014
|
+
}
|
|
2015
|
+
.precise-video-tile:hover {
|
|
2016
|
+
border-color: #7c3aed;
|
|
2017
|
+
}
|
|
2018
|
+
.precise-video-tile .pv-play {
|
|
2019
|
+
position: absolute;
|
|
2020
|
+
left: 50%;
|
|
2021
|
+
top: 50%;
|
|
2022
|
+
transform: translate(-50%, -50%);
|
|
2023
|
+
width: 42px;
|
|
2024
|
+
height: 42px;
|
|
2025
|
+
border-radius: 50%;
|
|
2026
|
+
background: rgba(0, 0, 0, 0.55);
|
|
2027
|
+
display: flex;
|
|
2028
|
+
align-items: center;
|
|
2029
|
+
justify-content: center;
|
|
2030
|
+
font-size: 20px;
|
|
2031
|
+
color: #fff;
|
|
2032
|
+
}
|
|
2033
|
+
.precise-video-tile .pv-count {
|
|
2034
|
+
position: absolute;
|
|
2035
|
+
left: 6px;
|
|
2036
|
+
top: 6px;
|
|
2037
|
+
font-size: 12px;
|
|
2038
|
+
color: #fff;
|
|
2039
|
+
text-shadow: 0 1px 2px #000;
|
|
2040
|
+
}
|
|
2041
|
+
/* 播放量最大的视频:播放量文字特殊高亮 */
|
|
2042
|
+
.precise-video-tile .pv-count-max {
|
|
2043
|
+
color: #1a1a24;
|
|
2044
|
+
font-weight: 800;
|
|
2045
|
+
background: #fbbf24;
|
|
2046
|
+
padding: 1px 5px;
|
|
2047
|
+
border-radius: 4px;
|
|
2048
|
+
text-shadow: none;
|
|
2049
|
+
box-shadow: 0 0 6px rgba(251, 191, 36, 0.8);
|
|
2050
|
+
}
|
|
2051
|
+
.precise-video-tile .pv-time {
|
|
2052
|
+
position: absolute;
|
|
2053
|
+
left: 6px;
|
|
2054
|
+
top: 24px;
|
|
2055
|
+
font-size: 11px;
|
|
2056
|
+
color: #cfcfcf;
|
|
2057
|
+
text-shadow: 0 1px 2px #000;
|
|
2058
|
+
}
|
|
2059
|
+
.precise-video-tile .pv-ad {
|
|
2060
|
+
position: absolute;
|
|
2061
|
+
right: 6px;
|
|
2062
|
+
top: 6px;
|
|
2063
|
+
font-size: 10px;
|
|
2064
|
+
background: rgba(254, 44, 85, 0.9);
|
|
2065
|
+
color: #fff;
|
|
2066
|
+
padding: 1px 5px;
|
|
2067
|
+
border-radius: 4px;
|
|
2068
|
+
}
|
|
2069
|
+
/* 商品名悬浮在图片上,最多 2 行、固定高度以容纳 2 行文字 */
|
|
2070
|
+
.precise-video-item .pv-product {
|
|
2071
|
+
position: absolute;
|
|
2072
|
+
left: 0;
|
|
2073
|
+
right: 0;
|
|
2074
|
+
bottom: 0;
|
|
2075
|
+
padding: 30px 8px 8px;
|
|
2076
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.92), rgba(0, 0, 0, 0));
|
|
2077
|
+
color: #fbbf24;
|
|
2078
|
+
font-size: 12px;
|
|
2079
|
+
line-height: 1.35;
|
|
2080
|
+
text-decoration: none;
|
|
2081
|
+
text-align: left;
|
|
2082
|
+
display: -webkit-box;
|
|
2083
|
+
-webkit-box-orient: vertical;
|
|
2084
|
+
-webkit-line-clamp: 2;
|
|
2085
|
+
line-clamp: 2;
|
|
2086
|
+
overflow: hidden;
|
|
2087
|
+
box-sizing: border-box;
|
|
2088
|
+
height: 71px;
|
|
2089
|
+
}
|
|
2090
|
+
.precise-video-item .pv-product:hover {
|
|
2091
|
+
color: #fff;
|
|
2092
|
+
text-decoration: underline;
|
|
2093
|
+
}
|
|
2094
|
+
.precise-video-item .pv-product .pv-product-icon {
|
|
2095
|
+
margin-right: 4px;
|
|
2096
|
+
}
|
package/src/watch/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
9
|
import { createStore } from "./data-store.js";
|
|
10
10
|
import { getTagDb } from "./db-schema.js";
|
|
11
|
-
import {
|
|
11
|
+
import { getTargetLocations } from "../lib/app-config.js";
|
|
12
12
|
import { tryHandleWatchdogRoute } from "./watchdog-server.js";
|
|
13
13
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -44,11 +44,17 @@ const SLOW_STATS_TTL = 300000; // 5 分钟
|
|
|
44
44
|
const usersCache = new Map(); // key -> { result, time }
|
|
45
45
|
const USERS_CACHE_TTL = 3000; // 3 秒
|
|
46
46
|
|
|
47
|
-
// 目标国家占位符(jobs 表聚合需要 location_created IN (...)
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// 目标国家占位符(jobs 表聚合需要 location_created IN (...),运行时按 getTargetLocations() 动态生成)
|
|
48
|
+
function targetPlaceholders() {
|
|
49
|
+
const locs = getTargetLocations();
|
|
50
|
+
return locs.map(() => "?").join(", ");
|
|
51
|
+
}
|
|
52
|
+
function targetInSql() {
|
|
53
|
+
const locs = getTargetLocations();
|
|
54
|
+
return locs.length
|
|
55
|
+
? `location_created IN (${targetPlaceholders()})`
|
|
56
|
+
: "1 = 0";
|
|
57
|
+
}
|
|
52
58
|
|
|
53
59
|
// 统计分组定义:每组包含若干 SQL(分片执行)与一个聚合函数
|
|
54
60
|
// sqls 项:{ key, sql, mode: 'one'|'all', params: 数组或返回数组的函数 }
|
|
@@ -80,8 +86,8 @@ const STAT_GROUPS = {
|
|
|
80
86
|
{
|
|
81
87
|
key: "jobsAgg",
|
|
82
88
|
mode: "one",
|
|
83
|
-
params:
|
|
84
|
-
sql: `SELECT
|
|
89
|
+
params: () => getTargetLocations(),
|
|
90
|
+
sql: () => `SELECT
|
|
85
91
|
SUM(jobsTotal) as jobsTotal,
|
|
86
92
|
SUM(done) as done,
|
|
87
93
|
SUM(error) as error,
|
|
@@ -94,7 +100,7 @@ const STAT_GROUPS = {
|
|
|
94
100
|
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done,
|
|
95
101
|
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
96
102
|
SUM(CASE WHEN status = 'restricted' THEN 1 ELSE 0 END) as restricted,
|
|
97
|
-
SUM(CASE WHEN tt_seller = 1 AND verified = 0 AND ${
|
|
103
|
+
SUM(CASE WHEN tt_seller = 1 AND verified = 0 AND ${targetInSql()} THEN 1 ELSE 0 END) as targetUsers,
|
|
98
104
|
SUM(CASE WHEN no_video = 1 THEN 1 ELSE 0 END) as noVideo
|
|
99
105
|
FROM jobs
|
|
100
106
|
UNION ALL
|
|
@@ -108,6 +114,22 @@ const STAT_GROUPS = {
|
|
|
108
114
|
FROM jobs_done
|
|
109
115
|
)`,
|
|
110
116
|
},
|
|
117
|
+
{
|
|
118
|
+
// 精确商家计数:目标商家 + 7 天 EC 视频播放>1000 + 有商品 + 评论0 + 销量>10
|
|
119
|
+
key: "preciseMerchants",
|
|
120
|
+
mode: "one",
|
|
121
|
+
params: () => getTargetLocations(),
|
|
122
|
+
sql: () => `SELECT COUNT(*) AS n FROM jobs
|
|
123
|
+
WHERE tt_seller = 1 AND verified = 0 AND ${targetInSql()}
|
|
124
|
+
AND top_video_seo_url IS NOT NULL AND top_video_seo_url != ''
|
|
125
|
+
AND product_review_count = 0
|
|
126
|
+
AND product_sold_count > 10
|
|
127
|
+
AND recent_ec_videos IS NOT NULL AND recent_ec_videos != ''
|
|
128
|
+
AND EXISTS (
|
|
129
|
+
SELECT 1 FROM json_each(recent_ec_videos)
|
|
130
|
+
WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000
|
|
131
|
+
)`,
|
|
132
|
+
},
|
|
111
133
|
],
|
|
112
134
|
combine(results) {
|
|
113
135
|
const tq = results.tqAgg || {};
|
|
@@ -123,6 +145,7 @@ const STAT_GROUPS = {
|
|
|
123
145
|
restrictedUsers: jb.restricted || 0,
|
|
124
146
|
errorUsers: jb.error || 0,
|
|
125
147
|
targetUsers: jb.targetUsers || 0,
|
|
148
|
+
preciseMerchants: results.preciseMerchants?.n || 0,
|
|
126
149
|
sourceStats: {
|
|
127
150
|
seed: tq.seed || 0,
|
|
128
151
|
video: tq.video || 0,
|
|
@@ -177,10 +200,10 @@ const STAT_GROUPS = {
|
|
|
177
200
|
{
|
|
178
201
|
key: "targetCountryStats",
|
|
179
202
|
mode: "all",
|
|
180
|
-
params:
|
|
181
|
-
sql: `SELECT location_created as country, COUNT(*) as count
|
|
203
|
+
params: () => getTargetLocations(),
|
|
204
|
+
sql: () => `SELECT location_created as country, COUNT(*) as count
|
|
182
205
|
FROM jobs
|
|
183
|
-
WHERE tt_seller = 1 AND verified = 0 AND ${
|
|
206
|
+
WHERE tt_seller = 1 AND verified = 0 AND ${targetInSql()}
|
|
184
207
|
GROUP BY location_created
|
|
185
208
|
ORDER BY count DESC`,
|
|
186
209
|
},
|
|
@@ -250,6 +273,7 @@ const EMPTY_GROUP = {
|
|
|
250
273
|
restrictedUsers: 0,
|
|
251
274
|
errorUsers: 0,
|
|
252
275
|
targetUsers: 0,
|
|
276
|
+
preciseMerchants: 0,
|
|
253
277
|
sourceStats: {
|
|
254
278
|
seed: 0,
|
|
255
279
|
video: 0,
|
|
@@ -284,9 +308,10 @@ function refreshGroupInShards(st, name) {
|
|
|
284
308
|
if (idx < spec.sqls.length) {
|
|
285
309
|
const s = spec.sqls[idx++];
|
|
286
310
|
try {
|
|
311
|
+
const sql = typeof s.sql === "function" ? s.sql() : s.sql;
|
|
287
312
|
const params =
|
|
288
313
|
typeof s.params === "function" ? s.params() : s.params || [];
|
|
289
|
-
const r = st.rawQuery(
|
|
314
|
+
const r = st.rawQuery(sql, params);
|
|
290
315
|
if (r?.error) {
|
|
291
316
|
console.error(`[stats] ${name}.${s.key} 查询失败: ${r.error}`);
|
|
292
317
|
results[s.key] = null;
|
|
@@ -334,9 +359,10 @@ function computeGroupStats(st, name) {
|
|
|
334
359
|
try {
|
|
335
360
|
const results = {};
|
|
336
361
|
for (const s of STAT_GROUPS[name].sqls) {
|
|
362
|
+
const sql = typeof s.sql === "function" ? s.sql() : s.sql;
|
|
337
363
|
const params =
|
|
338
364
|
typeof s.params === "function" ? s.params() : s.params || [];
|
|
339
|
-
const r = st.rawQuery(
|
|
365
|
+
const r = st.rawQuery(sql, params);
|
|
340
366
|
results[s.key] =
|
|
341
367
|
s.mode === "all" ? r?.rows || [] : r?.rows?.[0] || null;
|
|
342
368
|
}
|
|
@@ -641,7 +667,7 @@ export function startWatchServer(
|
|
|
641
667
|
// 按国家维度判断:只为 new 标签不足的国家发现
|
|
642
668
|
const countriesToDiscover = [];
|
|
643
669
|
const { getTagsByStatus } = await import("./db-tags.js");
|
|
644
|
-
for (const country of
|
|
670
|
+
for (const country of getTargetLocations()) {
|
|
645
671
|
const newTags = getTagsByStatus("new", 100, 0, country, null, "llm");
|
|
646
672
|
if (newTags.length < DISCOVER_PER_COUNTRY_THRESHOLD) {
|
|
647
673
|
countriesToDiscover.push(country);
|
|
@@ -1137,7 +1163,7 @@ export function startWatchServer(
|
|
|
1137
1163
|
|
|
1138
1164
|
if (req.method === "GET" && routePath === "/api/stats") {
|
|
1139
1165
|
const stats = computeStatsIncremental(store);
|
|
1140
|
-
stats.targetLocations =
|
|
1166
|
+
stats.targetLocations = getTargetLocations();
|
|
1141
1167
|
stats.clientLoginStatus = store.getClientLoginStatus();
|
|
1142
1168
|
stats.activeClients = store.getActiveClients();
|
|
1143
1169
|
sendJSON(res, 200, stats);
|
|
@@ -1494,7 +1520,7 @@ export function startWatchServer(
|
|
|
1494
1520
|
}
|
|
1495
1521
|
|
|
1496
1522
|
if (req.method === "GET" && routePath === "/api/target-users") {
|
|
1497
|
-
const targetResult = store.getTargetUsers(
|
|
1523
|
+
const targetResult = store.getTargetUsers(getTargetLocations());
|
|
1498
1524
|
const targets = targetResult.users;
|
|
1499
1525
|
if (req.headers["accept"]?.includes("text/csv")) {
|
|
1500
1526
|
const columns = [
|
|
@@ -1543,25 +1569,64 @@ export function startWatchServer(
|
|
|
1543
1569
|
return;
|
|
1544
1570
|
}
|
|
1545
1571
|
|
|
1572
|
+
// GET /api/precise-merchants — 精确商家列表(支持分页+搜索)
|
|
1573
|
+
if (req.method === "GET" && routePath === "/api/precise-merchants") {
|
|
1574
|
+
const result = store.getPreciseMerchants(getTargetLocations(), {
|
|
1575
|
+
search: params.search || undefined,
|
|
1576
|
+
limit: params.limit ? parseInt(params.limit, 10) : undefined,
|
|
1577
|
+
offset: params.offset ? parseInt(params.offset, 10) : undefined,
|
|
1578
|
+
});
|
|
1579
|
+
if (result && result.users) {
|
|
1580
|
+
result.users = result.users.map((u) => ({
|
|
1581
|
+
uniqueId: u.uniqueId,
|
|
1582
|
+
nickname: u.nickname || "",
|
|
1583
|
+
locationCreated: u.locationCreated || null,
|
|
1584
|
+
followerCount: u.followerCount || 0,
|
|
1585
|
+
videoCount: u.videoCount || 0,
|
|
1586
|
+
latestVideoTime: u.latestVideoTime || null,
|
|
1587
|
+
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
1588
|
+
topVideoHref: u.topVideoHref || null,
|
|
1589
|
+
topVideoIsAd: u.topVideoIsAd ?? null,
|
|
1590
|
+
topVideoProductId: u.topVideoProductId || null,
|
|
1591
|
+
topVideoProductTitle: u.topVideoProductTitle || null,
|
|
1592
|
+
topVideoSeoUrl: u.topVideoSeoUrl || null,
|
|
1593
|
+
productRating: u.productRating ?? null,
|
|
1594
|
+
productReviewCount: u.productReviewCount ?? null,
|
|
1595
|
+
productSoldCount: u.productSoldCount ?? null,
|
|
1596
|
+
productName: u.productName || null,
|
|
1597
|
+
ecOver1k: u.ecOver1k || 0,
|
|
1598
|
+
maxEcPlay: u.maxEcPlay || null,
|
|
1599
|
+
recentEcVideos: (() => {
|
|
1600
|
+
try {
|
|
1601
|
+
return Array.isArray(u.recentEcVideos)
|
|
1602
|
+
? u.recentEcVideos
|
|
1603
|
+
: JSON.parse(u.recentEcVideos || "[]");
|
|
1604
|
+
} catch {
|
|
1605
|
+
return [];
|
|
1606
|
+
}
|
|
1607
|
+
})(),
|
|
1608
|
+
}));
|
|
1609
|
+
}
|
|
1610
|
+
sendJSON(res, 200, result);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1546
1614
|
if (
|
|
1547
1615
|
req.method === "GET" &&
|
|
1548
1616
|
routePath === "/api/target-users-by-country"
|
|
1549
1617
|
) {
|
|
1550
1618
|
// 摘要模式:只返回各国统计数
|
|
1551
1619
|
if (params.summary === "1") {
|
|
1552
|
-
const result = store.getTargetUsersByCountry(
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
);
|
|
1620
|
+
const result = store.getTargetUsersByCountry(getTargetLocations(), {
|
|
1621
|
+
summaryOnly: true,
|
|
1622
|
+
});
|
|
1556
1623
|
sendJSON(res, 200, result);
|
|
1557
1624
|
return;
|
|
1558
1625
|
}
|
|
1559
1626
|
|
|
1560
1627
|
// CSV 导出:全量数据
|
|
1561
1628
|
if (req.headers["accept"]?.includes("text/csv")) {
|
|
1562
|
-
const result = store.getTargetUsersByCountry(
|
|
1563
|
-
DEFAULT_TARGET_LOCATIONS,
|
|
1564
|
-
);
|
|
1629
|
+
const result = store.getTargetUsersByCountry(getTargetLocations());
|
|
1565
1630
|
const columns = [
|
|
1566
1631
|
"uniqueId",
|
|
1567
1632
|
"nickname",
|
|
@@ -1615,17 +1680,14 @@ export function startWatchServer(
|
|
|
1615
1680
|
|
|
1616
1681
|
// 分页模式:按国家+搜索+分页查询用户
|
|
1617
1682
|
if (params.limit) {
|
|
1618
|
-
const result = store.getTargetUsersByCountry(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
sortOrder: params.sortOrder || "desc",
|
|
1627
|
-
},
|
|
1628
|
-
);
|
|
1683
|
+
const result = store.getTargetUsersByCountry(getTargetLocations(), {
|
|
1684
|
+
country: params.country || undefined,
|
|
1685
|
+
search: params.search || undefined,
|
|
1686
|
+
limit: parseInt(params.limit, 10),
|
|
1687
|
+
offset: parseInt(params.offset || "0", 10),
|
|
1688
|
+
sort: params.sort || undefined,
|
|
1689
|
+
sortOrder: params.sortOrder || "desc",
|
|
1690
|
+
});
|
|
1629
1691
|
// 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
|
|
1630
1692
|
if (result.users && Array.isArray(result.users)) {
|
|
1631
1693
|
result.users = result.users.map((u) => ({
|
|
@@ -1640,7 +1702,7 @@ export function startWatchServer(
|
|
|
1640
1702
|
}
|
|
1641
1703
|
|
|
1642
1704
|
// 默认:全量(兼容旧调用)
|
|
1643
|
-
const result = store.getTargetUsersByCountry(
|
|
1705
|
+
const result = store.getTargetUsersByCountry(getTargetLocations());
|
|
1644
1706
|
// 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
|
|
1645
1707
|
if (result.countries && Array.isArray(result.countries)) {
|
|
1646
1708
|
for (const country of result.countries) {
|
|
@@ -1662,9 +1724,8 @@ export function startWatchServer(
|
|
|
1662
1724
|
req.method === "GET" &&
|
|
1663
1725
|
routePath === "/api/target-users-other-countries"
|
|
1664
1726
|
) {
|
|
1665
|
-
const result =
|
|
1666
|
-
|
|
1667
|
-
);
|
|
1727
|
+
const result =
|
|
1728
|
+
store.getModifiedTargetUsersOutsideLocations(getTargetLocations());
|
|
1668
1729
|
sendJSON(res, 200, result);
|
|
1669
1730
|
return;
|
|
1670
1731
|
}
|
|
@@ -1842,6 +1903,49 @@ export function startWatchServer(
|
|
|
1842
1903
|
return;
|
|
1843
1904
|
}
|
|
1844
1905
|
|
|
1906
|
+
// ====== 目标国家配置(通用 app-config 落盘) ======
|
|
1907
|
+
|
|
1908
|
+
// GET /api/settings/target-locations — 读取当前目标国家
|
|
1909
|
+
if (
|
|
1910
|
+
req.method === "GET" &&
|
|
1911
|
+
routePath === "/api/settings/target-locations"
|
|
1912
|
+
) {
|
|
1913
|
+
try {
|
|
1914
|
+
const { getTargetLocations } = await import("../lib/app-config.js");
|
|
1915
|
+
sendJSON(res, 200, { locations: getTargetLocations() });
|
|
1916
|
+
} catch (e) {
|
|
1917
|
+
sendJSON(res, 500, { error: e.message });
|
|
1918
|
+
}
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// POST /api/settings/target-locations — 保存目标国家(body: { locations: ["CZ","GR",...] })
|
|
1923
|
+
if (
|
|
1924
|
+
req.method === "POST" &&
|
|
1925
|
+
routePath === "/api/settings/target-locations"
|
|
1926
|
+
) {
|
|
1927
|
+
try {
|
|
1928
|
+
const body = await readBody(req);
|
|
1929
|
+
const { setTargetLocations } = await import("../lib/app-config.js");
|
|
1930
|
+
const saved = setTargetLocations(body.locations);
|
|
1931
|
+
sendJSON(res, 200, { locations: saved });
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
sendJSON(res, 400, { error: e.message });
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// GET /api/settings/config — 读取整份通用配置(便于后续扩展其他配置)
|
|
1939
|
+
if (req.method === "GET" && routePath === "/api/settings/config") {
|
|
1940
|
+
try {
|
|
1941
|
+
const { getConfig } = await import("../lib/app-config.js");
|
|
1942
|
+
sendJSON(res, 200, { config: getConfig() });
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
sendJSON(res, 500, { error: e.message });
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1845
1949
|
// GET /api/llm-test — 直接测试 LLM API 连通性
|
|
1846
1950
|
if (req.method === "GET" && routePath === "/api/llm-test") {
|
|
1847
1951
|
try {
|
|
@@ -1939,7 +2043,7 @@ export function startWatchServer(
|
|
|
1939
2043
|
targetLocation: params.targetLocation,
|
|
1940
2044
|
limit: params.limit,
|
|
1941
2045
|
offset: params.offset,
|
|
1942
|
-
targetLocations:
|
|
2046
|
+
targetLocations: getTargetLocations(),
|
|
1943
2047
|
});
|
|
1944
2048
|
usersCache.set(cacheKey, { result, time: Date.now() });
|
|
1945
2049
|
}
|
|
@@ -22,8 +22,11 @@ import { pushSelfLog } from "./self-log-buffer.js";
|
|
|
22
22
|
const MAX_RESTART = 3;
|
|
23
23
|
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
24
24
|
const IS_WIN = process.platform === "win32";
|
|
25
|
-
const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
26
|
-
|
|
25
|
+
const SPAWN_STAGGER_MS = 30000; // syncCommands 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
26
|
+
// 代理切换后强制重启(restartAll)专用错峰间隔:切换出口后所有命令集中重建连接,
|
|
27
|
+
// 间隔给长一点,避免瞬时抢网络/触发风控(2026-08-29 按需加长)
|
|
28
|
+
const RESTART_STOP_STAGGER_MS = 20000; // 停止错峰 20s(杀 Edge/进程树瞬时占资源,错峰释放)
|
|
29
|
+
const RESTART_SPAWN_STAGGER_MS = 60000; // 启动错峰 60s
|
|
27
30
|
const STALE_LOG_TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟无日志输出视为假死
|
|
28
31
|
|
|
29
32
|
// === 控制台日志格式化(仅显示层增强,不影响任何功能链路) ===
|
|
@@ -739,9 +742,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
739
742
|
console.error(
|
|
740
743
|
`[watchdog] ${reason},强制重启全部命令 (${toRestart.length}个)...`,
|
|
741
744
|
);
|
|
742
|
-
// 错峰停止:第一个立即停,之后每个间隔
|
|
745
|
+
// 错峰停止:第一个立即停,之后每个间隔 RESTART_STOP_STAGGER_MS(平滑释放资源,避免同时杀 N 个浏览器)
|
|
743
746
|
for (let i = 0; i < toRestart.length; i++) {
|
|
744
|
-
if (i > 0)
|
|
747
|
+
if (i > 0)
|
|
748
|
+
await new Promise((r) => setTimeout(r, RESTART_STOP_STAGGER_MS));
|
|
745
749
|
if (this.shutdown) {
|
|
746
750
|
console.error("[watchdog] 看门狗已停止,中止停止命令");
|
|
747
751
|
break;
|
|
@@ -751,9 +755,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
751
755
|
`[watchdog] 命令 ${toRestart[i].id} 已停止 (${i + 1}/${toRestart.length})`,
|
|
752
756
|
);
|
|
753
757
|
}
|
|
754
|
-
// 错峰逐个重启:第一个立即起,之后每个间隔
|
|
758
|
+
// 错峰逐个重启:第一个立即起,之后每个间隔 RESTART_SPAWN_STAGGER_MS
|
|
755
759
|
for (let i = 0; i < toRestart.length; i++) {
|
|
756
|
-
if (i > 0)
|
|
760
|
+
if (i > 0)
|
|
761
|
+
await new Promise((r) => setTimeout(r, RESTART_SPAWN_STAGGER_MS));
|
|
757
762
|
if (this.shutdown) {
|
|
758
763
|
console.error("[watchdog] 看门狗已停止,中止重启命令");
|
|
759
764
|
break;
|