tt-help-cli-ycl 1.4.10 → 1.4.12
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/comments.js +139 -17
- package/src/watch/data-store.js +4 -12
- package/src/watch/db-stats.js +4 -4
- package/src/watch/public/app.js +1 -35
- package/src/watch/public/index.html +0 -5
package/package.json
CHANGED
package/src/cli/comments.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// 手动模式: tt-help comments <URL> [N] [--save] 单视频评论提取
|
|
6
6
|
// ────────────────────────────────────────
|
|
7
7
|
|
|
8
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
8
9
|
import os from "os";
|
|
10
|
+
import path from "path";
|
|
9
11
|
import { execSync } from "child_process";
|
|
10
12
|
|
|
11
13
|
import { fetchUserCommentsAPI } from "../lib/api-interceptor-comment.js";
|
|
@@ -65,6 +67,10 @@ const CFG = {
|
|
|
65
67
|
periodicRecycleEvery: 0,
|
|
66
68
|
/** 内存回收 — 回收时是否切换端口 */
|
|
67
69
|
switchPortOnRecycle: true,
|
|
70
|
+
/** 连续任务处理错误阈值(达到后上报服务端) */
|
|
71
|
+
consecutiveTaskErrorsMax: 20,
|
|
72
|
+
/** 单端口最大运行时间(ms),超时自动切换端口(默认 48 分钟) */
|
|
73
|
+
maxSessionRunTimeMs: 48 * 60 * 1000,
|
|
68
74
|
};
|
|
69
75
|
|
|
70
76
|
/** 从环境变量覆写配置 */
|
|
@@ -97,6 +103,14 @@ function loadEnvConfig() {
|
|
|
97
103
|
);
|
|
98
104
|
CFG.switchPortOnRecycle =
|
|
99
105
|
String(process.env.COMMENTS_SWITCH_PORT_ON_RECYCLE || "1") !== "0";
|
|
106
|
+
CFG.consecutiveTaskErrorsMax = Math.max(
|
|
107
|
+
1,
|
|
108
|
+
int("COMMENTS_CONSECUTIVE_TASK_ERRORS_MAX", CFG.consecutiveTaskErrorsMax),
|
|
109
|
+
);
|
|
110
|
+
CFG.maxSessionRunTimeMs = Math.max(
|
|
111
|
+
60000,
|
|
112
|
+
int("COMMENTS_MAX_SESSION_RUN_TIME_MS", CFG.maxSessionRunTimeMs),
|
|
113
|
+
);
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
// ────────────────────────────────────────
|
|
@@ -108,32 +122,82 @@ function formatMemoryUsage(mem = process.memoryUsage()) {
|
|
|
108
122
|
return `rss:${mb(mem.rss)}MB heap:${mb(mem.heapUsed)}MB ext:${mb(mem.external)}MB ab:${mb(mem.arrayBuffers)}MB`;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
|
|
125
|
+
/**
|
|
126
|
+
* 获取 Edge 浏览器进程的内存占用(跨平台:macOS/Linux/Windows)
|
|
127
|
+
*/
|
|
128
|
+
function getBrowserMemory(port = null) {
|
|
112
129
|
try {
|
|
113
130
|
const platform = os.platform();
|
|
114
|
-
if (platform !== "darwin" && platform !== "linux") return null;
|
|
115
|
-
const procName = platform === "darwin" ? "Microsoft Edge" : "msedge";
|
|
116
|
-
const c = procName[0];
|
|
117
|
-
const rest = procName.slice(1);
|
|
118
131
|
let cmd;
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
if (platform === "darwin") {
|
|
133
|
+
if (port) {
|
|
134
|
+
cmd = `ps aux | grep "[M]icrosoft Edge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
|
|
135
|
+
} else {
|
|
136
|
+
cmd =
|
|
137
|
+
'ps aux | grep "[M]icrosoft Edge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
|
|
138
|
+
}
|
|
139
|
+
const result = execSync(cmd, { timeout: 5000 }).toString().trim();
|
|
140
|
+
const mb = parseInt(result, 10);
|
|
141
|
+
return mb > 0 ? mb : null;
|
|
142
|
+
} else if (platform === "linux") {
|
|
143
|
+
if (port) {
|
|
144
|
+
cmd = `ps aux | grep "[m]sedge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
|
|
145
|
+
} else {
|
|
146
|
+
cmd =
|
|
147
|
+
'ps aux | grep "[m]sedge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
|
|
148
|
+
}
|
|
149
|
+
const result = execSync(cmd, { timeout: 5000 }).toString().trim();
|
|
150
|
+
const mb = parseInt(result, 10);
|
|
151
|
+
return mb > 0 ? mb : null;
|
|
152
|
+
} else if (platform === "win32") {
|
|
153
|
+
const ps1Path = path.join(os.tmpdir(), `edge-mem-${Date.now()}.ps1`);
|
|
154
|
+
try {
|
|
155
|
+
let ps1Content;
|
|
156
|
+
if (port) {
|
|
157
|
+
ps1Content =
|
|
158
|
+
`$procs = Get-CimInstance Win32_Process -Filter "Name LIKE 'msedge%'" -ErrorAction SilentlyContinue; ` +
|
|
159
|
+
`$sum = 0; ` +
|
|
160
|
+
`foreach ($p in $procs) { ` +
|
|
161
|
+
` if ($p.CommandLine -and $p.CommandLine -like "*--remote-debugging-port=${port}*") { ` +
|
|
162
|
+
` $sum += $p.WorkingSetSize ` +
|
|
163
|
+
` } ` +
|
|
164
|
+
`}; ` +
|
|
165
|
+
`Write-Output $sum`;
|
|
166
|
+
} else {
|
|
167
|
+
ps1Content =
|
|
168
|
+
`$procs = Get-CimInstance Win32_Process -Filter "Name LIKE 'msedge%'" -ErrorAction SilentlyContinue; ` +
|
|
169
|
+
`$sum = 0; ` +
|
|
170
|
+
`foreach ($p in $procs) { $sum += $p.WorkingSetSize }; ` +
|
|
171
|
+
`Write-Output $sum`;
|
|
172
|
+
}
|
|
173
|
+
writeFileSync(ps1Path, ps1Content);
|
|
174
|
+
cmd = `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`;
|
|
175
|
+
const result = execSync(cmd, { timeout: 5000 }).toString().trim();
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(ps1Path);
|
|
178
|
+
} catch {}
|
|
179
|
+
const bytes = parseInt(result, 10);
|
|
180
|
+
const mb = bytes > 0 ? Math.round(bytes / 1048576) : 0;
|
|
181
|
+
return mb > 0 ? mb : null;
|
|
182
|
+
} catch {
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(ps1Path);
|
|
185
|
+
} catch {}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
123
188
|
}
|
|
124
|
-
|
|
125
|
-
return mb > 0 ? mb : null;
|
|
189
|
+
return null;
|
|
126
190
|
} catch {
|
|
127
191
|
return null;
|
|
128
192
|
}
|
|
129
193
|
}
|
|
130
194
|
|
|
131
|
-
function formatMemoryWithBrowser(port) {
|
|
195
|
+
function formatMemoryWithBrowser(port = null) {
|
|
132
196
|
const nodeMem = formatMemoryUsage();
|
|
133
197
|
const edgeMB = getBrowserMemory(port);
|
|
134
198
|
if (!edgeMB) return nodeMem;
|
|
135
|
-
const
|
|
136
|
-
return nodeMem
|
|
199
|
+
const portLabel = port ? `:${port}` : "";
|
|
200
|
+
return `${nodeMem} edge${portLabel}:${edgeMB}MB`;
|
|
137
201
|
}
|
|
138
202
|
|
|
139
203
|
// ────────────────────────────────────────
|
|
@@ -225,6 +289,7 @@ class CdpSessionManager {
|
|
|
225
289
|
this.browser = null;
|
|
226
290
|
this.page = null;
|
|
227
291
|
this.lastRecycleAt = 0;
|
|
292
|
+
this.sessionStartTime = Date.now();
|
|
228
293
|
this._lastMemCheck = 0;
|
|
229
294
|
this._shuttingDown = false;
|
|
230
295
|
}
|
|
@@ -304,14 +369,28 @@ class CdpSessionManager {
|
|
|
304
369
|
this.browser = await ensureBrowserReady(opts);
|
|
305
370
|
this.page = await this._ensurePage();
|
|
306
371
|
this.lastRecycleAt = Date.now();
|
|
372
|
+
// 端口切换时重置运行计时
|
|
373
|
+
if (oldPort !== undefined && oldPort !== this.port) {
|
|
374
|
+
this.sessionStartTime = Date.now();
|
|
375
|
+
}
|
|
307
376
|
}
|
|
308
377
|
|
|
309
|
-
/** 内存检查 + 按需回收(每 30s 最多采样一次) */
|
|
378
|
+
/** 内存检查 + 定时轮换 + 按需回收(每 30s 最多采样一次) */
|
|
310
379
|
async maybeRecycleForMemory(processedCount) {
|
|
311
380
|
const now = Date.now();
|
|
312
381
|
if (now - this._lastMemCheck < 30000) return;
|
|
313
382
|
this._lastMemCheck = now;
|
|
314
383
|
|
|
384
|
+
// 定时轮换:单端口运行超时自动切换
|
|
385
|
+
const sessionElapsed = now - this.sessionStartTime;
|
|
386
|
+
if (sessionElapsed >= CFG.maxSessionRunTimeMs) {
|
|
387
|
+
const mins = Math.round(sessionElapsed / 60000);
|
|
388
|
+
await this.recycleSession(`定时轮换 (运行 ${mins} 分钟)`, {
|
|
389
|
+
switchPort: true,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
315
394
|
const mem = process.memoryUsage();
|
|
316
395
|
const rssMB = mem.rss / 1024 / 1024;
|
|
317
396
|
const heapMB = mem.heapUsed / 1024 / 1024;
|
|
@@ -481,7 +560,8 @@ async function runAutoMode(options) {
|
|
|
481
560
|
browserRestartCount = 0;
|
|
482
561
|
let consecutiveErrors = 0,
|
|
483
562
|
consecutive403 = 0,
|
|
484
|
-
consecutiveCaptchaCount = 0
|
|
563
|
+
consecutiveCaptchaCount = 0,
|
|
564
|
+
consecutiveTaskErrors = 0;
|
|
485
565
|
|
|
486
566
|
// ── CDP 会话 ──
|
|
487
567
|
const session = new CdpSessionManager({
|
|
@@ -720,15 +800,21 @@ async function runAutoMode(options) {
|
|
|
720
800
|
await apiPut(`${serverUrl}/api/comment-task/${id}`);
|
|
721
801
|
processedCount++;
|
|
722
802
|
consecutive403 = 0;
|
|
803
|
+
consecutiveTaskErrors = 0;
|
|
723
804
|
|
|
724
805
|
// 每 10 个任务输出内存汇总
|
|
725
806
|
if (totalTasks() % 10 === 0) {
|
|
807
|
+
const remainingMs =
|
|
808
|
+
session.sessionStartTime + CFG.maxSessionRunTimeMs - Date.now();
|
|
809
|
+
const remainingMins =
|
|
810
|
+
remainingMs > 0 ? Math.round(remainingMs / 60000) : 0;
|
|
726
811
|
console.error(
|
|
727
|
-
`── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount}`,
|
|
812
|
+
`── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距切换≈${remainingMins}min`,
|
|
728
813
|
);
|
|
729
814
|
}
|
|
730
815
|
} catch (err) {
|
|
731
816
|
errorCount++;
|
|
817
|
+
consecutiveTaskErrors++;
|
|
732
818
|
const isBrowserClosed = isBrowserClosedError(err);
|
|
733
819
|
const is403 =
|
|
734
820
|
err.message &&
|
|
@@ -754,6 +840,22 @@ async function runAutoMode(options) {
|
|
|
754
840
|
} else {
|
|
755
841
|
console.error(` ${err.message}`);
|
|
756
842
|
}
|
|
843
|
+
|
|
844
|
+
// 连续任务错误达到阈值,汇报服务端
|
|
845
|
+
if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
|
|
846
|
+
console.error(
|
|
847
|
+
` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
|
|
848
|
+
);
|
|
849
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
850
|
+
userId,
|
|
851
|
+
username: "",
|
|
852
|
+
errorType: "consecutive_task_errors",
|
|
853
|
+
errorMessage: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
|
|
854
|
+
stage: "comments",
|
|
855
|
+
errorStack: "",
|
|
856
|
+
}).catch(() => {});
|
|
857
|
+
consecutiveTaskErrors = 0;
|
|
858
|
+
}
|
|
757
859
|
}
|
|
758
860
|
|
|
759
861
|
// 任务间内存检查
|
|
@@ -761,6 +863,7 @@ async function runAutoMode(options) {
|
|
|
761
863
|
}
|
|
762
864
|
} catch (err) {
|
|
763
865
|
errorCount++;
|
|
866
|
+
consecutiveTaskErrors++;
|
|
764
867
|
if (isBrowserClosedError(err)) {
|
|
765
868
|
browserRestartCount++;
|
|
766
869
|
console.error(` ⚠ 浏览器断开 (#${browserRestartCount}) → 重启`);
|
|
@@ -768,6 +871,22 @@ async function runAutoMode(options) {
|
|
|
768
871
|
} else {
|
|
769
872
|
console.error(` × ${err.message}`);
|
|
770
873
|
}
|
|
874
|
+
|
|
875
|
+
// 连续轮间错误达到阈值,汇报服务端
|
|
876
|
+
if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
|
|
877
|
+
console.error(
|
|
878
|
+
` ⚠️ 连续轮间错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
|
|
879
|
+
);
|
|
880
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
881
|
+
userId,
|
|
882
|
+
username: "",
|
|
883
|
+
errorType: "consecutive_task_errors",
|
|
884
|
+
errorMessage: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
|
|
885
|
+
stage: "comments",
|
|
886
|
+
errorStack: "",
|
|
887
|
+
}).catch(() => {});
|
|
888
|
+
consecutiveTaskErrors = 0;
|
|
889
|
+
}
|
|
771
890
|
}
|
|
772
891
|
|
|
773
892
|
// 轮间内存检查 + 等待
|
|
@@ -794,7 +913,9 @@ async function runManualMode(url, options) {
|
|
|
794
913
|
save: shouldSave,
|
|
795
914
|
serverUrl = defaultServer,
|
|
796
915
|
basePort = 9222,
|
|
916
|
+
proxy: proxyOption,
|
|
797
917
|
} = options;
|
|
918
|
+
const effectiveProxy = proxyOption || configuredProxy;
|
|
798
919
|
|
|
799
920
|
let browser;
|
|
800
921
|
try {
|
|
@@ -930,5 +1051,6 @@ export async function handleComments(options) {
|
|
|
930
1051
|
save: commentsSave,
|
|
931
1052
|
serverUrl: commentsServer || defaultServer,
|
|
932
1053
|
basePort: commentsBasePort,
|
|
1054
|
+
proxy: commentsProxy,
|
|
933
1055
|
});
|
|
934
1056
|
}
|
package/src/watch/data-store.js
CHANGED
|
@@ -2647,8 +2647,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2647
2647
|
let sql = `
|
|
2648
2648
|
SELECT *
|
|
2649
2649
|
FROM jobs_base
|
|
2650
|
-
WHERE
|
|
2651
|
-
AND COALESCE(user_update_count, 0) <= 0
|
|
2650
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
2652
2651
|
`;
|
|
2653
2652
|
const sqlParams = [];
|
|
2654
2653
|
|
|
@@ -2698,13 +2697,7 @@ export function createStore(filePath, options = {}) {
|
|
|
2698
2697
|
const pending = data
|
|
2699
2698
|
.filter((u) => {
|
|
2700
2699
|
const updateCount = u.userUpdateCount;
|
|
2701
|
-
//
|
|
2702
|
-
const ttSellerEligible =
|
|
2703
|
-
u.ttSeller === null ||
|
|
2704
|
-
u.ttSeller === undefined ||
|
|
2705
|
-
u.ttSeller === "" ||
|
|
2706
|
-
u.ttSeller === 1;
|
|
2707
|
-
if (!ttSellerEligible) return false;
|
|
2700
|
+
// jobs_base 中所有 user_update_count <= 0 的记录都可以领取
|
|
2708
2701
|
if (
|
|
2709
2702
|
updateCount === null ||
|
|
2710
2703
|
updateCount === undefined ||
|
|
@@ -3395,14 +3388,13 @@ export function createStore(filePath, options = {}) {
|
|
|
3395
3388
|
};
|
|
3396
3389
|
}
|
|
3397
3390
|
|
|
3398
|
-
// 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(
|
|
3391
|
+
// 5. 验证:统计 jobs_base 中可被 attach 领取的记录数(user_update_count<=0)
|
|
3399
3392
|
let available = 0;
|
|
3400
3393
|
try {
|
|
3401
3394
|
const row = db
|
|
3402
3395
|
.prepare(
|
|
3403
3396
|
`SELECT COUNT(*) as total FROM jobs_base
|
|
3404
|
-
WHERE
|
|
3405
|
-
AND COALESCE(user_update_count, 0) <= 0
|
|
3397
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
3406
3398
|
AND video_count = 0`,
|
|
3407
3399
|
)
|
|
3408
3400
|
.get();
|
package/src/watch/db-stats.js
CHANGED
|
@@ -50,7 +50,7 @@ export function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
50
50
|
|
|
51
51
|
const userUpdateTasksRow = db
|
|
52
52
|
.prepare(
|
|
53
|
-
`SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(
|
|
53
|
+
`SELECT COUNT(*) as userUpdateTasks FROM jobs_base WHERE COALESCE(user_update_count, 0) <= 0`,
|
|
54
54
|
)
|
|
55
55
|
.get();
|
|
56
56
|
|
|
@@ -147,7 +147,7 @@ export function getUserUpdateByCountryFromDb() {
|
|
|
147
147
|
.prepare(
|
|
148
148
|
`SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
|
|
149
149
|
FROM jobs_base
|
|
150
|
-
WHERE
|
|
150
|
+
WHERE COALESCE(user_update_count, 0) <= 0
|
|
151
151
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
152
152
|
ORDER BY count DESC`,
|
|
153
153
|
)
|
|
@@ -161,7 +161,7 @@ export function getAttachStuckByCountryFromDb() {
|
|
|
161
161
|
.prepare(
|
|
162
162
|
`SELECT COALESCE(guessed_location, '未知') as country, COUNT(*) as count
|
|
163
163
|
FROM jobs_base
|
|
164
|
-
WHERE
|
|
164
|
+
WHERE COALESCE(user_update_count, 0) = 1
|
|
165
165
|
GROUP BY COALESCE(guessed_location, '未知')
|
|
166
166
|
ORDER BY count DESC`,
|
|
167
167
|
)
|
|
@@ -194,7 +194,7 @@ export function restoreAttachStuckByCountry(country) {
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
const whereSql = `COALESCE(
|
|
197
|
+
const whereSql = `COALESCE(user_update_count, 0) = 1 AND COALESCE(guessed_location, '未知') = ?`;
|
|
198
198
|
const count =
|
|
199
199
|
db
|
|
200
200
|
.prepare(`SELECT COUNT(*) as c FROM jobs_base WHERE ${whereSql}`)
|
package/src/watch/public/app.js
CHANGED
|
@@ -163,7 +163,6 @@ function renderStats() {
|
|
|
163
163
|
if (statJobsBase)
|
|
164
164
|
statJobsBase.textContent = `jobs_base: ${formatStatNum(d.jobsBaseTotal || 0)}`;
|
|
165
165
|
flashEl("statError", d.errorUsers);
|
|
166
|
-
flashEl("statRestricted", d.restrictedUsers);
|
|
167
166
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
168
167
|
flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
|
|
169
168
|
flashEl("statRawJobs", d.rawJobs || 0);
|
|
@@ -376,7 +375,6 @@ function renderSourceChart(sources) {
|
|
|
376
375
|
following: "关注",
|
|
377
376
|
follower: "粉丝",
|
|
378
377
|
processed: "已处理",
|
|
379
|
-
restricted: "受限(跳过)",
|
|
380
378
|
error: "错误(待重试)",
|
|
381
379
|
noVideo: "无视频",
|
|
382
380
|
};
|
|
@@ -419,13 +417,10 @@ function renderTable(users) {
|
|
|
419
417
|
const nowStatus = u.status;
|
|
420
418
|
const changed =
|
|
421
419
|
wasStatus !== nowStatus &&
|
|
422
|
-
(nowStatus === "done" ||
|
|
423
|
-
nowStatus === "restricted" ||
|
|
424
|
-
nowStatus === "error");
|
|
420
|
+
(nowStatus === "done" || nowStatus === "error");
|
|
425
421
|
const rowClass = changed ? ' class="row-flash"' : "";
|
|
426
422
|
|
|
427
423
|
const statusTags = {
|
|
428
|
-
restricted: '<span class="tag error">受限(跳过)</span>',
|
|
429
424
|
error: '<span class="tag error">错误(待重试)</span>',
|
|
430
425
|
done: '<span class="tag processed">已完成</span>',
|
|
431
426
|
processing: '<span class="tag processing">处理中</span>',
|
|
@@ -991,33 +986,6 @@ async function resetJob(uniqueId) {
|
|
|
991
986
|
}
|
|
992
987
|
}
|
|
993
988
|
|
|
994
|
-
async function moveRestrictedToBase() {
|
|
995
|
-
const count = currentStats?.restrictedUsers || 0;
|
|
996
|
-
if (count === 0) {
|
|
997
|
-
showToast("没有受限用户");
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (!confirm(`确定将 ${count} 个受限用户移到 jobs_base 吗?`)) return;
|
|
1001
|
-
showLoading("迁移中...");
|
|
1002
|
-
try {
|
|
1003
|
-
const res = await fetch("/api/jobs/move-restricted-to-base", {
|
|
1004
|
-
method: "POST",
|
|
1005
|
-
});
|
|
1006
|
-
const data = await res.json();
|
|
1007
|
-
if (data.error) {
|
|
1008
|
-
showToast(data.error, true);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
showToast(`已迁移 ${data.moved} 个受限用户`);
|
|
1012
|
-
fetchStats();
|
|
1013
|
-
fetchUsers();
|
|
1014
|
-
} catch (e) {
|
|
1015
|
-
showToast("迁移失败: " + e.message, true);
|
|
1016
|
-
} finally {
|
|
1017
|
-
hideLoading();
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
989
|
async function batchResetErrors() {
|
|
1022
990
|
const errorUsers = currentUsers.filter((u) => u.status === "error");
|
|
1023
991
|
if (errorUsers.length === 0) {
|
|
@@ -1762,8 +1730,6 @@ function renderTargetTable() {
|
|
|
1762
1730
|
statusTag = '<span class="tag pending">待处理</span>';
|
|
1763
1731
|
else if (u.status === "error")
|
|
1764
1732
|
statusTag = '<span class="tag error">错误</span>';
|
|
1765
|
-
else if (u.status === "restricted")
|
|
1766
|
-
statusTag = '<span class="tag error">受限</span>';
|
|
1767
1733
|
else statusTag = u.status || "-";
|
|
1768
1734
|
|
|
1769
1735
|
return `<tr data-user="${u.uniqueId}">
|
|
@@ -41,10 +41,6 @@
|
|
|
41
41
|
<div class="label">错误</div>
|
|
42
42
|
<div class="value error" id="statError">0</div>
|
|
43
43
|
</div>
|
|
44
|
-
<div class="stat-card clickable" onclick="moveRestrictedToBase()">
|
|
45
|
-
<div class="label">受限</div>
|
|
46
|
-
<div class="value error" id="statRestricted">0</div>
|
|
47
|
-
</div>
|
|
48
44
|
<div class="stat-card clickable pending-card" id="statUserUpdateCard" onclick="navigateToUserUpdate()">
|
|
49
45
|
<div class="label">待补资料</div>
|
|
50
46
|
<div class="value target" id="statUserUpdateTasks">0</div>
|
|
@@ -122,7 +118,6 @@
|
|
|
122
118
|
<button data-filter="pending" onclick="setFilter('pending')">待处理</button>
|
|
123
119
|
<button data-filter="done" onclick="setFilter('done')">已完成</button>
|
|
124
120
|
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
125
|
-
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
126
121
|
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标商家</button>
|
|
127
122
|
<select id="targetLocationFilter" onchange="onTargetLocationChange()"
|
|
128
123
|
style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;display:none">
|