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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.22",
3
+ "version": "1.4.23",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -541,7 +541,7 @@ export async function handleRefresh(options) {
541
541
 
542
542
  const payload = {
543
543
  userInfo: refreshUserInfo,
544
- topRecentVideo: result.topRecentVideo || null,
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
- topRecentVideo: null,
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) {
@@ -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
  }
@@ -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) =====
@@ -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 表 =====
@@ -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
- _cachedApiKey = dbConfig.llm_apikey || process.env.APIKEY || "";
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
- const AUTO_POOL = [
168
- "qwen-turbo",
169
- "qwen-plus",
170
- "qwen3.6-flash",
171
- "qwen3.6-27b",
172
- "qwen3.5-plus",
173
- "qwen3.6-plus",
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 = [...AUTO_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 idx = _autoRoundIdx % AUTO_POOL.length;
240
+ const pool = buildAutoPool();
241
+ const idx = _autoRoundIdx % pool.length;
206
242
  _autoRoundIdx++;
207
- return AUTO_POOL[idx];
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(跳过提示字符串:含 "..." 且长度 < 20 的视为 hint,不覆盖)
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("llm_apikey", updates.apiKey);
310
+ saveToDb(`llm_apikey_${_cachedProvider}`, updates.apiKey);
261
311
  }
262
312
  }
263
313
 
@@ -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
- const topPlayCount =
2382
- u.topVideoPlayCount != null && u.topVideoPlayCount >= 1000
2383
- ? formatNum(u.topVideoPlayCount)
2384
- : "-";
2385
- const adTag = u.topVideoIsAd
2386
- ? ' <span style="color:red">(AD)</span>'
2387
- : "";
2388
- const topPlayCountCell = u.topVideoHref
2389
- ? `<td data-label="最大播放量" style="font-size:11px;color:#888"><a href="${u.topVideoHref}" target="_blank" style="color:#3b82f6;text-decoration:none" title="点击查看视频">${topPlayCount}${adTag}</a></td>`
2390
- : `<td data-label="最大播放量" style="font-size:11px;color:#888">${topPlayCount}${adTag}</td>`;
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">${latestVideo}</td>
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 = true;
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
- renderTargetTable();
3216
+ // 重新从服务端加载(带排序参数)
3217
+ loadTargetUsersPage(false);
3181
3218
  });
3182
3219
  });
3183
3220
  }
@@ -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