tt-help-cli-ycl 1.4.22 → 1.4.23
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/src/cli/refresh.js +1 -1
- package/src/scraper/explore-core.js +14 -4
- package/src/watch/data-store.js +78 -1
- package/src/watch/db-columns.js +2 -0
- package/src/watch/db-schema.js +1 -0
- package/src/watch/llm-config.js +66 -16
- package/src/watch/public/app.js +52 -15
- package/src/watch/server.js +5 -0
package/package.json
CHANGED
package/src/cli/refresh.js
CHANGED
|
@@ -541,7 +541,7 @@ export async function handleRefresh(options) {
|
|
|
541
541
|
|
|
542
542
|
const payload = {
|
|
543
543
|
userInfo: refreshUserInfo,
|
|
544
|
-
|
|
544
|
+
recentEcVideos: result.recentEcVideos || null,
|
|
545
545
|
discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
|
|
546
546
|
handle: Array.isArray(f) ? f[0] : f,
|
|
547
547
|
displayName: Array.isArray(f) ? f[1] : null,
|
|
@@ -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);
|
|
@@ -2414,6 +2463,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2414
2463
|
"uniqueId",
|
|
2415
2464
|
"sources",
|
|
2416
2465
|
"topRecentVideo", // 单独处理,不进入通用循环
|
|
2466
|
+
"recentEcVideos", // 单独处理,不进入通用循环
|
|
2417
2467
|
];
|
|
2418
2468
|
for (const key of Object.keys(result)) {
|
|
2419
2469
|
if (extraFields.includes(key)) continue;
|
|
@@ -2431,6 +2481,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2431
2481
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2432
2482
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2433
2483
|
}
|
|
2484
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2485
|
+
if (
|
|
2486
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2487
|
+
result.recentEcVideos.length > 0
|
|
2488
|
+
) {
|
|
2489
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2490
|
+
} else if (result.recentEcVideos === null) {
|
|
2491
|
+
user.recentEcVideos = null;
|
|
2492
|
+
}
|
|
2434
2493
|
user.sources = [...new Set([...(user.sources || []), "processed"])];
|
|
2435
2494
|
}
|
|
2436
2495
|
if (user.status !== oldStatus) markStatsDirty();
|
|
@@ -2658,6 +2717,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2658
2717
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2659
2718
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2660
2719
|
}
|
|
2720
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2721
|
+
if (
|
|
2722
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2723
|
+
result.recentEcVideos.length > 0
|
|
2724
|
+
) {
|
|
2725
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2726
|
+
} else if (result.recentEcVideos === null) {
|
|
2727
|
+
user.recentEcVideos = null;
|
|
2728
|
+
}
|
|
2661
2729
|
const newUsers = processDiscoveredUsers(result);
|
|
2662
2730
|
const ret = updateJobInfo(uniqueId, user, false);
|
|
2663
2731
|
if (ret.error) return { saved: false, error: ret.error };
|
|
@@ -2684,6 +2752,15 @@ export function createStore(filePath, options = {}) {
|
|
|
2684
2752
|
user.topVideoHref = result.topRecentVideo.href || null;
|
|
2685
2753
|
user.topVideoIsAd = result.topRecentVideo.isAd ?? null;
|
|
2686
2754
|
}
|
|
2755
|
+
// 将 recentEcVideos 数组序列化为 JSON 字符串
|
|
2756
|
+
if (
|
|
2757
|
+
Array.isArray(result.recentEcVideos) &&
|
|
2758
|
+
result.recentEcVideos.length > 0
|
|
2759
|
+
) {
|
|
2760
|
+
user.recentEcVideos = JSON.stringify(result.recentEcVideos);
|
|
2761
|
+
} else if (result.recentEcVideos === null) {
|
|
2762
|
+
user.recentEcVideos = null;
|
|
2763
|
+
}
|
|
2687
2764
|
const newUsers = processDiscoveredUsers(result);
|
|
2688
2765
|
return { saved: true, newUsers };
|
|
2689
2766
|
}
|
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
package/src/watch/llm-config.js
CHANGED
|
@@ -133,7 +133,12 @@ export function loadLlmConfig() {
|
|
|
133
133
|
process.env.LLM_MODEL ||
|
|
134
134
|
LLM_PROVIDERS[0].models[0].id;
|
|
135
135
|
_cachedProvider = dbConfig.llm_provider || LLM_PROVIDERS[0].id;
|
|
136
|
-
|
|
136
|
+
// 按 provider 隔离读取 API Key:优先 llm_apikey_{provider},兼容旧 llm_apikey,最后 env
|
|
137
|
+
_cachedApiKey =
|
|
138
|
+
dbConfig[`llm_apikey_${_cachedProvider}`] ||
|
|
139
|
+
dbConfig.llm_apikey ||
|
|
140
|
+
process.env.APIKEY ||
|
|
141
|
+
"";
|
|
137
142
|
_loaded = true;
|
|
138
143
|
|
|
139
144
|
console.error(
|
|
@@ -163,15 +168,45 @@ export function getLlmUrl() {
|
|
|
163
168
|
return _cachedUrl;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
// Auto
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
// Auto 模式的候选模型池缓存(从 DashScope provider 动态构建,排除 auto 自身)
|
|
172
|
+
let _autoPoolCache = null;
|
|
173
|
+
|
|
174
|
+
/** 从 DashScope provider 动态构建 auto 候选池(按优先级:flash/turbo > plus > 27b/35b > max/pro > 第三方) */
|
|
175
|
+
function buildAutoPool() {
|
|
176
|
+
if (_autoPoolCache) return _autoPoolCache;
|
|
177
|
+
const dashscope = LLM_PROVIDERS.find((p) => p.id === "dashscope");
|
|
178
|
+
if (!dashscope) return [];
|
|
179
|
+
const models = dashscope.models.filter((m) => m.id !== "auto");
|
|
180
|
+
|
|
181
|
+
const priority = (id) => {
|
|
182
|
+
if (/flash|turbo/i.test(id)) return 0;
|
|
183
|
+
if (/plus/i.test(id)) return 1;
|
|
184
|
+
if (/27b|35b/i.test(id)) return 2;
|
|
185
|
+
if (/max|pro/i.test(id)) return 3;
|
|
186
|
+
return 4; // deepseek, glm, kimi 等第三方
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// 记录原始顺序,排序时优先按 capability tier,同 tier 保持原始顺序
|
|
190
|
+
const indexed = models.map((m, i) => ({ id: m.id, idx: i }));
|
|
191
|
+
indexed.sort((a, b) => {
|
|
192
|
+
const pa = priority(a.id);
|
|
193
|
+
const pb = priority(b.id);
|
|
194
|
+
if (pa !== pb) return pa - pb;
|
|
195
|
+
return a.idx - b.idx;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
_autoPoolCache = indexed.map((m) => m.id);
|
|
199
|
+
console.error(
|
|
200
|
+
`[llm-config] auto 候选池已构建: ${_autoPoolCache.length} 个模型(${_autoPoolCache.slice(0, 5).join(", ")}...)`,
|
|
201
|
+
);
|
|
202
|
+
return _autoPoolCache;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 清除 auto 候选池缓存(provider 变更后调用) */
|
|
206
|
+
function clearAutoPoolCache() {
|
|
207
|
+
_autoPoolCache = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
175
210
|
let _lastAutoSuccess = null; // 上次成功的模型,下次优先用它
|
|
176
211
|
|
|
177
212
|
/** 是否处于 auto 模式 */
|
|
@@ -187,7 +222,7 @@ export function recordAutoSuccess(model) {
|
|
|
187
222
|
|
|
188
223
|
/** 获取 auto 模式候选池(上次成功的排最前,失败时依次尝试) */
|
|
189
224
|
export function getAutoPool() {
|
|
190
|
-
const pool = [...
|
|
225
|
+
const pool = [...buildAutoPool()];
|
|
191
226
|
if (_lastAutoSuccess) {
|
|
192
227
|
const idx = pool.indexOf(_lastAutoSuccess);
|
|
193
228
|
if (idx > 0) {
|
|
@@ -202,9 +237,10 @@ export function getAutoPool() {
|
|
|
202
237
|
/** 解析当前实际使用的 model(auto 模式下轮询选一个) */
|
|
203
238
|
function resolveModel() {
|
|
204
239
|
if (_cachedModel !== "auto") return _cachedModel;
|
|
205
|
-
const
|
|
240
|
+
const pool = buildAutoPool();
|
|
241
|
+
const idx = _autoRoundIdx % pool.length;
|
|
206
242
|
_autoRoundIdx++;
|
|
207
|
-
return
|
|
243
|
+
return pool[idx];
|
|
208
244
|
}
|
|
209
245
|
|
|
210
246
|
/** 获取当前 LLM Model(内部调用,auto 模式自动轮询) */
|
|
@@ -226,17 +262,31 @@ export function getLlmApiKey() {
|
|
|
226
262
|
export function updateLlmConfig(updates = {}) {
|
|
227
263
|
if (!_loaded) loadLlmConfig();
|
|
228
264
|
|
|
229
|
-
// Provider 变更:切换 URL,Model
|
|
265
|
+
// Provider 变更:切换 URL,Model 用指定的或默认第一个,同时加载对应 provider 的 API Key
|
|
230
266
|
if (updates.provider !== undefined) {
|
|
231
267
|
const provider = findProvider(updates.provider);
|
|
268
|
+
const prevProvider = _cachedProvider;
|
|
232
269
|
_cachedProvider = provider.id;
|
|
233
270
|
_cachedUrl = provider.url;
|
|
271
|
+
clearAutoPoolCache(); // provider 变了,auto 候选池也需重建
|
|
234
272
|
// 如果同时传了 model 且在 provider 中存在,用指定的;否则用第一个
|
|
235
273
|
const specifiedModel =
|
|
236
274
|
updates.model !== undefined
|
|
237
275
|
? provider.models.find((m) => m.id === updates.model)
|
|
238
276
|
: null;
|
|
239
277
|
_cachedModel = specifiedModel ? specifiedModel.id : provider.models[0].id;
|
|
278
|
+
// 切换到新 provider:从 DB 加载该 provider 的 API Key
|
|
279
|
+
if (provider.id !== prevProvider) {
|
|
280
|
+
const dbConfig = loadFromDb();
|
|
281
|
+
_cachedApiKey =
|
|
282
|
+
dbConfig[`llm_apikey_${provider.id}`] ||
|
|
283
|
+
dbConfig.llm_apikey ||
|
|
284
|
+
process.env.APIKEY ||
|
|
285
|
+
"";
|
|
286
|
+
console.error(
|
|
287
|
+
`[llm-config] 切换到 ${provider.id},API Key: ${_cachedApiKey ? "已加载" : "未设置"}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
240
290
|
saveToDb("llm_provider", provider.id);
|
|
241
291
|
saveToDb("llm_url", provider.url);
|
|
242
292
|
saveToDb("llm_model", _cachedModel);
|
|
@@ -252,12 +302,12 @@ export function updateLlmConfig(updates = {}) {
|
|
|
252
302
|
saveToDb("llm_url", updates.url);
|
|
253
303
|
}
|
|
254
304
|
|
|
255
|
-
// API Key
|
|
305
|
+
// API Key(按 provider 隔离存储,跳过提示字符串)
|
|
256
306
|
if (updates.apiKey !== undefined && updates.apiKey !== "") {
|
|
257
307
|
const isHint = updates.apiKey.includes("...") && updates.apiKey.length < 20;
|
|
258
308
|
if (!isHint) {
|
|
259
309
|
_cachedApiKey = updates.apiKey;
|
|
260
|
-
saveToDb(
|
|
310
|
+
saveToDb(`llm_apikey_${_cachedProvider}`, updates.apiKey);
|
|
261
311
|
}
|
|
262
312
|
}
|
|
263
313
|
|
package/src/watch/public/app.js
CHANGED
|
@@ -2289,6 +2289,10 @@ async function loadTargetUsersPage(append = false, skipLoading = false) {
|
|
|
2289
2289
|
if (currentTargetLocation)
|
|
2290
2290
|
url += `&country=${encodeURIComponent(currentTargetLocation)}`;
|
|
2291
2291
|
if (search) url += `&search=${encodeURIComponent(search)}`;
|
|
2292
|
+
if (currentTargetSort.key) {
|
|
2293
|
+
url += `&sort=${encodeURIComponent(currentTargetSort.key)}`;
|
|
2294
|
+
url += `&sortOrder=${currentTargetSort.asc ? "asc" : "desc"}`;
|
|
2295
|
+
}
|
|
2292
2296
|
|
|
2293
2297
|
const res = await fetch(url);
|
|
2294
2298
|
data = await res.json();
|
|
@@ -2329,9 +2333,9 @@ function renderTargetTable() {
|
|
|
2329
2333
|
const el = document.getElementById("targetTable");
|
|
2330
2334
|
const moreHint = document.getElementById("targetMoreHint");
|
|
2331
2335
|
|
|
2332
|
-
//
|
|
2336
|
+
// 应用本地排序(topVideoPlayCount 由服务端排序,此处跳过)
|
|
2333
2337
|
let displayUsers = currentTargetUsers;
|
|
2334
|
-
if (currentTargetSort.key) {
|
|
2338
|
+
if (currentTargetSort.key && currentTargetSort.key !== "topVideoPlayCount") {
|
|
2335
2339
|
displayUsers = [...currentTargetUsers].sort((a, b) => {
|
|
2336
2340
|
let va = a[currentTargetSort.key];
|
|
2337
2341
|
let vb = b[currentTargetSort.key];
|
|
@@ -2378,16 +2382,47 @@ function renderTargetTable() {
|
|
|
2378
2382
|
? formatTime(u.latestVideoTime * 1000)
|
|
2379
2383
|
: "-";
|
|
2380
2384
|
const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2385
|
+
|
|
2386
|
+
// 解析 recentEcVideos JSON,获取所有高播放 EC 视频
|
|
2387
|
+
let ecVideos = [];
|
|
2388
|
+
try {
|
|
2389
|
+
if (typeof u.recentEcVideos === "string") {
|
|
2390
|
+
ecVideos = JSON.parse(u.recentEcVideos);
|
|
2391
|
+
} else if (Array.isArray(u.recentEcVideos)) {
|
|
2392
|
+
ecVideos = u.recentEcVideos;
|
|
2393
|
+
}
|
|
2394
|
+
} catch (e) {
|
|
2395
|
+
ecVideos = [];
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// 最大播放量单元格:列出所有 playCount >= 1000 的视频
|
|
2399
|
+
const highPlayVideos = ecVideos.filter((v) => v.playCount >= 1000);
|
|
2400
|
+
let topPlayCountCell;
|
|
2401
|
+
if (highPlayVideos.length > 0) {
|
|
2402
|
+
const videoLinks = highPlayVideos
|
|
2403
|
+
.sort((a, b) => b.playCount - a.playCount)
|
|
2404
|
+
.map((v) => {
|
|
2405
|
+
const adTag = v.isAd ? ' <span style="color:red">(AD)</span>' : "";
|
|
2406
|
+
return `<a href="${v.href}" target="_blank" style="color:#3b82f6;text-decoration:none;display:block;line-height:1.6" title="点击查看视频">${formatNum(v.playCount)}${adTag}</a>`;
|
|
2407
|
+
})
|
|
2408
|
+
.join("");
|
|
2409
|
+
topPlayCountCell = `<td data-label="最大播放量" style="font-size:11px;color:#888">${videoLinks}</td>`;
|
|
2410
|
+
} else {
|
|
2411
|
+
topPlayCountCell = `<td data-label="最大播放量" style="font-size:11px;color:#888">-</td>`;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// 最近发布时间:仅当有 playCount >= 1000 的视频时才显示
|
|
2415
|
+
let latestEcTime;
|
|
2416
|
+
if (highPlayVideos.length > 0) {
|
|
2417
|
+
const maxCreateTime = ecVideos.reduce(
|
|
2418
|
+
(max, v) => (v.createTime > max ? v.createTime : max),
|
|
2419
|
+
0,
|
|
2420
|
+
);
|
|
2421
|
+
if (maxCreateTime > 0) {
|
|
2422
|
+
latestEcTime = formatTime(maxCreateTime * 1000);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
const latestVideoCell = latestEcTime || "-";
|
|
2391
2426
|
|
|
2392
2427
|
const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
|
|
2393
2428
|
const locationCell = u.modifiedAt
|
|
@@ -2414,7 +2449,7 @@ function renderTargetTable() {
|
|
|
2414
2449
|
${locationCell}
|
|
2415
2450
|
<td data-label="确认国家" style="font-size:11px">${confirmedLocation}</td>
|
|
2416
2451
|
${topPlayCountCell}
|
|
2417
|
-
<td data-label="最近发布" style="font-size:11px;color:#888">${
|
|
2452
|
+
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideoCell}</td>
|
|
2418
2453
|
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
2419
2454
|
<td data-label="操作">
|
|
2420
2455
|
<button class="btn-pending" onclick="moveToBase('${escapeJsString(u.uniqueId)}')" title="移到 jobs_base 重走流程">待确认</button>
|
|
@@ -3158,6 +3193,7 @@ function initTableSorting() {
|
|
|
3158
3193
|
}
|
|
3159
3194
|
|
|
3160
3195
|
function initTargetTableSorting() {
|
|
3196
|
+
console.log("[target-sort] 初始化排序监听...");
|
|
3161
3197
|
document.querySelectorAll("th.sortable-target").forEach((th) => {
|
|
3162
3198
|
th.addEventListener("click", () => {
|
|
3163
3199
|
const key = th.dataset.sort;
|
|
@@ -3166,7 +3202,7 @@ function initTargetTableSorting() {
|
|
|
3166
3202
|
currentTargetSort.asc = !currentTargetSort.asc;
|
|
3167
3203
|
} else {
|
|
3168
3204
|
currentTargetSort.key = key;
|
|
3169
|
-
currentTargetSort.asc =
|
|
3205
|
+
currentTargetSort.asc = false; // 默认降序(高播放在前)
|
|
3170
3206
|
}
|
|
3171
3207
|
// 更新排序指示器
|
|
3172
3208
|
document.querySelectorAll("th.sortable-target").forEach((h) => {
|
|
@@ -3177,7 +3213,8 @@ function initTargetTableSorting() {
|
|
|
3177
3213
|
th.classList.add(currentTargetSort.asc ? "sort-asc" : "sort-desc");
|
|
3178
3214
|
const icon = th.querySelector(".sort-icon");
|
|
3179
3215
|
if (icon) icon.textContent = currentTargetSort.asc ? "↑" : "↓";
|
|
3180
|
-
|
|
3216
|
+
// 重新从服务端加载(带排序参数)
|
|
3217
|
+
loadTargetUsersPage(false);
|
|
3181
3218
|
});
|
|
3182
3219
|
});
|
|
3183
3220
|
}
|
package/src/watch/server.js
CHANGED
|
@@ -993,6 +993,9 @@ export function startWatchServer(
|
|
|
993
993
|
followerCount: u.followerCount || 0,
|
|
994
994
|
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
995
995
|
topVideoHref: u.topVideoHref || null,
|
|
996
|
+
topVideoIsAd: u.topVideoIsAd ?? null,
|
|
997
|
+
recentEcVideos: u.recentEcVideos || null,
|
|
998
|
+
latestVideoTime: u.latestVideoTime || null,
|
|
996
999
|
}));
|
|
997
1000
|
sendJSON(res, 200, { total: targets.length, users });
|
|
998
1001
|
}
|
|
@@ -1078,6 +1081,8 @@ export function startWatchServer(
|
|
|
1078
1081
|
search: params.search || undefined,
|
|
1079
1082
|
limit: parseInt(params.limit, 10),
|
|
1080
1083
|
offset: parseInt(params.offset || "0", 10),
|
|
1084
|
+
sort: params.sort || undefined,
|
|
1085
|
+
sortOrder: params.sortOrder || "desc",
|
|
1081
1086
|
},
|
|
1082
1087
|
);
|
|
1083
1088
|
// 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
|