tt-help-cli-ycl 1.4.73 → 1.4.74

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.73",
3
+ "version": "1.4.74",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3510,13 +3510,17 @@ function renderWatchdogDashboard(clients, clientCommands) {
3510
3510
  if (uid) userClients[uid] = c;
3511
3511
  });
3512
3512
 
3513
- // 收集所有卡片:在线客户端 + 离线但有命令配置的 userId
3513
+ // 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
3514
3514
  var allCards = [];
3515
3515
  clients.forEach(function (c) {
3516
3516
  allCards.push(renderClientCard(c, clientCommands));
3517
3517
  });
3518
3518
  for (var uid in clientCommands) {
3519
- if (!userClients[uid]) {
3519
+ if (
3520
+ !userClients[uid] &&
3521
+ clientCommands[uid] &&
3522
+ clientCommands[uid].length > 0
3523
+ ) {
3520
3524
  allCards.push(renderOfflineCard(uid, clientCommands[uid]));
3521
3525
  }
3522
3526
  }
@@ -3526,11 +3530,40 @@ function renderWatchdogDashboard(clients, clientCommands) {
3526
3530
  if (fingerprint === _wdLastRendered) return;
3527
3531
  _wdLastRendered = fingerprint;
3528
3532
 
3533
+ // 注入动画样式(仅首次)
3534
+ if (!document.getElementById("wdAnimStyle")) {
3535
+ var style = document.createElement("style");
3536
+ style.id = "wdAnimStyle";
3537
+ style.textContent =
3538
+ "@keyframes wdFlash{0%{color:#4ade80}50%{color:#60a5fa}100%{color:inherit}}" +
3539
+ ".wd-flash{animation:wdFlash 1.5s ease}";
3540
+ document.head.appendChild(style);
3541
+ }
3542
+
3543
+ // 重建前扫描旧值(按 data-wd-key 索引)
3544
+ var prevVals = {};
3545
+ var oldCells = el.querySelectorAll("[data-wd-key]");
3546
+ for (var i = 0; i < oldCells.length; i++) {
3547
+ prevVals[oldCells[i].getAttribute("data-wd-key")] = oldCells[i].textContent;
3548
+ }
3549
+
3529
3550
  el.innerHTML = styled(
3530
3551
  "div",
3531
3552
  "display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
3532
3553
  allCards.join(""),
3533
3554
  );
3555
+
3556
+ // 只对变化的值加闪烁动画
3557
+ var newCells = el.querySelectorAll("[data-wd-key]");
3558
+ for (var i = 0; i < newCells.length; i++) {
3559
+ var key = newCells[i].getAttribute("data-wd-key");
3560
+ if (
3561
+ prevVals[key] !== undefined &&
3562
+ prevVals[key] !== newCells[i].textContent
3563
+ ) {
3564
+ newCells[i].className = "wd-flash";
3565
+ }
3566
+ }
3534
3567
  }
3535
3568
 
3536
3569
  function renderOfflineCard(userId, cmds) {
@@ -3766,6 +3799,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3766
3799
  var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
3767
3800
  var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
3768
3801
 
3802
+ var prefix = "wd-" + esc(userId) + "-" + idx;
3769
3803
  html += styled(
3770
3804
  "div",
3771
3805
  "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:10px;border-top:1px solid #1a1a2a",
@@ -3789,12 +3823,27 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3789
3823
  )
3790
3824
  : ""),
3791
3825
  ) +
