tt-help-cli-ycl 1.3.7 → 1.3.8
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/watch/public/index.html +144 -29
- package/src/watch/server.mjs +18 -1
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
.header h1 { font-size: 18px; color: #fe2c55; }
|
|
12
12
|
.header .meta { font-size: 12px; color: #888; }
|
|
13
13
|
.header .status { font-size: 12px; color: #4ade80; }
|
|
14
|
+
.script-link { font-size: 12px; color: #60a5fa; text-decoration: none; padding: 2px 8px; border: 1px solid #60a5fa; border-radius: 4px; }
|
|
15
|
+
.script-link:hover { background: #60a5fa; color: #fff; }
|
|
14
16
|
.stats { display: grid; grid-template-columns: repeat(7, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
15
17
|
.stat-card { background: #1a1a24; border-radius: 8px; padding: 16px; text-align: center; }
|
|
16
18
|
.stat-card .label { font-size: 12px; color: #888; margin-bottom: 8px; }
|
|
@@ -42,11 +44,22 @@
|
|
|
42
44
|
.controls button:hover { border-color: #fe2c55; color: #fff; }
|
|
43
45
|
.controls button.active { background: #fe2c55; color: #fff; border-color: #fe2c55; }
|
|
44
46
|
.add-users { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
|
45
|
-
.add-users input { flex: 1; padding: 6px 12px; border: 1px solid #333; border-radius: 6px; background: #0f0f13; color: #e0e0e0; font-size: 13px; outline: none; }
|
|
46
|
-
.add-users input:focus { border-color: #fe2c55; }
|
|
47
47
|
.add-users button { padding: 6px 16px; border: none; border-radius: 6px; background: #fe2c55; color: #fff; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
48
48
|
.add-users button:hover { background: #e61944; }
|
|
49
|
-
.
|
|
49
|
+
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
|
50
|
+
.modal { background: #1a1a24; border-radius: 12px; padding: 24px; width: 520px; max-width: 90vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
51
|
+
.modal h3 { font-size: 16px; color: #e0e0e0; margin-bottom: 6px; }
|
|
52
|
+
.modal .hint { font-size: 12px; color: #888; margin-bottom: 16px; }
|
|
53
|
+
.modal textarea { width: 100%; height: 180px; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #0f0f13; color: #e0e0e0; font-size: 13px; font-family: inherit; outline: none; resize: vertical; line-height: 1.6; }
|
|
54
|
+
.modal textarea:focus { border-color: #fe2c55; }
|
|
55
|
+
.modal textarea::placeholder { color: #555; }
|
|
56
|
+
.modal .preview { margin-top: 8px; font-size: 12px; color: #60a5fa; min-height: 20px; }
|
|
57
|
+
.modal .btn-row { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
|
58
|
+
.modal .btn-row button { padding: 8px 20px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
59
|
+
.modal .btn-cancel { background: #2a2a3a; color: #ccc; }
|
|
60
|
+
.modal .btn-cancel:hover { background: #333; }
|
|
61
|
+
.modal .btn-submit { background: #fe2c55; color: #fff; }
|
|
62
|
+
.modal .btn-submit:hover { background: #e61944; }
|
|
50
63
|
.toast { position: fixed; top: 16px; right: 16px; padding: 10px 20px; border-radius: 6px; font-size: 13px; z-index: 999; transition: opacity 0.3s; }
|
|
51
64
|
.toast.success { background: #166534; color: #fff; }
|
|
52
65
|
.toast.error { background: #991b1b; color: #fff; }
|
|
@@ -82,15 +95,46 @@
|
|
|
82
95
|
::-webkit-scrollbar { width: 6px; }
|
|
83
96
|
::-webkit-scrollbar-track { background: #1a1a24; }
|
|
84
97
|
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
85
|
-
@media (max-width: 768px) {
|
|
98
|
+
@media (max-width: 768px) {
|
|
99
|
+
body { padding: 8px; }
|
|
100
|
+
.header { flex-direction: column; gap: 6px; align-items: flex-start; }
|
|
101
|
+
.header h1 { font-size: 16px; }
|
|
102
|
+
.stats { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
|
103
|
+
.stat-card { padding: 10px; }
|
|
104
|
+
.stat-card .label { font-size: 11px; }
|
|
105
|
+
.stat-card .value { font-size: 18px; }
|
|
106
|
+
.charts { grid-template-columns: 1fr; }
|
|
107
|
+
.table-wrap { padding: 10px; }
|
|
108
|
+
.controls { flex-wrap: wrap; gap: 6px; }
|
|
109
|
+
.controls input { flex: 0 0 100%; width: 100%; }
|
|
110
|
+
.controls button { flex: 0 0 calc(33.33% - 4px); min-width: 0; text-align: center; white-space: nowrap; font-size: 11px; padding: 8px 4px; }
|
|
111
|
+
.controls select { flex: 0 0 100%; width: 100%; }
|
|
112
|
+
.table-scroll { max-height: none; overflow: visible; }
|
|
113
|
+
table, thead, tbody, th, td, tr { display: block; }
|
|
114
|
+
thead { display: none; }
|
|
115
|
+
tr { background: #22222e; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; border: 1px solid #2a2a3a; }
|
|
116
|
+
tr:hover { background: #2a2a3a; }
|
|
117
|
+
td { padding: 4px 0; border: none; text-align: left; position: relative; padding-left: 40%; font-size: 13px; }
|
|
118
|
+
td::before { content: attr(data-label); position: absolute; left: 0; width: 36%; text-align: right; color: #888; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
119
|
+
td.user-id { font-size: 15px; font-weight: 700; color: #60a5fa; padding-left: 0; border-bottom: 1px solid #2a2a3a; margin-bottom: 4px; padding-bottom: 6px; }
|
|
120
|
+
td.user-id::before { display: none; }
|
|
121
|
+
td.user-id:hover { color: #fe2c55; }
|
|
122
|
+
.add-users { justify-content: center; }
|
|
123
|
+
.modal { width: 95vw; padding: 16px; }
|
|
124
|
+
.modal textarea { height: 140px; }
|
|
125
|
+
}
|
|
86
126
|
</style>
|
|
87
127
|
</head>
|
|
88
128
|
<body>
|
|
89
|
-
<div class="header">
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</
|
|
129
|
+
<div class="header">
|
|
130
|
+
<h1>TikTok 采集监控</h1>
|
|
131
|
+
<div class="meta" id="fileMeta">加载中...</div>
|
|
132
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
133
|
+
<a href="/scripts/run-explore.sh" class="script-link" download>mac</a>
|
|
134
|
+
<a href="/scripts/run-explore.bat" class="script-link" download>windows</a>
|
|
135
|
+
<span class="status" id="lastUpdate">--</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
94
138
|
<div class="stats">
|
|
95
139
|
<div class="stat-card"><div class="label">总任务</div><div class="value total" id="statTotal">0</div></div>
|
|
96
140
|
<div class="stat-card"><div class="label">处理中</div><div class="value total" id="statProcessing">0</div></div>
|
|
@@ -113,9 +157,7 @@
|
|
|
113
157
|
<div class="table-wrap">
|
|
114
158
|
<h3>用户列表</h3>
|
|
115
159
|
<div class="add-users">
|
|
116
|
-
<
|
|
117
|
-
<button onclick="addUsers()">插入队列</button>
|
|
118
|
-
<span class="hint">插入到队列最前面优先处理</span>
|
|
160
|
+
<button onclick="openAddModal()">+ 插入队列</button>
|
|
119
161
|
</div>
|
|
120
162
|
<div id="toast" class="toast" style="display:none"></div>
|
|
121
163
|
<div class="controls">
|
|
@@ -127,6 +169,9 @@
|
|
|
127
169
|
<button data-filter="error" onclick="setFilter('error')">错误</button>
|
|
128
170
|
<button data-filter="restricted" onclick="setFilter('restricted')">受限</button>
|
|
129
171
|
<button data-filter="target" onclick="setFilter('target')" style="background:#7c3aed;color:#fff">目标用户</button>
|
|
172
|
+
<select id="locationFilter" onchange="onLocationChange()" style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
173
|
+
<option value="">全部国家</option>
|
|
174
|
+
</select>
|
|
130
175
|
</div>
|
|
131
176
|
<div class="table-scroll">
|
|
132
177
|
<table>
|
|
@@ -142,6 +187,7 @@ const COLORS = ['#fe2c55','#60a5fa','#4ade80','#facc15','#f97316','#a855f7','#ec
|
|
|
142
187
|
let currentFilter = 'all';
|
|
143
188
|
let currentStats = null;
|
|
144
189
|
let currentUsers = [];
|
|
190
|
+
let currentLocation = '';
|
|
145
191
|
let prevStatValues = {};
|
|
146
192
|
let prevUserMap = {};
|
|
147
193
|
|
|
@@ -150,6 +196,7 @@ async function fetchStats() {
|
|
|
150
196
|
const res = await fetch('/api/stats');
|
|
151
197
|
currentStats = await res.json();
|
|
152
198
|
renderStats();
|
|
199
|
+
renderLocationFilter();
|
|
153
200
|
} catch (e) {
|
|
154
201
|
document.getElementById('lastUpdate').textContent = '\u8fde\u63a5\u5931\u8d25';
|
|
155
202
|
}
|
|
@@ -165,6 +212,7 @@ async function fetchUsers() {
|
|
|
165
212
|
}
|
|
166
213
|
const search = document.getElementById('searchInput').value.trim();
|
|
167
214
|
if (search) params.set('search', search);
|
|
215
|
+
if (currentLocation) params.set('location', currentLocation);
|
|
168
216
|
params.set('limit', '200');
|
|
169
217
|
const res = await fetch('/api/users?' + params.toString());
|
|
170
218
|
const data = await res.json();
|
|
@@ -255,17 +303,24 @@ function renderTable(users) {
|
|
|
255
303
|
if (u.noVideo) extraTags.push('<span class="tag no-video">\u65e0\u89c6\u9891</span>');
|
|
256
304
|
if (u.keepFollow) extraTags.push('<span class="tag keep-follow">\u5173\u6ce8\u5df2\u4fdd\u7559</span>');
|
|
257
305
|
if (u.hasFollowData === false) extraTags.push('<span class="tag no-follow">\u5173\u6ce8\u672a\u83b7\u53d6</span>');
|
|
306
|
+
const nick = (u.nickname || '').replace(/</g, '<').replace(/>/g, '>');
|
|
307
|
+
const fans = u.followerCount != null ? formatNum(u.followerCount) : '-';
|
|
308
|
+
const videos = u.videoCount != null ? u.videoCount : '-';
|
|
309
|
+
const loc = u.locationCreated || '-';
|
|
310
|
+
const claimer = u.claimedBy || '-';
|
|
311
|
+
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : '-';
|
|
312
|
+
const procTime = u.processedAt ? formatTime(u.processedAt) : '-';
|
|
258
313
|
return `<tr${rowClass}>
|
|
259
|
-
<td class="user-id">@${u.uniqueId}</td>
|
|
260
|
-
<td>${
|
|
261
|
-
<td>${
|
|
262
|
-
<td>${
|
|
263
|
-
<td>${
|
|
264
|
-
<td>${sources || '-'}</td>
|
|
265
|
-
<td>${statusTag} ${extraTags.join(' ')}</td>
|
|
266
|
-
<td style="font-size:11px;color:#888">${
|
|
267
|
-
<td style="font-size:11px;color:#888">${
|
|
268
|
-
<td style="font-size:11px;color:#888">${
|
|
314
|
+
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
315
|
+
<td data-label="昵称">${nick}</td>
|
|
316
|
+
<td data-label="粉丝">${fans}</td>
|
|
317
|
+
<td data-label="视频">${videos}</td>
|
|
318
|
+
<td data-label="国家">${loc}</td>
|
|
319
|
+
<td data-label="来源">${sources || '-'}</td>
|
|
320
|
+
<td data-label="状态">${statusTag} ${extraTags.join(' ')}</td>
|
|
321
|
+
<td data-label="接收人" style="font-size:11px;color:#888">${claimer}</td>
|
|
322
|
+
<td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
|
|
323
|
+
<td data-label="提交时间" style="font-size:11px;color:#888">${procTime}</td>
|
|
269
324
|
</tr>`;
|
|
270
325
|
}).join('');
|
|
271
326
|
|
|
@@ -292,16 +347,76 @@ function setFilter(f) {
|
|
|
292
347
|
fetchUsers();
|
|
293
348
|
}
|
|
294
349
|
|
|
350
|
+
function renderLocationFilter() {
|
|
351
|
+
if (!currentStats || !currentStats.countryStats) return;
|
|
352
|
+
const sel = document.getElementById('locationFilter');
|
|
353
|
+
if (!sel) return;
|
|
354
|
+
const val = sel.value;
|
|
355
|
+
const entries = currentStats.countryStats.sort((a, b) => b.count - a.count);
|
|
356
|
+
sel.innerHTML = '<option value="">全部国家</option>' +
|
|
357
|
+
entries.map(c => `<option value="${c.country}"${val === c.country ? ' selected' : ''}>${c.country} (${c.count})</option>`).join('');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function onLocationChange() {
|
|
361
|
+
const sel = document.getElementById('locationFilter');
|
|
362
|
+
currentLocation = sel.value;
|
|
363
|
+
fetchUsers();
|
|
364
|
+
}
|
|
365
|
+
|
|
295
366
|
document.getElementById('searchInput').addEventListener('input', () => {
|
|
296
367
|
fetchUsers();
|
|
297
368
|
});
|
|
298
369
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
370
|
+
function parseUsernames(raw) {
|
|
371
|
+
return raw.split(/[,,\n\r]+/).map(s => s.replace(/^@/, '').trim()).filter(Boolean);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function openAddModal() {
|
|
375
|
+
let overlay = document.getElementById('addModalOverlay');
|
|
376
|
+
if (overlay) return;
|
|
377
|
+
overlay = document.createElement('div');
|
|
378
|
+
overlay.id = 'addModalOverlay';
|
|
379
|
+
overlay.className = 'modal-overlay';
|
|
380
|
+
overlay.innerHTML = `
|
|
381
|
+
<div class="modal">
|
|
382
|
+
<h3>插入用户到队列</h3>
|
|
383
|
+
<div class="hint">每行一个用户名,或用逗号分隔。支持 @username 或 username 格式。插入到队列最前面优先处理。</div>
|
|
384
|
+
<textarea id="modalUserInput" placeholder="例如: user1 user2 user3 或:user1, user2, user3"></textarea>
|
|
385
|
+
<div class="preview" id="modalPreview"></div>
|
|
386
|
+
<div class="btn-row">
|
|
387
|
+
<button class="btn-cancel" onclick="closeAddModal()">取消</button>
|
|
388
|
+
<button class="btn-submit" onclick="submitAddUsers()">确认插入</button>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
document.body.appendChild(overlay);
|
|
393
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeAddModal(); });
|
|
394
|
+
const ta = document.getElementById('modalUserInput');
|
|
395
|
+
ta.focus();
|
|
396
|
+
ta.addEventListener('input', () => {
|
|
397
|
+
const names = parseUsernames(ta.value);
|
|
398
|
+
const preview = document.getElementById('modalPreview');
|
|
399
|
+
preview.textContent = names.length ? `共 ${names.length} 个用户名` : '';
|
|
400
|
+
});
|
|
401
|
+
ta.addEventListener('keydown', e => {
|
|
402
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
submitAddUsers();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function closeAddModal() {
|
|
410
|
+
const overlay = document.getElementById('addModalOverlay');
|
|
411
|
+
if (overlay) overlay.remove();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function submitAddUsers() {
|
|
415
|
+
const ta = document.getElementById('modalUserInput');
|
|
416
|
+
const raw = ta.value.trim();
|
|
302
417
|
if (!raw) return;
|
|
303
418
|
|
|
304
|
-
const names = raw
|
|
419
|
+
const names = parseUsernames(raw);
|
|
305
420
|
if (names.length === 0) return;
|
|
306
421
|
|
|
307
422
|
try {
|
|
@@ -312,8 +427,8 @@ async function addUsers() {
|
|
|
312
427
|
});
|
|
313
428
|
const data = await res.json();
|
|
314
429
|
if (data.error) { showToast(data.error, true); return; }
|
|
430
|
+
closeAddModal();
|
|
315
431
|
showToast(data.message || `\u5df2\u63d2\u5165 ${data.added} \u4e2a\u7528\u6237`);
|
|
316
|
-
input.value = '';
|
|
317
432
|
fetchStats();
|
|
318
433
|
fetchUsers();
|
|
319
434
|
} catch (e) {
|
|
@@ -329,8 +444,8 @@ function showToast(msg, isError) {
|
|
|
329
444
|
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
330
445
|
}
|
|
331
446
|
|
|
332
|
-
document.
|
|
333
|
-
if (e.key === '
|
|
447
|
+
document.addEventListener('keydown', e => {
|
|
448
|
+
if (e.key === 'Escape') closeAddModal();
|
|
334
449
|
});
|
|
335
450
|
|
|
336
451
|
document.getElementById('userTable').addEventListener('click', e => {
|
package/src/watch/server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
@@ -233,6 +233,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
233
233
|
(u.nickname || '').toLowerCase().includes(s)
|
|
234
234
|
);
|
|
235
235
|
}
|
|
236
|
+
if (params.location) {
|
|
237
|
+
filtered = filtered.filter(u => u.locationCreated === params.location);
|
|
238
|
+
}
|
|
236
239
|
|
|
237
240
|
const statusOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
|
|
238
241
|
filtered.sort((a, b) => {
|
|
@@ -261,6 +264,20 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
261
264
|
return;
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
const scriptMatch = routePath.match(/^\/scripts\/(.+)$/);
|
|
268
|
+
if (req.method === 'GET' && scriptMatch) {
|
|
269
|
+
const batDir = join(__dirname, '../../bat');
|
|
270
|
+
const scriptFile = join(batDir, scriptMatch[1]);
|
|
271
|
+
if (existsSync(scriptFile)) {
|
|
272
|
+
const content = readFileSync(scriptFile);
|
|
273
|
+
const ext = scriptMatch[1].split('.').pop();
|
|
274
|
+
const mime = ext === 'sh' ? 'text/x-sh' : ext === 'bat' ? 'text/bat' : 'text/plain';
|
|
275
|
+
res.writeHead(200, { 'Content-Type': `${mime}; charset=utf-8` });
|
|
276
|
+
res.end(content);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
264
281
|
res.writeHead(404);
|
|
265
282
|
res.end('Not Found');
|
|
266
283
|
});
|