tt-help-cli-ycl 1.4.73 → 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.73",
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,41 +3503,99 @@ 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
- // 收集所有卡片:在线客户端 + 离线但有命令配置的 userId
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
- if (!userClients[uid]) {
3520
- allCards.push(renderOfflineCard(uid, clientCommands[uid]));
3538
+ if (
3539
+ !userClients[uid] &&
3540
+ clientCommands[uid] &&
3541
+ clientCommands[uid].length > 0
3542
+ ) {
3543
+ offlineUids.push(uid);
3521
3544
  }
3522
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
+ });
3523
3552
 
3524
3553
  // 数据指纹:只有变化时才重建卡片 HTML
3525
3554
  var fingerprint = JSON.stringify(allCards);
3526
3555
  if (fingerprint === _wdLastRendered) return;
3527
3556
  _wdLastRendered = fingerprint;
3528
3557
 
3558
+ // 注入动画样式(仅首次)
3559
+ if (!document.getElementById("wdAnimStyle")) {
3560
+ var style = document.createElement("style");
3561
+ style.id = "wdAnimStyle";
3562
+ style.textContent =
3563
+ "@keyframes wdFlash{0%{color:#4ade80}50%{color:#60a5fa}100%{color:inherit}}" +
3564
+ ".wd-flash{animation:wdFlash 1.5s ease}";
3565
+ document.head.appendChild(style);
3566
+ }
3567
+
3568
+ // 重建前扫描旧值(按 data-wd-key 索引)
3569
+ var prevVals = {};
3570
+ var oldCells = el.querySelectorAll("[data-wd-key]");
3571
+ for (var i = 0; i < oldCells.length; i++) {
3572
+ prevVals[oldCells[i].getAttribute("data-wd-key")] = oldCells[i].textContent;
3573
+ }
3574
+
3529
3575
  el.innerHTML = styled(
3530
3576
  "div",
3531
3577
  "display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
3532
3578
  allCards.join(""),
3533
3579
  );
3580
+
3581
+ // 只对变化的值加闪烁动画
3582
+ var newCells = el.querySelectorAll("[data-wd-key]");
3583
+ for (var i = 0; i < newCells.length; i++) {
3584
+ var key = newCells[i].getAttribute("data-wd-key");
3585
+ if (
3586
+ prevVals[key] !== undefined &&
3587
+ prevVals[key] !== newCells[i].textContent
3588
+ ) {
3589
+ newCells[i].className = "wd-flash";
3590
+ }
3591
+ }
3534
3592
  }
3535
3593
 