3792
- styled("span", "color:#888", pid) +
3793
- styled("span", "color:#aaa", mem) +
3794
- styled("span", "color:#888", uptime) +
3795
- styled(
3826
+ tag(
3827
+ "span",
3828
+ { style: "color:#888", "data-wd-key": prefix + "-pid" },
3829
+ pid,
3830
+ ) +
3831
+ tag(
3832
+ "span",
3833
+ { style: "color:#aaa", "data-wd-key": prefix + "-mem" },
3834
+ mem,
3835
+ ) +
3836
+ tag(
3837
+ "span",
3838
+ { style: "color:#888", "data-wd-key": prefix + "-up" },
3839
+ uptime,
3840
+ ) +
3841
+ tag(
3796
3842
  "span",
3797
- "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
3843
+ {
3844
+ style: "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
3845
+ "data-wd-key": prefix + "-rst",
3846
+ },
3798
3847
  restart,
3799
3848
  ) +
3800
3849
  styled(
@@ -3862,6 +3911,8 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3862
3911
  // 未匹配到的运行中进程(孤儿)
3863
3912
  match.unmatched.forEach(function (p) {
3864
3913
  var sc = p.status === "running" ? "#4ade80" : "#f87171";
3914
+ var oid = p.pid || p.cmdId || "?";
3915
+ var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
3865
3916
  html += styled(
3866
3917
  "div",
3867
3918
  "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:10px;border-top:1px solid #1a1a2a;opacity:0.5",
@@ -3870,14 +3921,26 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3870
3921
  "color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
3871
3922
  "⚡ " + (p.name || p.cmdId),
3872
3923
  ) +
3873
- styled("span", "color:#888", p.pid || "-") +
3874
- styled(
3924
+ tag(
3875
3925
  "span",
3876
- "color:#aaa",
3926
+ { style: "color:#888", "data-wd-key": opre + "-pid" },
3927
+ p.pid || "-",
3928
+ ) +
3929
+ tag(
3930
+ "span",
3931
+ { style: "color:#aaa", "data-wd-key": opre + "-mem" },
3877
3932
  p.memoryMB != null ? p.memoryMB + "MB" : "-",
3878
3933
  ) +
3879
- styled("span", "color:#888", p.uptimeDisplay || p.uptimeSeconds + "s") +
3880
- styled("span", "color:#555", "0") +
3934
+ tag(
3935
+ "span",
3936
+ { style: "color:#888", "data-wd-key": opre + "-up" },
3937
+ p.uptimeDisplay || p.uptimeSeconds + "s",
3938
+ ) +
3939
+ tag(
3940
+ "span",
3941
+ { style: "color:#555", "data-wd-key": opre + "-rst" },
3942
+ "0",
3943
+ ) +
3881
3944
  styled("span", "color:#555;font-size:9px", "孤立"),
3882
3945
  );
3883
3946
  });
@@ -4072,17 +4135,40 @@ function wdToggleClientCmd(userId, idx) {
4072
4135
  wdSaveClientCmds(userId, myCmds);
4073
4136
  }
4074
4137
 
4138
+ var _wdSaving = false;
4139
+
4140
+ function wdShowSaving() {
4141
+ _wdSaving = true;
4142
+ var el = document.getElementById("wdSavingTip");
4143
+ if (!el) {
4144
+ el = document.createElement("div");
4145
+ el.id = "wdSavingTip";
4146
+ el.style.cssText =
4147
+ "position:fixed;top:60px;right:20px;background:#f59e0b;color:#000;padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;z-index:9998;pointer-events:none";
4148
+ document.body.appendChild(el);
4149
+ }
4150
+ el.textContent = "⏳ 处理中...";
4151
+ el.style.display = "block";
4152
+ }
4153
+ function wdHideSaving() {
4154
+ _wdSaving = false;
4155
+ var el = document.getElementById("wdSavingTip");
4156
+ if (el) el.style.display = "none";
4157
+ }
4158
+
4075
4159
  async function wdSaveClientCmds(userId, commands) {
4160
+ wdShowSaving();
4076
4161
  try {
4077
4162
  await fetch("/api/watchdog/client-commands", {
4078
4163
  method: "POST",
4079
4164
  headers: { "Content-Type": "application/json" },
4080
4165
  body: JSON.stringify({ userId: userId, commands: commands }),
4081
4166
  });
4167
+ wdHideSaving();
4082
4168
  fetchWatchdogData();
4083
4169
  } catch (e) {
4170
+ wdHideSaving();
4084
4171
  alert("保存失败: " + e.message);
4085
- // 失败时立即刷新恢复按钮状态
4086
4172
  fetchWatchdogData();
4087
4173
  }
4088
4174
  }
@@ -4092,9 +4178,23 @@ function wdDeleteClient(userId) {
4092
4178
  !confirm(
4093
4179
  "确定删除 " +
4094
4180
  userId +
4095
- " 的所有命令配置?\n\n删除后该客户端将不再出现在列表中。",
4181
+ " 及其所有命令配置?\n\n删除后该客户端将从列表中移除。",
4096
4182
  )
4097
4183
  )
4098
4184
  return;
4099
- wdSaveClientCmds(userId, []);
4185
+ wdShowSaving();
4186
+ fetch("/api/watchdog/delete-client", {
4187
+ method: "POST",
4188
+ headers: { "Content-Type": "application/json" },
4189
+ body: JSON.stringify({ userId: userId }),
4190
+ })
4191
+ .then(function () {
4192
+ wdHideSaving();
4193
+ fetchWatchdogData();
4194
+ })
4195
+ .catch(function (e) {
4196
+ wdHideSaving();
4197
+ alert("删除失败: " + e.message);
4198
+ fetchWatchdogData();
4199
+ });
4100
4200
  }
@@ -577,9 +577,9 @@
577
577
  placeholder="默认">
578
578
  </div>
579
579
  <div>
580
- <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">用户编号</label>
581
- <input id="wdCfgUserId"
582
- style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
580
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">用户编号(不可修改)</label>
581
+ <input id="wdCfgUserId" readonly
582
+ style="width:100%;padding:6px;background:#1a1a2a;border:1px solid #333;color:#666;border-radius:4px;font-size:11px;box-sizing:border-box;cursor:not-allowed"
583
583
  placeholder="自动生成">
584
584
  </div>
585
585
  <div>
@@ -134,7 +134,17 @@ function setClientCommands(userId, commands) {
134
134
  });
