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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- .add-users .hint { font-size: 11px; color: #666; white-space: nowrap; }
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) { .stats { grid-template-columns: repeat(3, 1fr); } .charts { grid-template-columns: 1fr; } }
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
- <h1>TikTok 采集监控</h1>
91
- <span class="meta" id="fileMeta"></span>
92
- <span class="status" id="lastUpdate"></span>
93
- </div>
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
- <input type="text" id="addUserInput" placeholder="输入用户名,多个用逗号或空格分隔(如 @user1,@user2)">
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, '&lt;').replace(/>/g, '&gt;');
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>${(u.nickname || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</td>
261
- <td>${u.followerCount != null ? formatNum(u.followerCount) : '-'}</td>
262
- <td>${u.videoCount != null ? u.videoCount : '-'}</td>
263
- <td>${u.locationCreated || '-'}</td>
264
- <td>${sources || '-'}</td>
265
- <td>${statusTag} ${extraTags.join(' ')}</td>
266
- <td style="font-size:11px;color:#888">${u.claimedBy || '-'}</td>
267
- <td style="font-size:11px;color:#888">${u.claimedAt ? formatTime(u.claimedAt) : '-'}</td>
268
- <td style="font-size:11px;color:#888">${u.processedAt ? formatTime(u.processedAt) : '-'}</td>
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
- async function addUsers() {
300
- const input = document.getElementById('addUserInput');
301
- const raw = input.value.trim();
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="例如:&#10;user1&#10;user2&#10;user3&#10;&#10;或: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.split(/[,,\s]+/).map(s => s.replace(/^@/, '').trim()).filter(Boolean);
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.getElementById('addUserInput').addEventListener('keydown', e => {
333
- if (e.key === 'Enter') addUsers();
447
+ document.addEventListener('keydown', e => {
448
+ if (e.key === 'Escape') closeAddModal();
334
449
  });
335
450
 
336
451
  document.getElementById('userTable').addEventListener('click', e => {
@@ -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
  });