3536
3594
  function renderOfflineCard(userId, cmds) {
3537
3595
  var cmdCount = cmds ? cmds.length : 0;
3538
3596
  return styled(
3539
3597
  "div",
3540
- "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",
3541
3599
  styled(
3542
3600
  "div",
3543
3601
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
@@ -3548,7 +3606,7 @@ function renderOfflineCard(userId, cmds) {
3548
3606
  "span",
3549
3607
  {
3550
3608
  style:
3551
- "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
3609
+ "color:#f87171;font-size:16px;font-weight:bold;cursor:pointer",
3552
3610
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3553
3611
  title: "点击编辑客户端配置",
3554
3612
  },
@@ -3559,28 +3617,38 @@ function renderOfflineCard(userId, cmds) {
3559
3617
  {
3560
3618
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3561
3619
  style:
3562
- "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",
3563
3621
  title: "编辑客户端配置",
3564
3622
  },
3565
3623
  "⚙️",
3566
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
+ ) +
3567
3635
  styled(
3568
3636
  "span",
3569
- "color:#555;font-size:11px;margin-left:8px",
3637
+ "color:#555;font-size:12px;margin-left:8px",
3570
3638
  "(未上线)",
3571
3639
  ),
3572
3640
  ) +
3573
3641
  styled(
3574
3642
  "div",
3575
3643
  "display:flex;gap:10px;align-items:center",
3576
- styled("span", "color:#f87171", "● 离线") +
3577
- styled("span", "color:#555;font-size:10px", "—") +
3644
+ styled("span", "color:#f87171;font-size:13px", "● 离线") +
3645
+ styled("span", "color:#555;font-size:12px", "—") +
3578
3646
  tag(
3579
3647
  "button",
3580
3648
  {
3581
3649
  onclick: "wdDeleteClient('" + esc(userId) + "')",
3582
3650
  style:
3583
- "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",
3584
3652
  title: "删除此客户端配置",
3585
3653
  },
3586
3654
  "删除",
@@ -3589,7 +3657,7 @@ function renderOfflineCard(userId, cmds) {
3589
3657
  ) +
3590
3658
  styled(
3591
3659
  "div",
3592
- "color:#555;font-size:10px",
3660
+ "color:#555;font-size:12px",
3593
3661
  "该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
3594
3662
  ) +
3595
3663
  renderCmdRows(cmds || [], userId, [], false),
@@ -3632,6 +3700,7 @@ function renderClientCard(c, clientCommands) {
3632
3700
  procs = c.processes || [];
3633
3701
  var userId = cfg.userId || "";
3634
3702
  var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
3703
+ var online = c.online;
3635
3704
 
3636
3705
  var osStr = [
3637
3706
  os.platform,
@@ -3650,11 +3719,15 @@ function renderClientCard(c, clientCommands) {
3650
3719
  (os.nodeVersion || "?") +
3651
3720
  " | 运行 " +
3652
3721
  (os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
3653
- var onlineBadge = c.online
3722
+ var onlineBadge = online
3654
3723
  ? styled("span", "color:#4ade80", "● 在线")
3655
3724
  : styled("span", "color:#f87171", "● 离线");
3656
3725
  var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
3657
3726
 
3727
+ // 离线时标题和边框变红
3728
+ var titleColor = online ? "#60a5fa" : "#f87171";
3729
+ var borderColor = online ? "#333" : "#f87171";
3730
+
3658
3731
  var header = styled(
3659
3732
  "div",
3660
3733
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
@@ -3664,7 +3737,10 @@ function renderClientCard(c, clientCommands) {
3664
3737
  tag(
3665
3738
  "span",
3666
3739
  {
3667
- 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",
3668
3744
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3669
3745
  title: "点击编辑客户端配置",
3670
3746
  },
@@ -3675,30 +3751,44 @@ function renderClientCard(c, clientCommands) {
3675
3751
  {
3676
3752
  onclick: "wdShowConfigModal('" + esc(userId) + "')",
3677
3753
  style:
3678
- "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",
3679
3757
  title: "编辑客户端配置",
3680
3758
  },
3681
3759
  "⚙️",
3682
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
+ : "") +
3683
3773
  styled(
3684
3774
  "span",
3685
- "color:#555;font-size:10px;margin-left:8px",
3775
+ "color:#555;font-size:12px;margin-left:8px",
3686
3776
  c.hostname || "",
3687
3777
  ),
3688
3778
  ) +
3689
3779
  styled(
3690
3780
  "div",
3691
3781
  "display:flex;gap:10px;align-items:center",
3692
- styled("span", "color:#888;font-size:11px", c.ip) +
3782
+ styled("span", "color:#888;font-size:13px", c.ip) +
3693
3783
  onlineBadge +
3694
- styled("span", "color:#666;font-size:10px", lastSeen) +
3695
- (!c.online && userId
3784
+ styled("span", "color:#666;font-size:12px", lastSeen) +
3785
+ (!online && userId
3696
3786
  ? tag(
3697
3787
  "button",
3698
3788
  {
3699
3789
  onclick: "wdDeleteClient('" + esc(userId) + "')",
3700
3790
  style:
3701
- "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",
3702
3792
  title: "删除此客户端配置",
3703
3793
  },
3704
3794
  "删除",
@@ -3709,11 +3799,13 @@ function renderClientCard(c, clientCommands) {
3709
3799
 
3710
3800
  return styled(
3711
3801
  "div",
3712
- "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",
3713
3805
  header +
3714
- styled("div", "color:#888;font-size:10px", osStr) +
3715
- styled("div", "color:#888;font-size:10px;margin-bottom:6px", infoLine) +
3716
- 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),
3717
3809
  );
3718
3810
  }
3719
3811
 
@@ -3728,7 +3820,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3728
3820
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
3729
3821
  styled(
3730
3822
  "span",
3731
- "font-size:11px;color:#888",
3823
+ "font-size:13px;color:#888",
3732
3824
  "命令 (" + myCmds.length + "条)",
3733
3825
  ) +
3734
3826
  tag(
@@ -3736,7 +3828,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3736
3828
  {
3737
3829
  onclick: "wdShowAddModal('" + esc(userId) + "')",
3738
3830
  style:
3739
- "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",
3740
3832
  },
3741
3833
  "+ 添加",
3742
3834
  ),
@@ -3746,7 +3838,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3746
3838
  // 表头
3747
3839
  html += styled(
3748
3840
  "div",
3749
- "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",
3750
3842
  "<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
3751
3843
  );
3752
3844
 
@@ -3766,9 +3858,10 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3766
3858
  var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
3767
3859
  var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
3768
3860
 
3861
+ var prefix = "wd-" + esc(userId) + "-" + idx;
3769
3862
  html += styled(
3770
3863
  "div",
3771
- "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",
3772
3865
  styled(
3773
3866
  "span",
3774
3867
  "color:" +
@@ -3784,17 +3877,32 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3784
3877
  (c.name
3785
3878
  ? styled(
3786
3879
  "span",
3787
- "color:#555;font-size:8px;margin-left:4px;font-family:monospace",
3880
+ "color:#555;font-size:10px;margin-left:4px;font-family:monospace",
3788
3881
  c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
3789
3882
  )
3790
3883
  : ""),
3791
3884
  ) +
3792
- styled("span", "color:#888", pid) +
3793
- styled("span", "color:#aaa", mem) +
3794
- styled("span", "color:#888", uptime) +
3795
- styled(
3885
+ tag(
3886
+ "span",
3887
+ { style: "color:#888", "data-wd-key": prefix + "-pid" },
3888
+ pid,
3889
+ ) +
3890
+ tag(
3891
+ "span",
3892
+ { style: "color:#aaa", "data-wd-key": prefix + "-mem" },
3893
+ mem,
3894
+ ) +
3895
+ tag(
3796
3896
  "span",
3797
- "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
3897
+ { style: "color:#888", "data-wd-key": prefix + "-up" },
3898
+ uptime,
3899
+ ) +
3900
+ tag(
3901
+ "span",
3902
+ {
3903
+ style: "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
3904
+ "data-wd-key": prefix + "-rst",
3905
+ },
3798
3906
  restart,
3799
3907
  ) +
3800
3908
  styled(
@@ -3807,7 +3915,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3807
3915
  onclick:
3808
3916
  "wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
3809
3917
  style:
3810
- "padding:1px 5px;font-size:9px;background:" +
3918
+ "padding:1px 5px;font-size:11px;background:" +
3811
3919
  (c.enabled ? "#f59e0b" : "#4ade80") +
3812
3920
  ";color:#000;border:none;border-radius:2px;cursor:pointer",
3813
3921
  title: c.enabled ? "停止" : "启动",
@@ -3820,7 +3928,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3820
3928
  {
3821
3929
  onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
3822
3930
  style:
3823
- "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",
3824
3932
  title: "编辑",
3825
3933
  },
3826
3934
  "✎",
@@ -3830,7 +3938,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3830
3938
  {
3831
3939
  onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
3832
3940
  style:
3833
- "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",
3834
3942
  title: "删除",
3835
3943
  },
3836
3944
  "✕",
@@ -3853,7 +3961,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3853
3961
  : "";
3854
3962
  html += styled(
3855
3963
  "div",
3856
- "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",
3857
3965
  styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
3858
3966
  );
3859
3967
  }
@@ -3862,23 +3970,37 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3862
3970
  // 未匹配到的运行中进程(孤儿)
3863
3971
  match.unmatched.forEach(function (p) {
3864
3972
  var sc = p.status === "running" ? "#4ade80" : "#f87171";
3973
+ var oid = p.pid || p.cmdId || "?";
3974
+ var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
3865
3975
  html += styled(
3866
3976
  "div",
3867
- "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",
3868
3978
  styled(
3869
3979
  "span",
3870
3980
  "color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
3871
3981
  "⚡ " + (p.name || p.cmdId),
3872
3982
  ) +
3873
- styled("span", "color:#888", p.pid || "-") +
3874
- styled(
3983
+ tag(
3875
3984
  "span",
3876
- "color:#aaa",
3985
+ { style: "color:#888", "data-wd-key": opre + "-pid" },
3986
+ p.pid || "-",
3987
+ ) +
3988
+ tag(
3989
+ "span",
3990
+ { style: "color:#aaa", "data-wd-key": opre + "-mem" },
3877
3991
  p.memoryMB != null ? p.memoryMB + "MB" : "-",
3878
3992
  ) +
3879
- styled("span", "color:#888", p.uptimeDisplay || p.uptimeSeconds + "s") +
3880
- styled("span", "color:#555", "0") +
3881
- styled("span", "color:#555;font-size:9px", "孤立"),
3993
+ tag(
3994
+ "span",
3995
+ { style: "color:#888", "data-wd-key": opre + "-up" },
3996
+ p.uptimeDisplay || p.uptimeSeconds + "s",
3997
+ ) +
3998
+ tag(
3999
+ "span",
4000
+ { style: "color:#555", "data-wd-key": opre + "-rst" },
4001
+ "0",
4002
+ ) +
4003
+ styled("span", "color:#555;font-size:11px", "孤立"),
3882
4004
  );
3883
4005
  });
3884
4006
 
@@ -4072,29 +4194,92 @@ function wdToggleClientCmd(userId, idx) {
4072
4194
  wdSaveClientCmds(userId, myCmds);
4073
4195
  }
4074
4196
 
4197
+ var _wdSaving = false;
4198
+
4199
+ function wdShowSaving() {
4200
+ _wdSaving = true;
4201
+ var el = document.getElementById("wdSavingTip");
4202
+ if (!el) {
4203
+ el = document.createElement("div");
4204
+ el.id = "wdSavingTip";
4205
+ el.style.cssText =
4206
+ "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";
4207
+ document.body.appendChild(el);
4208
+ }
4209
+ el.textContent = "⏳ 处理中...";
4210
+ el.style.display = "block";
4211
+ }
4212
+ function wdHideSaving() {
4213
+ _wdSaving = false;
4214
+ var el = document.getElementById("wdSavingTip");
4215
+ if (el) el.style.display = "none";
4216
+ }
4217
+
4075
4218
  async function wdSaveClientCmds(userId, commands) {
4219
+ wdShowSaving();
4076
4220
  try {
4077
4221
  await fetch("/api/watchdog/client-commands", {
4078
4222
  method: "POST",
4079
4223
  headers: { "Content-Type": "application/json" },
4080
4224
  body: JSON.stringify({ userId: userId, commands: commands }),
4081
4225
  });
4226
+ wdHideSaving();
4082
4227
  fetchWatchdogData();
4083
4228
  } catch (e) {
4229
+ wdHideSaving();
4084
4230
  alert("保存失败: " + e.message);
4085
- // 失败时立即刷新恢复按钮状态
4086
4231
  fetchWatchdogData();
4087
4232
  }
4088
4233
  }
4089
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
+
4090
4261
  function wdDeleteClient(userId) {
4091
4262
  if (
4092
4263
  !confirm(
4093
4264
  "确定删除 " +
4094
4265
  userId +
4095
- " 的所有命令配置?\n\n删除后该客户端将不再出现在列表中。",
4266
+ " 及其所有命令配置?\n\n删除后该客户端将从列表中移除。",
4096
4267
  )
4097
4268
  )
4098
4269
  return;
4099
- wdSaveClientCmds(userId, []);
4270
+ wdShowSaving();
4271
+ fetch("/api/watchdog/delete-client", {
4272
+ method: "POST",
4273
+ headers: { "Content-Type": "application/json" },
4274
+ body: JSON.stringify({ userId: userId }),
4275
+ })
4276
+ .then(function () {
4277
+ wdHideSaving();
4278
+ fetchWatchdogData();
4279
+ })
4280
+ .catch(function (e) {
4281
+ wdHideSaving();
4282
+ alert("删除失败: " + e.message);
4283
+ fetchWatchdogData();
4284
+ });
4100
4285
  }
@@ -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>
@@ -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,
@@ -134,7 +154,17 @@ function setClientCommands(userId, commands) {
134
154
  });
135
155
  const cfg = loadConfig();
136
156
  if (!cfg.clientCommands) cfg.clientCommands = {};
137
- cfg.clientCommands[userId] = cleaned;
157
+ if (cleaned.length === 0) {
158
+ delete cfg.clientCommands[userId];
159
+ console.error(
160
+ `[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已清空,卡片已删除`,
161
+ );
162
+ } else {
163
+ cfg.clientCommands[userId] = cleaned;
164
+ console.error(
165
+ `[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已更新: ${cleaned.length} 条`,
166
+ );
167
+ }
138
168
  saveConfig(cfg);
139
169
  }
140
170
 
@@ -276,13 +306,72 @@ async function handleAnalyzeLogs(params, body, res) {
276
306
  console.error(
277
307
  `[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
278
308
  );
309
+ // 写入服务端缓存,前端直接读取,不再依赖 agent 转发
310
+ const now = Date.now();
311
+ for (const item of llmAnalysis) {
312
+ llmCache.set(item.cmdId, { ...item, cachedAt: now });
313
+ }
279
314
  sendJSON(res, 200, { llmAnalysis });
280
315
  } catch (e) {
281
316
  console.error(`[watchdog] LLM 分析失败: ${e.message}`);
282
- sendJSON(res, 500, { error: e.message });
317
+ sendJSON(res, 200, { llmAnalysis: [] });
283
318
  }
284
319
  }
285
320
 
321
+ async function handleDeleteClient(params, body, res) {
322
+ const userId = body.userId;
323
+ if (!userId) {
324
+ sendJSON(res, 400, { error: "缺少 userId" });
325
+ return;
326
+ }
327
+ // 1. 删除命令配置
328
+ const cfg = loadConfig();
329
+ if (cfg.clientCommands) delete cfg.clientCommands[userId];
330
+ if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
331
+ saveConfig(cfg);
332
+ // 2. 从内存客户端列表中移除所有匹配的客户端
333
+ let removed = 0;
334
+ for (const [clientId, info] of clients) {
335
+ if ((info.config && info.config.userId) === userId) {
336
+ clients.delete(clientId);
337
+ removed++;
338
+ }
339
+ }
340
+ console.error(
341
+ `[watchdog] ${userId} 已删除(${removed}个客户端,命令+配置已清除)`,
342
+ );
343
+ sendJSON(res, 200, { ok: true, removed });
344
+ }
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
+
286
375
  // ── 公开 ──
287
376
 
288
377
  export function registerWatchdogRoutes(httpServer) {
@@ -340,5 +429,13 @@ export async function tryHandleWatchdogRoute(
340
429
  await handleAnalyzeLogs(params, body, res);
341
430
  return true;
342
431
  }
432
+ if (method === "POST" && routePath === "/api/watchdog/delete-client") {
433
+ await handleDeleteClient(params, body, res);
434
+ return true;
435
+ }
436
+ if (method === "POST" && routePath === "/api/watchdog/clear-llm") {
437
+ await handleClearLlmAnalysis(params, body, res);
438
+ return true;
439
+ }
343
440
  return false;
344
441
  }
@@ -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) {
@@ -200,7 +212,7 @@ export class WatchdogAgent {
200
212
  // 同步命令
201
213
  if (Array.isArray(data.commands)) {
202
214
  const prev = this.processManager.runningCount;
203
- this.processManager.syncCommands(data.commands);
215
+ await this.processManager.syncCommands(data.commands);
204
216
  const now = this.processManager.runningCount;
205
217
  if (prev !== now) {
206
218
  console.error(
@@ -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
  }
@@ -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
  }