135
135
  const cfg = loadConfig();
136
136
  if (!cfg.clientCommands) cfg.clientCommands = {};
137
- cfg.clientCommands[userId] = cleaned;
137
+ if (cleaned.length === 0) {
138
+ delete cfg.clientCommands[userId];
139
+ console.error(
140
+ `[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已清空,卡片已删除`,
141
+ );
142
+ } else {
143
+ cfg.clientCommands[userId] = cleaned;
144
+ console.error(
145
+ `[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已更新: ${cleaned.length} 条`,
146
+ );
147
+ }
138
148
  saveConfig(cfg);
139
149
  }
140
150
 
@@ -279,8 +289,33 @@ async function handleAnalyzeLogs(params, body, res) {
279
289
  sendJSON(res, 200, { llmAnalysis });
280
290
  } catch (e) {
281
291
  console.error(`[watchdog] LLM 分析失败: ${e.message}`);
282
- sendJSON(res, 500, { error: e.message });
292
+ sendJSON(res, 200, { llmAnalysis: [] });
293
+ }
294
+ }
295
+
296
+ async function handleDeleteClient(params, body, res) {
297
+ const userId = body.userId;
298
+ if (!userId) {
299
+ sendJSON(res, 400, { error: "缺少 userId" });
300
+ return;
301
+ }
302
+ // 1. 删除命令配置
303
+ const cfg = loadConfig();
304
+ if (cfg.clientCommands) delete cfg.clientCommands[userId];
305
+ if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
306
+ saveConfig(cfg);
307
+ // 2. 从内存客户端列表中移除所有匹配的客户端
308
+ let removed = 0;
309
+ for (const [clientId, info] of clients) {
310
+ if ((info.config && info.config.userId) === userId) {
311
+ clients.delete(clientId);
312
+ removed++;
313
+ }
283
314
  }
315
+ console.error(
316
+ `[watchdog] ${userId} 已删除(${removed}个客户端,命令+配置已清除)`,
317
+ );
318
+ sendJSON(res, 200, { ok: true, removed });
284
319
  }
285
320
 
286
321
  // ── 公开 ──
@@ -340,5 +375,9 @@ export async function tryHandleWatchdogRoute(
340
375
  await handleAnalyzeLogs(params, body, res);
341
376
  return true;
342
377
  }
378
+ if (method === "POST" && routePath === "/api/watchdog/delete-client") {
379
+ await handleDeleteClient(params, body, res);
380
+ return true;
381
+ }
343
382
  return false;
344
383
  }
@@ -42,9 +42,21 @@ function applyClientConfig(newCfg) {
42
42
  ];
43
43
  let changed = false;
