tt-help-cli-ycl 1.4.74 → 1.4.75

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.74",
3
+ "version": "1.4.75",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3503,27 +3503,52 @@ function renderWatchdogDashboard(clients, clientCommands) {
3503
3503
  return;
3504
3504
  }
3505
3505
 
3506
+ // 排序:按名称升序,离线排最后
3507
+ var sortedClients = clients.slice().sort(function (a, b) {
3508
+ var nameA = (
3509
+ (a.config && a.config.userId) ||
3510
+ a.hostname ||
3511
+ ""
3512
+ ).toLowerCase();
3513
+ var nameB = (
3514
+ (b.config && b.config.userId) ||
3515
+ b.hostname ||
3516
+ ""
3517
+ ).toLowerCase();
3518
+ // 在线优先
3519
+ if (a.online !== b.online) return a.online ? -1 : 1;
3520
+ return nameA.localeCompare(nameB);
3521
+ });
3522
+
3506
3523
  // 构建 userId → 在线客户端 映射
3507
3524
  var userClients = {};
3508
- clients.forEach(function (c) {
3525
+ sortedClients.forEach(function (c) {
3509
3526
  var uid = (c.config && c.config.userId) || "";
3510
3527
  if (uid) userClients[uid] = c;
3511
3528
  });
3512
3529
 
3513
3530
  // 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
3514
3531
  var allCards = [];
3515
- clients.forEach(function (c) {
3532
+ sortedClients.forEach(function (c) {
3516
3533
  allCards.push(renderClientCard(c, clientCommands));
3517
3534
  });
3535
+ // 离线卡也按名称排序
3536
+ var offlineUids = [];
3518
3537
  for (var uid in clientCommands) {
3519
3538
  if (
3520
3539
  !userClients[uid] &&
3521
3540
  clientCommands[uid] &&
3522
3541
  clientCommands[uid].length > 0
3523
3542
  ) {
3524
- allCards.push(renderOfflineCard(uid, clientCommands[uid]));
3543
+ offlineUids.push(uid);
3525
3544
  }
3526
3545
  }
3546
+ offlineUids.sort(function (a, b) {
3547
+ return a.toLowerCase().localeCompare(b.toLowerCase());
3548
+ });
3549
+ offlineUids.forEach(function (uid) {
3550
+ allCards.push(renderOfflineCard(uid, clientCommands[uid]));
3551
+ });
3527
3552
 
3528
3553
  // 数据指纹:只有变化时才重建卡片 HTML
3529
3554
  var fingerprint = JSON.stringify(allCards);
@@ -3570,7 +3595,7 @@ function renderOfflineCard(userId, cmds) {
3570
3595
  var cmdCount = cmds ? cmds.length : 0;
3571
3596
  return styled(
3572
3597
  "div",
3573
- "background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px;opacity:0.9",
3598
+ "background:#1e1e2e;border:2px solid #f87171;border-radius:12px;padding:16px;opacity:0.9",
3574
3599
  styled(
3575
3600
  "div",
3576
3601
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
@@ -3581,7 +3606,7 @@ function renderOfflineCard(userId, cmds) {
3581
3606
  "span",
3582
3607
  {
3583
3608
  style:
3584
- "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
3609
+ "color:#f87171;font-size:16px;font-weight:bold;cursor:pointer",
3585
3610
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3586
3611
  title: "点击编辑客户端配置",
3587
3612
  },
@@ -3592,28 +3617,38 @@ function renderOfflineCard(userId, cmds) {
3592
3617
  {
3593
3618
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3594
3619
  style:
3595
- "padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3620
+ "padding:1px 5px;font-size:12px;background:transparent;color:#f87171;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3596
3621
  title: "编辑客户端配置",
3597
3622
  },
3598
3623
  "⚙️",
3599
3624
  ) +
3625
+ tag(
3626
+ "button",
3627
+ {
3628
+ onclick: "wdClearLlm('" + esc(userId) + "')",
3629
+ style:
3630
+ "padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3631
+ title: "清除此客户端的 LLM 分析提示",
3632
+ },
3633
+ "🧹",
3634
+ ) +
3600
3635
  styled(
3601
3636
  "span",
3602
- "color:#555;font-size:11px;margin-left:8px",
3637
+ "color:#555;font-size:12px;margin-left:8px",
3603
3638
  "(未上线)",
3604
3639
  ),
3605
3640
  ) +
3606
3641
  styled(
3607
3642
  "div",
3608
3643
  "display:flex;gap:10px;align-items:center",
3609
- styled("span", "color:#f87171", "● 离线") +
3610
- styled("span", "color:#555;font-size:10px", "—") +
3644
+ styled("span", "color:#f87171;font-size:13px", "● 离线") +
3645
+ styled("span", "color:#555;font-size:12px", "—") +
3611
3646
  tag(
3612
3647
  "button",
3613
3648
  {
3614
3649
  onclick: "wdDeleteClient('" + esc(userId) + "')",
3615
3650
  style:
3616
- "padding:2px 8px;font-size:10px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer",
3651
+ "padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer",
3617
3652
  title: "删除此客户端配置",
3618
3653
  },
3619
3654
  "删除",
@@ -3622,7 +3657,7 @@ function renderOfflineCard(userId, cmds) {
3622
3657
  ) +
3623
3658
  styled(
3624
3659
  "div",
3625
- "color:#555;font-size:10px",
3660
+ "color:#555;font-size:12px",
3626
3661
  "该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
3627
3662
  ) +
3628
3663
  renderCmdRows(cmds || [], userId, [], false),
@@ -3665,6 +3700,7 @@ function renderClientCard(c, clientCommands) {
3665
3700
  procs = c.processes || [];
3666
3701
  var userId = cfg.userId || "";
3667
3702
  var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
3703
+ var online = c.online;
3668
3704
 
3669
3705
  var osStr = [
3670
3706
  os.platform,
@@ -3683,11 +3719,15 @@ function renderClientCard(c, clientCommands) {
3683
3719
  (os.nodeVersion || "?") +
3684
3720
  " | 运行 " +
3685
3721
  (os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
3686
- var onlineBadge = c.online
3722
+ var onlineBadge = online
3687
3723
  ? styled("span", "color:#4ade80", "● 在线")
3688
3724
  : styled("span", "color:#f87171", "● 离线");
3689
3725
  var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
3690
3726
 
3727
+ // 离线时标题和边框变红
3728
+ var titleColor = online ? "#60a5fa" : "#f87171";
3729
+ var borderColor = online ? "#333" : "#f87171";
3730
+
3691
3731
  var header = styled(
3692
3732
  "div",
3693
3733
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
@@ -3697,7 +3737,10 @@ function renderClientCard(c, clientCommands) {
3697
3737
  tag(
3698
3738
  "span",
3699
3739
  {
3700
- style: "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
3740
+ style:
3741
+ "color:" +
3742
+ titleColor +
3743
+ ";font-size:16px;font-weight:bold;cursor:pointer",
3701
3744
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3702
3745
  title: "点击编辑客户端配置",
3703
3746
  },
@@ -3708,30 +3751,44 @@ function renderClientCard(c, clientCommands) {
3708
3751
  {
3709
3752
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3710
3753
  style:
3711
- "padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3754
+ "padding:1px 5px;font-size:12px;background:transparent;color:" +
3755
+ titleColor +
3756
+ ";border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3712
3757
  title: "编辑客户端配置",
3713
3758
  },
3714
3759
  "⚙️",
3715
3760
  ) +
3761
+ (userId
3762
+ ? tag(
3763
+ "button",
3764
+ {
3765
+ onclick: "wdClearLlm('" + esc(userId) + "')",
3766
+ style:
3767
+ "padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3768
+ title: "清除此客户端的 LLM 分析提示",
3769
+ },
3770
+ "🧹",
3771
+ )
3772
+ : "") +
3716
3773
  styled(
3717
3774
  "span",
3718
- "color:#555;font-size:10px;margin-left:8px",
3775
+ "color:#555;font-size:12px;margin-left:8px",
3719
3776
  c.hostname || "",
3720
3777
  ),
3721
3778
  ) +
3722
3779
  styled(
3723
3780
  "div",
3724
3781
  "display:flex;gap:10px;align-items:center",
3725
- styled("span", "color:#888;font-size:11px", c.ip) +
3782
+ styled("span", "color:#888;font-size:13px", c.ip) +
3726
3783
  onlineBadge +
3727
- styled("span", "color:#666;font-size:10px", lastSeen) +
3728
- (!c.online && userId
3784
+ styled("span", "color:#666;font-size:12px", lastSeen) +
3785
+ (!online && userId
3729
3786
  ? tag(
3730
3787
  "button",
3731
3788
  {
3732
3789
  onclick: "wdDeleteClient('" + esc(userId) + "')",
3733
3790
  style:
3734
- "padding:2px 8px;font-size:10px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer;margin-left:6px",
3791
+ "padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer;margin-left:6px",
3735
3792
  title: "删除此客户端配置",
3736
3793
  },
3737
3794
  "删除",
@@ -3742,11 +3799,13 @@ function renderClientCard(c, clientCommands) {
3742
3799
 
3743
3800
  return styled(
3744
3801
  "div",
3745
- "background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px",
3802
+ "background:#1e1e2e;border:2px solid " +
3803
+ borderColor +
3804
+ ";border-radius:12px;padding:16px",
3746
3805
  header +
3747
- styled("div", "color:#888;font-size:10px", osStr) +
3748
- styled("div", "color:#888;font-size:10px;margin-bottom:6px", infoLine) +
3749
- renderCmdRows(myCmds, userId, procs, c.online, c.llmAnalysis),
3806
+ styled("div", "color:#888;font-size:12px", osStr) +
3807
+ styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
3808
+ renderCmdRows(myCmds, userId, procs, online, c.llmAnalysis),
3750
3809
  );
3751
3810
  }
3752
3811
 
@@ -3761,7 +3820,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3761
3820
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
3762
3821
  styled(
3763
3822
  "span",
3764
- "font-size:11px;color:#888",
3823
+ "font-size:13px;color:#888",
3765
3824
  "命令 (" + myCmds.length + "条)",
3766
3825
  ) +
3767
3826
  tag(
@@ -3769,7 +3828,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3769
3828
  {
3770
3829
  onclick: "wdShowAddModal('" + esc(userId) + "')",
3771
3830
  style:
3772
- "padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600",
3831
+ "padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:600",
3773
3832
  },
3774
3833
  "+ 添加",
3775
3834
  ),
@@ -3779,7 +3838,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3779
3838
  // 表头
3780
3839
  html += styled(
3781
3840
  "div",
3782
- "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:9px;border-bottom:1px solid #222",
3841
+ "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:11px;border-bottom:1px solid #222",
3783
3842
  "<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
3784
3843
  );
3785
3844
 
@@ -3802,7 +3861,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3802
3861
  var prefix = "wd-" + esc(userId) + "-" + idx;
3803
3862
  html += styled(
3804
3863
  "div",
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",
3864
+ "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a",
3806
3865
  styled(
3807
3866
  "span",
3808
3867
  "color:" +
@@ -3818,7 +3877,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3818
3877
  (c.name
3819
3878
  ? styled(
3820
3879
  "span",
3821
- "color:#555;font-size:8px;margin-left:4px;font-family:monospace",
3880
+ "color:#555;font-size:10px;margin-left:4px;font-family:monospace",
3822
3881
  c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
3823
3882
  )
3824
3883
  : ""),
@@ -3856,7 +3915,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3856
3915
  onclick:
3857
3916
  "wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
3858
3917
  style:
3859
- "padding:1px 5px;font-size:9px;background:" +
3918
+ "padding:1px 5px;font-size:11px;background:" +
3860
3919
  (c.enabled ? "#f59e0b" : "#4ade80") +
3861
3920
  ";color:#000;border:none;border-radius:2px;cursor:pointer",
3862
3921
  title: c.enabled ? "停止" : "启动",
@@ -3869,7 +3928,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3869
3928
  {
3870
3929
  onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
3871
3930
  style:
3872
- "padding:1px 5px;font-size:9px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
3931
+ "padding:1px 5px;font-size:11px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
3873
3932
  title: "编辑",
3874
3933
  },
3875
3934
  "✎",
@@ -3879,7 +3938,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3879
3938
  {
3880
3939
  onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
3881
3940
  style:
3882
- "padding:1px 5px;font-size:9px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
3941
+ "padding:1px 5px;font-size:11px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
3883
3942
  title: "删除",
3884
3943
  },
3885
3944
  "✕",
@@ -3902,7 +3961,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3902
3961
  : "";
3903
3962
  html += styled(
3904
3963
  "div",
3905
- "font-size:9px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
3964
+ "font-size:11px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
3906
3965
  styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
3907
3966
  );
3908
3967
  }
@@ -3915,7 +3974,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3915
3974
  var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
3916
3975
  html += styled(
3917
3976
  "div",
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",
3977
+ "display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a;opacity:0.5",
3919
3978
  styled(
3920
3979
  "span",
3921
3980
  "color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
@@ -3941,7 +4000,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3941
4000
  { style: "color:#555", "data-wd-key": opre + "-rst" },
3942
4001
  "0",
3943
4002
  ) +
3944
- styled("span", "color:#555;font-size:9px", "孤立"),
4003
+ styled("span", "color:#555;font-size:11px", "孤立"),
3945
4004
  );
3946
4005
  });
3947
4006
 
@@ -4173,6 +4232,32 @@ async function wdSaveClientCmds(userId, commands) {
4173
4232
  }
4174
4233
  }
4175
4234
 
4235
+ function wdClearLlm(userId) {
4236
+ if (
4237
+ !confirm(
4238
+ "清除 " +
4239
+ userId +
4240
+ " 的所有 LLM 分析提示?\n\n下次 agent 心跳或分析周期会自动重新生成。",
4241
+ )
4242
+ )
4243
+ return;
4244
+ wdShowSaving();
4245
+ fetch("/api/watchdog/clear-llm", {
4246
+ method: "POST",
4247
+ headers: { "Content-Type": "application/json" },
4248
+ body: JSON.stringify({ userId: userId }),
4249
+ })
4250
+ .then(function () {
4251
+ wdHideSaving();
4252
+ fetchWatchdogData();
4253
+ })
4254
+ .catch(function (e) {
4255
+ wdHideSaving();
4256
+ alert("清除失败: " + e.message);
4257
+ fetchWatchdogData();
4258
+ });
4259
+ }
4260
+
4176
4261
  function wdDeleteClient(userId) {
4177
4262
  if (
4178
4263
  !confirm(
@@ -9,6 +9,7 @@ body {
9
9
  background: #0f0f13;
10
10
  color: #e0e0e0;
11
11
  padding: 16px;
12
+ font-size: 14px;
12
13
  }
13
14
 
14
15
  .header {
@@ -65,13 +66,13 @@ body {
65
66
  }
66
67
 
67
68
  .stat-card .label {
68
- font-size: 12px;
69
+ font-size: 14px;
69
70
  color: #888;
70
71
  margin-bottom: 8px;
71
72
  }
72
73
 
73
74
  .stat-card .value {
74
- font-size: 28px;
75
+ font-size: 32px;
75
76
  font-weight: 700;
76
77
  }
77
78
 
@@ -17,6 +17,10 @@ const clients = new Map();
17
17
  const CLIENT_TIMEOUT_MS = 60 * 1000;
18
18
  const CLIENT_CLEANUP_MS = 24 * 60 * 60 * 1000; // 离线超过 24h 清理
19
19
 
20
+ // LLM 分析缓存:cmdId → { severity, summary, suggestions, cachedAt }
21
+ const llmCache = new Map();
22
+ const LLM_CACHE_TTL = 15 * 60 * 1000; // 15 分钟过期
23
+
20
24
  // ── 配置文件读写 ──
21
25
 
22
26
  function loadConfig() {
@@ -56,9 +60,24 @@ function getOnlineClients() {
56
60
  console.error(`[watchdog] 清理离线客户端: ${clientId}`);
57
61
  }
58
62
  }
63
+ // 清理过期缓存
64
+ for (const [cmdId, entry] of llmCache) {
65
+ if (now - entry.cachedAt > LLM_CACHE_TTL) llmCache.delete(cmdId);
66
+ }
59
67
  const result = [];
60
68
  for (const [clientId, info] of clients) {
61
69
  const online = now - info.lastHeartbeat < CLIENT_TIMEOUT_MS;
70
+ // 合并:agent 上报的 llmAnalysis + 服务端缓存(以服务端缓存为准,更新)
71
+ const processes = info.processes || [];
72
+ const cachedAnalysis = [];
73
+ for (const p of processes) {
74
+ if (p.cmdId && llmCache.has(p.cmdId)) {
75
+ cachedAnalysis.push(llmCache.get(p.cmdId));
76
+ }
77
+ }
78
+ // 优先用服务端缓存(更新),fallback 到 agent 上报的
79
+ const llmAnalysis =
80
+ cachedAnalysis.length > 0 ? cachedAnalysis : info.llmAnalysis || [];
62
81
  result.push({
63
82
  clientId,
64
83
  hostname: info.hostname || "unknown",
@@ -66,7 +85,8 @@ function getOnlineClients() {
66
85
  ttHelpVersion: info.ttHelpVersion || "unknown",
67
86
  osInfo: info.osInfo || {},
68
87
  config: info.config || {},
69
- processes: info.processes || [],
88
+ processes,
89
+ llmAnalysis,
70
90
  lastHeartbeat: info.lastHeartbeat,
71
91
  registeredAt: info.registeredAt,
72
92
  online,
@@ -286,6 +306,11 @@ async function handleAnalyzeLogs(params, body, res) {
286
306
  console.error(
287
307
  `[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
288
308
  );
309
+ // 写入服务端缓存,前端直接读取,不再依赖 agent 转发
310
+ const now = Date.now();
311
+ for (const item of llmAnalysis) {
312
+ llmCache.set(item.cmdId, { ...item, cachedAt: now });
313
+ }
289
314
  sendJSON(res, 200, { llmAnalysis });
290
315
  } catch (e) {
291
316
  console.error(`[watchdog] LLM 分析失败: ${e.message}`);
@@ -318,6 +343,35 @@ async function handleDeleteClient(params, body, res) {
318
343
  sendJSON(res, 200, { ok: true, removed });
319
344
  }
320
345
 
346
+ async function handleClearLlmAnalysis(params, body, res) {
347
+ const userId = body.userId;
348
+ if (!userId) {
349
+ sendJSON(res, 400, { error: "缺少 userId" });
350
+ return;
351
+ }
352
+ // 收集该 userId 下所有客户端进程的 cmdId,从缓存中清除
353
+ let cleared = 0;
354
+ for (const [clientId, info] of clients) {
355
+ const uid = (info.config && info.config.userId) || "";
356
+ if (uid !== userId) continue;
357
+ for (const p of info.processes || []) {
358
+ if (p.cmdId && llmCache.has(p.cmdId)) {
359
+ llmCache.delete(p.cmdId);
360
+ cleared++;
361
+ }
362
+ }
363
+ }
364
+ // 同时清除 agent 上报的 llmAnalysis(如果存在)
365
+ for (const [clientId, info] of clients) {
366
+ const uid = (info.config && info.config.userId) || "";
367
+ if (uid === userId && info.llmAnalysis) {
368
+ info.llmAnalysis = [];
369
+ }
370
+ }
371
+ console.error(`[watchdog] ${userId} LLM 分析已清除(${cleared}条缓存)`);
372
+ sendJSON(res, 200, { ok: true, cleared });
373
+ }
374
+
321
375
  // ── 公开 ──
322
376
 
323
377
  export function registerWatchdogRoutes(httpServer) {
@@ -379,5 +433,9 @@ export async function tryHandleWatchdogRoute(
379
433
  await handleDeleteClient(params, body, res);
380
434
  return true;
381
435
  }
436
+ if (method === "POST" && routePath === "/api/watchdog/clear-llm") {
437
+ await handleClearLlmAnalysis(params, body, res);
438
+ return true;
439
+ }
382
440
  return false;
383
441
  }
@@ -212,7 +212,7 @@ export class WatchdogAgent {
212
212
  // 同步命令
213
213
  if (Array.isArray(data.commands)) {
214
214
  const prev = this.processManager.runningCount;
215
- this.processManager.syncCommands(data.commands);
215
+ await this.processManager.syncCommands(data.commands);
216
216
  const now = this.processManager.runningCount;
217
217
  if (prev !== now) {
218
218
  console.error(
@@ -19,6 +19,7 @@ import { join } from "path";
19
19
  const MAX_RESTART = 3;
20
20
  const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
21
21
  const IS_WIN = process.platform === "win32";
22
+ const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
22
23
 
23
24
  // 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
24
25
  function extractCdpPorts(cmdLine) {
@@ -385,9 +386,10 @@ export class ProcessManager extends EventEmitter {
385
386
  * 与期望命令列表同步:
386
387
  * - 启动期望列表中已启用但未运行的命令
387
388
  * - 停止正在运行但不在期望列表中的命令
389
+ * - 批量启动时每个进程间隔 SPAWN_STAGGER_MS,避免同时抢网络
388
390
  * @param {Array} expectedCmds - 服务端返回的命令列表,格式 [{cmd, maxUptimeSeconds, maxMemoryMB}]
389
391
  */
390
- syncCommands(expectedCmds) {
392
+ async syncCommands(expectedCmds) {
391
393
  if (!Array.isArray(expectedCmds)) return;
392
394
 
393
395
  // 标准化:使用服务端分配的 id,无 id 时用命令行截断兜底
@@ -428,12 +430,26 @@ export class ProcessManager extends EventEmitter {
428
430
  }
429
431
  }
430
432
 
431
- // 启动缺失的,并更新已有进程的阈值
433
+ // 收集需要新启动的命令
434
+ const toStart = normalized.filter((cmd) => !runningIds.has(cmd.id));
435
+
436
+ if (toStart.length > 1) {
437
+ console.error(
438
+ `[watchdog] 批量启动 ${toStart.length} 个命令(间隔 ${SPAWN_STAGGER_MS / 1000}s)...`,
439
+ );
440
+ }
441
+
442
+ // 启动缺失的,错峰延迟
443
+ for (let i = 0; i < toStart.length; i++) {
444
+ if (i > 0) {
445
+ await new Promise((r) => setTimeout(r, SPAWN_STAGGER_MS));
446
+ }
447
+ this.startCommand(toStart[i]);
448
+ }
449
+
450
+ // 更新已有进程的阈值(可能被管理员修改了)
432
451
  for (const cmd of normalized) {
433
- if (!runningIds.has(cmd.id)) {
434
- this.startCommand(cmd);
435
- } else {
436
- // 更新阈值(可能被管理员修改了)
452
+ if (runningIds.has(cmd.id)) {
437
453
  const info = this.processes.get(cmd.id);
438
454
  if (info) info.thresholds = cmd.thresholds;
439
455
  }