44
44
  for (const f of fields) {
45
- if (newCfg[f] !== undefined && newCfg[f] !== cfg[f]) {
46
- cfg[f] = newCfg[f];
47
- changed = true;
45
+ if (newCfg[f] !== undefined) {
46
+ var newVal = newCfg[f],
47
+ oldVal = cfg[f];
48
+ // 忽略无意义的零值/空值(避免每次心跳都写文件)
49
+ if (
50
+ (typeof newVal === "number" &&
51
+ newVal === 0 &&
52
+ oldVal === undefined) ||
53
+ (typeof newVal === "string" && newVal === "" && oldVal === undefined)
54
+ )
55
+ continue;
56
+ if (newVal !== oldVal) {
57
+ cfg[f] = newVal;
58
+ changed = true;
59
+ }
48
60
  }
49
61
  }
50
62
  if (changed) {
@@ -1,21 +1,12 @@
1
1
  /**
2
2
  * LLM 日志分析模块 — 供服务端调用(传入 llmFetch),或客户端独立使用(读环境变量)
3
- *
4
- * 分析结果通过心跳上报到 watch 服务端展示。
5
3
  */
6
4
 
7
5
  // ── 分析入口 ──
8
6
 
9
- /**
10
- * 对所有进程的日志调用 LLM 分析
11
- * @param {Array} processLogs — [{cmdId, logs[], uptimeSeconds, restartCount}]
12
- * @param {Function} [llmFetch] — 可选,服务端传入的 llmFetch;不传则读环境变量
13
- * @returns {Promise<Array<{cmdId, severity, summary, suggestions}>>}
14
- */
15
7
  export async function analyzeProcessLogs(processLogs, llmFetch) {
16
8
  if (!processLogs || processLogs.length === 0) return [];
17
9
 
18
- // 如果没有传入 llmFetch,使用内置的轻量调用(客户端模式)
19
10
  if (!llmFetch) {
20
11
  llmFetch = await createStandaloneFetch();
21
12
  if (!llmFetch) return [];
@@ -41,7 +32,7 @@ export async function analyzeProcessLogs(processLogs, llmFetch) {
41
32
  return results;
42
33
  }
43
34
 
44
- // ── 客户端独立模式(从环境变量读配置) ──
35
+ // ── 客户端独立模式 ──
45
36
 
46
37
  async function createStandaloneFetch() {
47
38
  try {
@@ -59,8 +50,6 @@ async function createStandaloneFetch() {
59
50
  const model = process.env.LLM_MODEL || fileCfg.llmModel || "auto";
60
51
  if (!apiKey) { console.error("[llm-analyzer] 未配置 LLM_API_KEY"); return null; }
61
52
  console.error(`[llm-analyzer] 客户端模式: ${model} @ ${url}`);
62
-
63
- // 返回兼容 llmFetch 签名的函数
64
53
  return async ({ messages, max_tokens = 256, temperature = 0.1 }) => {
65
54
  const resp = await fetch(url, {
66
55
  method: "POST",
@@ -79,34 +68,13 @@ async function createStandaloneFetch() {
79
68
  return null;
80
69
  }
81
70
  }
82
- },
83
- ],
84
- 256,
85
- );
86
- const parsed = parseResponse(content);
87
- console.error(
88
- `[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${parsed.severity}: ${parsed.summary}`,
89
- );
90
- results.push({ cmdId: proc.cmdId, ...parsed });
91
- } catch (e) {
92
- console.error(
93
- `[llm-analyzer] ${proc.cmdId.substring(0, 30)} 失败: ${e.message}`,
94
- );
95
- }
96
- }
97
- return results;
98
- } catch (e) {
99
- console.error(`[llm-analyzer] 异常: ${e.message}`);
100
- return [];
101
- }
102
- }
103
71
 
104
- function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
105
- const uptime =
106
- uptimeSeconds >= 3600
107
- ? Math.round(uptimeSeconds / 3600) + "h"
108
- : uptimeSeconds + "s";
72
+ // ── Prompt ──
109
73
 
74
+ function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
75
+ const uptime = uptimeSeconds >= 3600
76
+ ? Math.round(uptimeSeconds / 3600) + "h"
77
+ : uptimeSeconds + "s";
110
78
  return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
111
79
 
112
80
  进程信息:
@@ -127,38 +95,20 @@ ${logs || "(无日志)"}
127
95
  - 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
128
96
  }
129
97
 
130
- // ── 响应解析 ──
98
+ // ── 解析 ──
131
99
 
132
100
  function parseResponse(content) {
133
101
  try {
134
102
  const parsed = JSON.parse(content);
135
- return {
136
- severity: parsed.severity || "ok",
137
- summary: parsed.summary || "",
138
- suggestions: Array.isArray(parsed.suggestions)
139
- ? parsed.suggestions.slice(0, 3)
140
- : [],
141
- };
103
+ return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
142
104
  } catch {
143
- // 尝试提取 JSON 块
144
105
  const match = content.match(/\{[\s\S]*\}/);
145
106
  if (match) {
146
107
  try {
147
108
  const parsed = JSON.parse(match[0]);
148
- return {
149
- severity: parsed.severity || "ok",
150
- summary: parsed.summary || "",
151
- suggestions: Array.isArray(parsed.suggestions)
152
- ? parsed.suggestions.slice(0, 3)
153
- : [],
154
- };
109
+ return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
155
110
  } catch {}
156
111
  }
157
- // 解析失败:将原始内容作为 summary
158
- return {
159
- severity: "warn",
160
- summary: content.substring(0, 100),
161
- suggestions: [],
162
- };
112
+ return { severity: "warn", summary: content.substring(0, 100), suggestions: [] };
163
113
  }
164
114
  }