tt-help-cli-ycl 1.4.69 → 1.4.71

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.69",
3
+ "version": "1.4.71",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3445,28 +3445,17 @@ function styled(el, style, content) {
3445
3445
  return tag(el, { style: style }, content);
3446
3446
  }
3447
3447
 
3448
- function fmtThreshold(cmd) {
3449
- if (typeof cmd === "string") return "";
3450
- var maxUp = cmd.maxUptimeSeconds || 0,
3451
- maxMem = cmd.maxMemoryMB || 0;
3452
- if (!maxUp && !maxMem) return "";
3453
- var parts = [];
3454
- if (maxUp > 0)
3455
- parts.push(
3456
- "最长 " + (maxUp >= 3600 ? Math.round(maxUp / 3600) + "h" : maxUp + "s"),
3457
- );
3458
- if (maxMem > 0) parts.push("最大 " + maxMem + "MB");
3459
- return styled(
3460
- "span",
3461
- "color:#f59e0b;font-size:9px;margin-left:4px",
3462
- "[" + parts.join("/") + "]",
3463
- );
3464
- }
3465
-
3466
3448
  function cmdObj(cmd) {
3467
3449
  if (typeof cmd === "string")
3468
- return { cmd: cmd, enabled: true, maxUptimeSeconds: 0, maxMemoryMB: 0 };
3450
+ return {
3451
+ name: "",
3452
+ cmd: cmd,
3453
+ enabled: true,
3454
+ maxUptimeSeconds: 0,
3455
+ maxMemoryMB: 0,
3456
+ };
3469
3457
  return {
3458
+ name: cmd.name || "",
3470
3459
  cmd: cmd.cmd || "",
3471
3460
  enabled: cmd.enabled !== false,
3472
3461
  maxUptimeSeconds: cmd.maxUptimeSeconds || 0,
@@ -3537,10 +3526,7 @@ function renderWatchdogDashboard(clients, clientCommands) {
3537
3526
 
3538
3527
  el.innerHTML = styled(
3539
3528
  "div",
3540
- {
3541
- style:
3542
- "display:grid;grid-template-columns:repeat(auto-fill,minmax(440px,1fr));gap:16px",
3543
- },
3529
+ "display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
3544
3530
  allCards.join(""),
3545
3531
  );
3546
3532
  }
@@ -3549,14 +3535,33 @@ function renderOfflineCard(userId, cmds) {
3549
3535
  var cmdCount = cmds ? cmds.length : 0;
3550
3536
  return styled(
3551
3537
  "div",
3552
- "background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px;opacity:0.7",
3538
+ "background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px;opacity:0.9",
3553
3539
  styled(
3554
3540
  "div",
3555
3541
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
3556
3542
  styled(
3557
3543
  "div",
3558
3544
  "",
3559
- styled("strong", "color:#666;font-size:14px", userId) +
3545
+ tag(
3546
+ "span",
3547
+ {
3548
+ style:
3549
+ "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
3550
+ onclick: "wdShowConfigModal('" + esc(userId) + "')",
3551
+ title: "点击编辑客户端配置",
3552
+ },
3553
+ userId,
3554
+ ) +
3555
+ tag(
3556
+ "button",
3557
+ {
3558
+ onclick: "wdShowConfigModal('" + esc(userId) + "')",
3559
+ style:
3560
+ "padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3561
+ title: "编辑客户端配置",
3562
+ },
3563
+ "⚙️",
3564
+ ) +
3560
3565
  styled(
3561
3566
  "span",
3562
3567
  "color:#555;font-size:11px;margin-left:8px",
@@ -3585,10 +3590,51 @@ function renderOfflineCard(userId, cmds) {
3585
3590
  "color:#555;font-size:10px",
3586
3591
  "该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
3587
3592
  ) +
3588
- renderCmdList(cmds || [], userId),
3593
+ renderCmdRows(cmds || [], userId, [], false),
3589
3594
  );
3590
3595
  }
3591
3596
 
3597
+ // 匹配进程到命令(按命令行前缀匹配)
3598
+ function matchProcsToCmds(myCmds, procs) {
3599
+ var matched = {}; // cmdIdx -> proc
3600
+ var unmatchedProcs = [];
3601
+ procs.forEach(function (p) {
3602
+ var found = false;
3603
+ for (var i = 0; i < myCmds.length; i++) {
3604
+ var c = cmdObj(myCmds[i]);
3605
+ var pName = p.name || p.cmdId || "";
3606
+ if (
3607
+ c.cmd &&
3608
+ pName &&
3609
+ (c.cmd.indexOf(pName.substring(0, 30)) === 0 ||
3610
+ pName.indexOf(c.cmd.substring(0, 30)) === 0)
3611
+ ) {
3612
+ matched[i] = p;
3613
+ found = true;
3614
+ break;
3615
+ }
3616
+ }
3617
+ if (!found) unmatchedProcs.push(p);
3618
+ });
3619
+ return { matched: matched, unmatched: unmatchedProcs };
3620
+ }
3621
+
3622
+ // 根据命令行匹配 LLM 分析结果
3623
+ function findAnalysis(llmAnalysis, cmdLine) {
3624
+ if (!llmAnalysis || !cmdLine) return null;
3625
+ for (var i = 0; i < llmAnalysis.length; i++) {
3626
+ var a = llmAnalysis[i];
3627
+ if (
3628
+ a.cmdId &&
3629
+ (a.cmdId.indexOf(cmdLine.substring(0, 30)) === 0 ||
3630
+ cmdLine.indexOf(a.cmdId.substring(0, 30)) === 0)
3631
+ ) {
3632
+ return a;
3633
+ }
3634
+ }
3635
+ return null;
3636
+ }
3637
+
3592
3638
  function renderClientCard(c, clientCommands) {
3593
3639
  var os = c.osInfo || {},
3594
3640
  cfg = c.config || {},
@@ -3605,33 +3651,49 @@ function renderClientCard(c, clientCommands) {
3605
3651
  ]
3606
3652
  .filter(Boolean)
3607
3653
  .join(" | ");
3608
-
3609
- // 信息行
3610
3654
  var infoLine =
3611
- "用户编号: " +
3612
3655
  styled("span", "color:#f59e0b", userId || "未设置") +
3613
- " | tt-help v" +
3614
- (c.ttHelpVersion || "?") +
3656
+ " | " +
3657
+ styled("span", "color:#a78bfa", "tt-help v" + (c.ttHelpVersion || "?")) +
3615
3658
  " | Node " +
3616
3659
  (os.nodeVersion || "?") +
3617
3660
  " | 运行 " +
3618
3661
  (os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
3619
-
3620
- // 在线状态
3621
3662
  var onlineBadge = c.online
3622
3663
  ? styled("span", "color:#4ade80", "● 在线")
3623
3664
  : styled("span", "color:#f87171", "● 离线");
3624
3665
  var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
3625
3666
 
3626
- // 头部
3627
3667
  var header = styled(
3628
3668
  "div",
3629
3669
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
3630
3670
  styled(
3631
3671
  "div",
3632
3672
  "",
3633
- styled("strong", "color:#60a5fa;font-size:14px", c.hostname) +
3634
- styled("span", "color:#666;font-size:11px;margin-left:8px", c.clientId),
3673
+ tag(
3674
+ "span",
3675
+ {
3676
+ style: "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
3677
+ onclick: "wdShowConfigModal('" + esc(userId) + "')",
3678
+ title: "点击编辑客户端配置",
3679
+ },
3680
+ userId || c.hostname || "?",
3681
+ ) +
3682
+ tag(
3683
+ "button",
3684
+ {
3685
+ onclick: "wdShowConfigModal('" + esc(userId) + "')",
3686
+ style:
3687
+ "padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
3688
+ title: "编辑客户端配置",
3689
+ },
3690
+ "⚙️",
3691
+ ) +
3692
+ styled(
3693
+ "span",
3694
+ "color:#555;font-size:10px;margin-left:8px",
3695
+ c.hostname || "",
3696
+ ),
3635
3697
  ) +
3636
3698
  styled(
3637
3699
  "div",
@@ -3654,232 +3716,336 @@ function renderClientCard(c, clientCommands) {
3654
3716
  ),
3655
3717
  );
3656
3718
 
3657
- // 进程表格
3658
- var procTable = "";
3659
- if (procs.length > 0) {
3660
- var rows = procs
3661
- .map(function (p) {
3662
- var sc = p.status === "running" ? "#4ade80" : "#f87171";
3663
- return tag(
3664
- "tr",
3665
- {},
3666
- tag(
3667
- "td",
3668
- {
3669
- style:
3670
- "padding:3px;font-family:monospace;font-size:11px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
3671
- title: p.name || p.cmdId,
3672
- },
3673
- p.name || p.cmdId,
3674
- ) +
3675
- tag(
3676
- "td",
3677
- { style: "padding:3px;color:#aaa;font-size:10px" },
3678
- p.pid || "-",
3679
- ) +
3680
- tag(
3681
- "td",
3682
- { style: "padding:3px;font-size:11px" },
3683
- p.memoryMB != null ? p.memoryMB + "MB" : "-",
3684
- ) +
3685
- tag(
3686
- "td",
3687
- { style: "padding:3px;color:#aaa;font-size:10px" },
3688
- p.uptimeDisplay || p.uptimeSeconds + "s",
3689
- ) +
3690
- tag(
3691
- "td",
3692
- { style: "padding:3px;color:" + sc + ";font-size:10px" },
3693
- p.status,
3694
- ) +
3695
- tag(
3696
- "td",
3697
- { style: "padding:3px;color:#f59e0b;font-size:10px" },
3698
- p.restartCount > 0 ? String(p.restartCount) : "0",
3699
- ),
3700
- );
3701
- })
3702
- .join("");
3703
- procTable = styled(
3704
- "div",
3705
- "margin-top:10px",
3706
- tag(
3707
- "table",
3708
- { style: "width:100%;font-size:11px;border-collapse:collapse" },
3709
- tag(
3710
- "thead",
3711
- {},
3712
- tag(
3713
- "tr",
3714
- { style: "color:#888;font-size:10px" },
3715
- tag("th", { style: "text-align:left;padding:3px" }, "命令") +
3716
- tag("th", { style: "text-align:left;padding:3px" }, "PID") +
3717
- tag("th", { style: "text-align:left;padding:3px" }, "内存") +
3718
- tag("th", { style: "text-align:left;padding:3px" }, "运行") +
3719
- tag("th", { style: "text-align:left;padding:3px" }, "状态") +
3720
- tag("th", { style: "text-align:left;padding:3px" }, "重启"),
3721
- ),
3722
- ) + tag("tbody", {}, rows),
3723
- ),
3724
- );
3725
- }
3726
-
3727
3719
  return styled(
3728
3720
  "div",
3729
3721
  "background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px",
3730
3722
  header +
3731
3723
  styled("div", "color:#888;font-size:10px", osStr) +
3732
- styled("div", "color:#888;font-size:10px", infoLine) +
3733
- renderCmdList(myCmds, userId) +
3734
- (procs.length > 0
3735
- ? procTable
3736
- : styled(
3737
- "div",
3738
- "color:#555;font-size:11px;margin-top:6px",
3739
- "无运行进程",
3740
- )),
3724
+ styled("div", "color:#888;font-size:10px;margin-bottom:6px", infoLine) +
3725
+ renderCmdRows(myCmds, userId, procs, c.online, c.llmAnalysis),
3741
3726
  );
3742
3727
  }
3743
3728
 
3744
- function renderCmdList(myCmds, userId) {
3729
+ // 统一的命令行视图:每条命令一行,含运行状态
3730
+ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
3731
+ var match = matchProcsToCmds(myCmds, procs);
3745
3732
  var html = styled(
3746
3733
  "div",
3747
- "margin-top:10px;border-top:1px solid #2a2a3a;padding-top:10px",
3734
+ "margin-top:6px;border-top:1px solid #2a2a3a;padding-top:8px",
3748
3735
  styled(
3749
3736
  "div",
3750
3737
  "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
3751
3738
  styled(
3752
3739
  "span",
3753
3740
  "font-size:11px;color:#888",
3754
- "📋 命令列表 (" + myCmds.length + "条)",
3741
+ "命令 (" + myCmds.length + "条)",
3755
3742
  ) +
3756
3743
  tag(
3757
3744
  "button",
3758
3745
  {
3759
- onclick: "wdPromptAddCmd('" + esc(userId) + "')",
3746
+ onclick: "wdShowAddModal('" + esc(userId) + "')",
3760
3747
  style:
3761
3748
  "padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600",
3762
3749
  },
3763
- "+ 添加命令",
3750
+ "+ 添加",
3764
3751
  ),
3765
3752
  ),
3766
3753
  );
3767
3754
 
3755
+ // 表头
3756
+ html += styled(
3757
+ "div",
3758
+ "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",
3759
+ "<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
3760
+ );
3761
+
3762
+ // 命令行
3768
3763
  myCmds.forEach(function (cmd, idx) {
3769
3764
  var c = cmdObj(cmd);
3770
- var color = c.enabled ? "#a78bfa" : "#555";
3771
- var badge = c.enabled
3772
- ? styled("span", "color:#4ade80;font-size:9px", "▶")
3773
- : styled("span", "color:#f87171;font-size:9px", "⏸");
3774
- var codeStyle =
3775
- "color:" +
3776
- color +
3777
- ";font-size:10px;word-break:break-all;flex:1;margin:0 6px;" +
3778
- (c.enabled ? "" : "text-decoration:line-through");
3765
+ var p = match.matched[idx] || null;
3766
+ var isRunning = p && p.status === "running";
3767
+ var rowColor = c.enabled ? (isRunning ? "#4ade80" : "#f59e0b") : "#555";
3768
+ var statusIcon = c.enabled ? (isRunning ? "●" : "○") : "⏸";
3769
+
3770
+ var label =
3771
+ c.name || (c.cmd.length > 40 ? c.cmd.substring(0, 37) + "..." : c.cmd);
3772
+ var cmdTitle = c.name ? c.name + "\n" + c.cmd : c.cmd;
3773
+ var pid = p ? p.pid : "-";
3774
+ var mem = p && p.memoryMB != null ? p.memoryMB + "MB" : "-";
3775
+ var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
3776
+ var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
3779
3777
 
3780
3778
  html += styled(
3781
3779
  "div",
3782
- "display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-size:11px;border-top:1px solid #222",
3783
- badge +
3784
- tag(
3785
- "code",
3786
- { style: codeStyle, title: c.cmd },
3787
- c.cmd + fmtThreshold(cmd),
3788
- ) +
3789
- tag(
3790
- "button",
3791
- {
3792
- onclick: "wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
3793
- style:
3794
- "padding:2px 6px;font-size:9px;background:" +
3795
- (c.enabled ? "#f59e0b" : "#4ade80") +
3796
- ";color:#000;border:none;border-radius:3px;cursor:pointer;flex-shrink:0;margin-right:3px",
3797
- title: c.enabled ? "停止" : "启动",
3798
- },
3799
- c.enabled ? "⏹" : "▶",
3800
- ) +
3801
- tag(
3802
- "button",
3803
- {
3804
- onclick: "wdPromptEditCmd('" + esc(userId) + "'," + idx + ")",
3805
- style:
3806
- "padding:2px 6px;font-size:9px;background:#60a5fa;color:#fff;border:none;border-radius:3px;cursor:pointer;flex-shrink:0;margin-right:3px",
3807
- title: "编辑",
3808
- },
3809
- "✎",
3780
+ "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",
3781
+ styled(
3782
+ "span",
3783
+ "color:" +
3784
+ rowColor +
3785
+ ";overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
3786
+ statusIcon +
3787
+ " " +
3788
+ styled(
3789
+ "span",
3790
+ "color:" + rowColor + ";font-weight:" + (c.name ? "500" : "normal"),
3791
+ label,
3792
+ ) +
3793
+ (c.name
3794
+ ? styled(
3795
+ "span",
3796
+ "color:#555;font-size:8px;margin-left:4px;font-family:monospace",
3797
+ c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
3798
+ )
3799
+ : ""),
3800
+ ) +
3801
+ styled("span", "color:#888", pid) +
3802
+ styled("span", "color:#aaa", mem) +
3803
+ styled("span", "color:#888", uptime) +
3804
+ styled(
3805
+ "span",
3806
+ "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
3807
+ restart,
3810
3808
  ) +
3811
- tag(
3812
- "button",
3813
- {
3814
- onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
3815
- style:
3816
- "padding:2px 6px;font-size:9px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer;flex-shrink:0",
3817
- title: "删除",
3818
- },
3819
- "✕",
3809
+ styled(
3810
+ "span",
3811
+ "display:flex;gap:2px",
3812
+ (isOnline
3813
+ ? tag(
3814
+ "button",
3815
+ {
3816
+ onclick:
3817
+ "wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
3818
+ style:
3819
+ "padding:1px 5px;font-size:9px;background:" +
3820
+ (c.enabled ? "#f59e0b" : "#4ade80") +
3821
+ ";color:#000;border:none;border-radius:2px;cursor:pointer",
3822
+ title: c.enabled ? "停止" : "启动",
3823
+ },
3824
+ c.enabled ? "⏹" : "▶",
3825
+ )
3826
+ : "") +
3827
+ tag(
3828
+ "button",
3829
+ {
3830
+ onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
3831
+ style:
3832
+ "padding:1px 5px;font-size:9px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
3833
+ title: "编辑",
3834
+ },
3835
+ "✎",
3836
+ ) +
3837
+ tag(
3838
+ "button",
3839
+ {
3840
+ onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
3841
+ style:
3842
+ "padding:1px 5px;font-size:9px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
3843
+ title: "删除",
3844
+ },
3845
+ "✕",
3846
+ ),
3820
3847
  ),
3821
3848
  );
3849
+
3850
+ // AI 分析结果
3851
+ var ai = findAnalysis(llmAnalysis, c.cmd);
3852
+ if (ai) {
3853
+ var sevColor =
3854
+ ai.severity === "error"
3855
+ ? "#f87171"
3856
+ : ai.severity === "warn"
3857
+ ? "#fbbf24"
3858
+ : "#4ade80";
3859
+ var tip =
3860
+ ai.suggestions && ai.suggestions.length > 0
3861
+ ? " 💡" + ai.suggestions.join(" | ")
3862
+ : "";
3863
+ html += styled(
3864
+ "div",
3865
+ "font-size:9px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
3866
+ styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
3867
+ );
3868
+ }
3822
3869
  });
3823
3870
 
3871
+ // 未匹配到的运行中进程(孤儿)
3872
+ match.unmatched.forEach(function (p) {
3873
+ var sc = p.status === "running" ? "#4ade80" : "#f87171";
3874
+ html += styled(
3875
+ "div",
3876
+ "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",
3877
+ styled(
3878
+ "span",
3879
+ "color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
3880
+ "⚡ " + (p.name || p.cmdId),
3881
+ ) +
3882
+ styled("span", "color:#888", p.pid || "-") +
3883
+ styled(
3884
+ "span",
3885
+ "color:#aaa",
3886
+ p.memoryMB != null ? p.memoryMB + "MB" : "-",
3887
+ ) +
3888
+ styled("span", "color:#888", p.uptimeDisplay || p.uptimeSeconds + "s") +
3889
+ styled("span", "color:#555", "0") +
3890
+ styled("span", "color:#555;font-size:9px", "孤立"),
3891
+ );
3892
+ });
3893
+
3894
+ if (myCmds.length === 0 && match.unmatched.length === 0) {
3895
+ html += styled(
3896
+ "div",
3897
+ "color:#555;font-size:10px;padding:8px 0",
3898
+ "暂无命令,点击「+ 添加」配置",
3899
+ );
3900
+ }
3901
+
3824
3902
  return html;
3825
3903
  }
3826
3904
 
3827
- // ── 命令管理操作 ──
3905
+ // ── 弹窗:添加/编辑命令 ──
3828
3906
 
3829
- function wdPromptAddCmd(userId) {
3830
- var cmd = prompt(
3831
- "输入完整命令行:\n\n例如: tt-help explore --location DE --server http://...",
3832
- );
3833
- if (!cmd || !cmd.trim()) return;
3834
- if (!_wdData) return;
3835
- var allCmds = _wdData.clientCommands || {};
3836
- var myCmds = (allCmds[userId] || []).slice();
3837
- myCmds.push({
3838
- cmd: cmd.trim(),
3839
- enabled: true,
3840
- maxUptimeSeconds: 0,
3841
- maxMemoryMB: 0,
3842
- });
3843
- wdSaveClientCmds(userId, myCmds);
3907
+ var _wdModalUserId = "";
3908
+ var _wdModalEditIdx = -1;
3909
+
3910
+ function wdShowAddModal(userId) {
3911
+ _wdModalUserId = userId;
3912
+ _wdModalEditIdx = -1;
3913
+ document.getElementById("wdModalTitle").textContent = "添加命令";
3914
+ document.getElementById("wdModalName").value = "";
3915
+ document.getElementById("wdModalCmd").value = "";
3916
+ document.getElementById("wdModalMaxUp").value = "0";
3917
+ document.getElementById("wdModalMaxMem").value = "0";
3918
+ document.getElementById("wdCmdModal").style.display = "flex";
3919
+ document.getElementById("wdModalName").focus();
3844
3920
  }
3845
3921
 
3846
- function wdPromptEditCmd(userId, idx) {
3922
+ function wdShowEditModal(userId, idx) {
3847
3923
  if (!_wdData) return;
3848
3924
  var allCmds = _wdData.clientCommands || {};
3849
- var myCmds = (allCmds[userId] || []).slice();
3925
+ var myCmds = allCmds[userId] || [];
3850
3926
  if (idx >= myCmds.length) return;
3851
3927
  var c = cmdObj(myCmds[idx]);
3852
3928
 
3853
- var newText = prompt("编辑命令行(留空不变,0=不限制):", c.cmd);
3854
- if (newText === null) return;
3855
- var newMaxUp =
3856
- parseInt(
3857
- prompt("最大运行时长(秒,0=不限制):", c.maxUptimeSeconds || "0"),
3858
- ) || 0;
3859
- var newMaxMem =
3860
- parseInt(prompt("最大内存(MB,0=不限制):", c.maxMemoryMB || "0")) || 0;
3929
+ _wdModalUserId = userId;
3930
+ _wdModalEditIdx = idx;
3931
+ document.getElementById("wdModalTitle").textContent = "编辑命令";
3932
+ document.getElementById("wdModalName").value = c.name || "";
3933
+ document.getElementById("wdModalCmd").value = c.cmd;
3934
+ document.getElementById("wdModalMaxUp").value = c.maxUptimeSeconds || 0;
3935
+ document.getElementById("wdModalMaxMem").value = c.maxMemoryMB || 0;
3936
+ document.getElementById("wdCmdModal").style.display = "flex";
3937
+ document.getElementById("wdModalName").focus();
3938
+ }
3861
3939
 
3862
- if (typeof myCmds[idx] === "string") {
3863
- myCmds[idx] = {
3864
- cmd: (newText || c.cmd).trim(),
3865
- enabled: true,
3866
- maxUptimeSeconds: newMaxUp,
3867
- maxMemoryMB: newMaxMem,
3868
- };
3940
+ function wdModalCancel() {
3941
+ document.getElementById("wdCmdModal").style.display = "none";
3942
+ }
3943
+
3944
+ function wdModalSave() {
3945
+ var name = document.getElementById("wdModalName").value.trim();
3946
+ var cmd = document.getElementById("wdModalCmd").value.trim();
3947
+ if (!cmd) {
3948
+ alert("请输入命令行");
3949
+ return;
3950
+ }
3951
+ var maxUp = parseInt(document.getElementById("wdModalMaxUp").value) || 0;
3952
+ var maxMem = parseInt(document.getElementById("wdModalMaxMem").value) || 0;
3953
+
3954
+ if (!_wdData) return;
3955
+ var allCmds = _wdData.clientCommands || {};
3956
+ var myCmds = (allCmds[_wdModalUserId] || []).slice();
3957
+
3958
+ if (_wdModalEditIdx >= 0) {
3959
+ // 编辑模式
3960
+ if (_wdModalEditIdx < myCmds.length) {
3961
+ myCmds[_wdModalEditIdx] = {
3962
+ name: name,
3963
+ cmd: cmd,
3964
+ enabled: cmdObj(myCmds[_wdModalEditIdx]).enabled,
3965
+ maxUptimeSeconds: maxUp,
3966
+ maxMemoryMB: maxMem,
3967
+ };
3968
+ }
3869
3969
  } else {
3870
- if (newText && newText.trim()) myCmds[idx].cmd = newText.trim();
3871
- myCmds[idx].maxUptimeSeconds = newMaxUp;
3872
- myCmds[idx].maxMemoryMB = newMaxMem;
3970
+ // 添加模式
3971
+ myCmds.push({
3972
+ name: name,
3973
+ cmd: cmd,
3974
+ enabled: true,
3975
+ maxUptimeSeconds: maxUp,
3976
+ maxMemoryMB: maxMem,
3977
+ });
3978
+ }
3979
+
3980
+ wdSaveClientCmds(_wdModalUserId, myCmds);
3981
+ document.getElementById("wdCmdModal").style.display = "none";
3982
+ }
3983
+
3984
+ // ── 配置弹窗 ──
3985
+
3986
+ var _wdCfgUserId = "";
3987
+
3988
+ function wdShowConfigModal(userId) {
3989
+ _wdCfgUserId = userId;
3990
+ document.getElementById("wdConfigUserId").textContent = userId;
3991
+ // 从客户端数据中预填已有配置
3992
+ var cfg = {};
3993
+ if (_wdData && _wdData.clients) {
3994
+ for (var i = 0; i < _wdData.clients.length; i++) {
3995
+ var c = _wdData.clients[i];
3996
+ if ((c.config && c.config.userId) === userId) {
3997
+ cfg = c.config || {};
3998
+ break;
3999
+ }
4000
+ }
4001
+ }
4002
+ document.getElementById("wdCfgServer").value = cfg.server || "";
4003
+ document.getElementById("wdCfgProxy").value = cfg.proxy || "";
4004
+ document.getElementById("wdCfgBrowser").value = cfg.browser || "";
4005
+ document.getElementById("wdCfgUserId").value = cfg.userId || userId;
4006
+ document.getElementById("wdCfgMaxVideos").value = cfg.maxVideos || "";
4007
+ document.getElementById("wdCfgMaxComments").value = cfg.maxComments || "";
4008
+ document.getElementById("wdCfgMaxFollowing").value = cfg.maxFollowing || "";
4009
+ document.getElementById("wdCfgMaxFollowers").value = cfg.maxFollowers || "";
4010
+ document.getElementById("wdConfigModal").style.display = "flex";
4011
+ }
4012
+
4013
+ async function wdSaveConfig() {
4014
+ var cfg = {
4015
+ server: document.getElementById("wdCfgServer").value.trim(),
4016
+ proxy: document.getElementById("wdCfgProxy").value.trim(),
4017
+ browser: document.getElementById("wdCfgBrowser").value.trim(),
4018
+ userId: document.getElementById("wdCfgUserId").value.trim(),
4019
+ maxVideos: parseInt(document.getElementById("wdCfgMaxVideos").value) || 0,
4020
+ maxComments:
4021
+ parseInt(document.getElementById("wdCfgMaxComments").value) || 0,
4022
+ maxFollowing:
4023
+ parseInt(document.getElementById("wdCfgMaxFollowing").value) || 0,
4024
+ maxFollowers:
4025
+ parseInt(document.getElementById("wdCfgMaxFollowers").value) || 0,
4026
+ };
4027
+ try {
4028
+ await fetch("/api/watchdog/client-config", {
4029
+ method: "POST",
4030
+ headers: { "Content-Type": "application/json" },
4031
+ body: JSON.stringify({ userId: _wdCfgUserId, config: cfg }),
4032
+ });
4033
+ document.getElementById("wdConfigModal").style.display = "none";
4034
+ // 配置保存后无需立即刷新(下次心跳回来后看门狗自动应用)
4035
+ } catch (e) {
4036
+ alert("保存失败: " + e.message);
3873
4037
  }
3874
- wdSaveClientCmds(userId, myCmds);
3875
4038
  }
3876
4039
 
4040
+ // ── 操作 ──
4041
+
3877
4042
  function wdRemoveClientCmd(userId, idx) {
3878
4043
  if (!_wdData) return;
3879
4044
  var allCmds = _wdData.clientCommands || {};
3880
4045
  var myCmds = (allCmds[userId] || []).slice();
3881
4046
  if (idx >= myCmds.length) return;
3882
- var txt = cmdObj(myCmds[idx]).cmd;
4047
+ var obj = cmdObj(myCmds[idx]);
4048
+ var txt = obj.name ? obj.name + "\n" + obj.cmd : obj.cmd;
3883
4049
  if (!confirm("确定删除命令?\n\n" + txt)) return;
3884
4050
  myCmds.splice(idx, 1);
3885
4051
  wdSaveClientCmds(userId, myCmds);
@@ -513,6 +513,105 @@
513
513
  </div>
514
514
  <div id="wdClientsList" style="color:#666;font-size:12px">加载中...</div>
515
515
  </div>
516
+
517
+ <!-- 命令编辑弹窗 -->
518
+ <div id="wdCmdModal"
519
+ style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
520
+ <div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:560px">
521
+ <div style="font-size:16px;color:#60a5fa;margin-bottom:16px" id="wdModalTitle">添加命令</div>
522
+ <div style="margin-bottom:12px">
523
+ <label style="display:block;color:#888;font-size:11px;margin-bottom:4px">名称</label>
524
+ <input id="wdModalName"
525
+ style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box"
526
+ placeholder="例如:explore-DE、refresh-ES、comments">
527
+ </div>
528
+ <div style="margin-bottom:12px">
529
+ <label style="display:block;color:#888;font-size:11px;margin-bottom:4px">命令行</label>
530
+ <textarea id="wdModalCmd" rows="3"
531
+ style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical"
532
+ placeholder="tt-help explore --location DE --server http://..."></textarea>
533
+ </div>
534
+ <div style="display:flex;gap:12px;margin-bottom:12px">
535
+ <div style="flex:1">
536
+ <label style="display:block;color:#888;font-size:11px;margin-bottom:4px">最长运行(秒,0=不限)</label>
537
+ <input id="wdModalMaxUp" type="number" min="0" value="0"
538
+ style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box">
539
+ </div>
540
+ <div style="flex:1">
541
+ <label style="display:block;color:#888;font-size:11px;margin-bottom:4px">最大内存(MB,0=不限)</label>
542
+ <input id="wdModalMaxMem" type="number" min="0" value="0"
543
+ style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box">
544
+ </div>
545
+ </div>
546
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
547
+ <button onclick="wdModalCancel()"
548
+ style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
549
+ <button onclick="wdModalSave()"
550
+ style="padding:8px 16px;background:#4ade80;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存</button>
551
+ </div>
552
+ </div>
553
+ </div>
554
+
555
+ <!-- 客户端配置编辑弹窗 -->
556
+ <div id="wdConfigModal"
557
+ style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
558
+ <div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:480px">
559
+ <div style="font-size:16px;color:#60a5fa;margin-bottom:16px">⚙️ 客户端配置 — <span id="wdConfigUserId"></span></div>
560
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
561
+ <div>
562
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">服务端地址</label>
563
+ <input id="wdCfgServer"
564
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
565
+ placeholder="http://...">
566
+ </div>
567
+ <div>
568
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">代理</label>
569
+ <input id="wdCfgProxy"
570
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
571
+ placeholder="http://127.0.0.1:7890">
572
+ </div>
573
+ <div>
574
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">浏览器路径</label>
575
+ <input id="wdCfgBrowser"
576
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
577
+ placeholder="默认">
578
+ </div>
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"
583
+ placeholder="自动生成">
584
+ </div>
585
+ <div>
586
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大视频数</label>
587
+ <input id="wdCfgMaxVideos" type="number" min="0"
588
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
589
+ </div>
590
+ <div>
591
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大评论数</label>
592
+ <input id="wdCfgMaxComments" type="number" min="0"
593
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
594
+ </div>
595
+ <div>
596
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大关注数</label>
597
+ <input id="wdCfgMaxFollowing" type="number" min="0"
598
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
599
+ </div>
600
+ <div>
601
+ <label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大粉丝数</label>
602
+ <input id="wdCfgMaxFollowers" type="number" min="0"
603
+ style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
604
+ </div>
605
+ </div>
606
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
607
+ <button onclick="document.getElementById('wdConfigModal').style.display='none'"
608
+ style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
609
+ <button onclick="wdSaveConfig()"
610
+ style="padding:8px 16px;background:#4ade80;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并下发</button>
611
+ </div>
612
+ </div>
613
+ </div>
614
+
516
615
  <script src="app.js"></script>
517
616
  </body>
518
617
 
@@ -130,6 +130,21 @@ function getAllClientCommandsRaw() {
130
130
  return cfg.clientCommands || {};
131
131
  }
132
132
 
133
+ // ── 客户端配置管理(用于远程下发 ~/.tt-help.json 配置) ──
134
+
135
+ function getClientConfigStored(userId) {
136
+ if (!userId) return null;
137
+ const cfg = loadConfig();
138
+ return (cfg.clientConfigs && cfg.clientConfigs[userId]) || null;
139
+ }
140
+
141
+ function setClientConfigStored(userId, config) {
142
+ const cfg = loadConfig();
143
+ if (!cfg.clientConfigs) cfg.clientConfigs = {};
144
+ cfg.clientConfigs[userId] = config;
145
+ saveConfig(cfg);
146
+ }
147
+
133
148
  // ── 工具函数 ──
134
149
 
135
150
  function readBody(req) {
@@ -189,6 +204,7 @@ async function handleHeartbeat(params, body, res) {
189
204
  sendJSON(res, 200, {
190
205
  ok: true,
191
206
  commands,
207
+ clientConfig: getClientConfigStored(userId),
192
208
  serverTime: Date.now(),
193
209
  });
194
210
  }
@@ -219,6 +235,18 @@ async function handleSetClientCommands(params, body, res) {
219
235
  sendJSON(res, 200, { ok: true, count: commands.length });
220
236
  }
221
237
 
238
+ async function handleClientConfig(params, body, res) {
239
+ const userId = body.userId;
240
+ const config = body.config;
241
+ if (!userId || !config) {
242
+ sendJSON(res, 400, { error: "缺少 userId 或 config" });
243
+ return;
244
+ }
245
+ setClientConfigStored(userId, config);
246
+ console.error(`[watchdog] ${userId} 配置已更新`);
247
+ sendJSON(res, 200, { ok: true });
248
+ }
249
+
222
250
  // ── 公开 ──
223
251
 
224
252
  export function registerWatchdogRoutes(httpServer) {
@@ -245,5 +273,9 @@ export async function tryHandleWatchdogRoute(
245
273
  await handleSetClientCommands(params, body, res);
246
274
  return true;
247
275
  }
276
+ if (method === "POST" && routePath === "/api/watchdog/client-config") {
277
+ await handleClientConfig(params, body, res);
278
+ return true;
279
+ }
248
280
  return false;
249
281
  }
@@ -15,6 +15,47 @@ import {
15
15
  isGlobalInstall,
16
16
  startUpgradeChecker,
17
17
  } from "./upgrader.js";
18
+ import { analyzeProcessLogs } from "./llm-analyzer.js";
19
+ import { writeFileSync, existsSync, readFileSync } from "fs";
20
+ import { join } from "path";
21
+ import { homedir } from "os";
22
+
23
+ const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
24
+ const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
25
+
26
+ /** 将服务端下发的配置合并写回 ~/.tt-help.json */
27
+ function applyClientConfig(newCfg) {
28
+ try {
29
+ let cfg = {};
30
+ if (existsSync(TT_HELP_CONFIG)) {
31
+ cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
32
+ }
33
+ // 合并下发字段(只更新存在的 key)
34
+ const fields = [
35
+ "server",
36
+ "proxy",
37
+ "browser",
38
+ "userId",
39
+ "maxVideos",
40
+ "maxComments",
41
+ "maxFollowing",
42
+ "maxFollowers",
43
+ ];
44
+ let changed = false;
45
+ for (const f of fields) {
46
+ if (newCfg[f] !== undefined && newCfg[f] !== cfg[f]) {
47
+ cfg[f] = newCfg[f];
48
+ changed = true;
49
+ }
50
+ }
51
+ if (changed) {
52
+ writeFileSync(TT_HELP_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
53
+ console.error("[watchdog] ~/.tt-help.json 已更新(来自服务端下发)");
54
+ }
55
+ } catch (e) {
56
+ console.error(`[watchdog] 配置更新失败: ${e.message}`);
57
+ }
58
+ }
18
59
 
19
60
  export class WatchdogAgent {
20
61
  /**
@@ -30,6 +71,8 @@ export class WatchdogAgent {
30
71
  this.running = false;
31
72
  this.heartbeatTimer = null;
32
73
  this.thresholdTimer = null;
74
+ this.llmAnalyzerTimer = null;
75
+ this._llmResults = []; // 最近一次 LLM 分析结果
33
76
  this.processManager = new ProcessManager();
34
77
  this.consecutiveFailures = 0;
35
78
  this.maxFailures = 10;
@@ -70,6 +113,9 @@ export class WatchdogAgent {
70
113
  this.processManager.checkThresholds();
71
114
  }, 5000);
72
115
 
116
+ // LLM 日志分析(每 10 分钟)
117
+ this._startLlmAnalyzer();
118
+
73
119
  // 自动升级检测(仅全局安装模式)
74
120
  this._startUpgradeChecker();
75
121
  }
@@ -87,6 +133,10 @@ export class WatchdogAgent {
87
133
  clearInterval(this.thresholdTimer);
88
134
  this.thresholdTimer = null;
89
135
  }
136
+ if (this.llmAnalyzerTimer) {
137
+ clearInterval(this.llmAnalyzerTimer);
138
+ this.llmAnalyzerTimer = null;
139
+ }
90
140
  if (this.upgradeChecker) {
91
141
  this.upgradeChecker.stop();
92
142
  this.upgradeChecker = null;
@@ -116,6 +166,7 @@ export class WatchdogAgent {
116
166
  },
117
167
  config: getClientConfig(),
118
168
  processes: this.processManager.getProcessesStatus(),
169
+ llmAnalysis: this._llmResults,
119
170
  };
120
171
  }
121
172
 
@@ -159,6 +210,11 @@ export class WatchdogAgent {
159
210
  }
160
211
  }
161
212
 
213
+ // 服务端下发的客户端配置(远程修改 ~/.tt-help.json)
214
+ if (data.clientConfig) {
215
+ applyClientConfig(data.clientConfig);
216
+ }
217
+
162
218
  // 心跳日志:仅状态变化或每 10 次打印一次
163
219
  this._heartbeatSeq++;
164
220
  const cmdCount = data.commands?.length || 0;
@@ -185,6 +241,54 @@ export class WatchdogAgent {
185
241
  }
186
242
  }
187
243
 
244
+ /**
245
+ * 启动 LLM 日志分析定时器(每 10 分钟)
246
+ */
247
+ _startLlmAnalyzer() {
248
+ // 首次 60s 后执行(等进程启动稳定),之后每 10 分钟
249
+ const run = async () => {
250
+ try {
251
+ console.error("[llm-analyzer] 定时触发...");
252
+ const logs = this.processManager.getProcessesLogs();
253
+ if (logs.length === 0) {
254
+ console.error(
255
+ "[llm-analyzer] 无日志,跳过(进程可能无 stderr 输出)",
256
+ );
257
+ return;
258
+ }
259
+ console.error(`[llm-analyzer] 开始分析 ${logs.length} 个进程的日志...`);
260
+ this._llmResults = await analyzeProcessLogs(logs);
261
+ if (this._llmResults.length === 0) {
262
+ this._llmResults = [
263
+ {
264
+ cmdId: "_status",
265
+ severity: "warn",
266
+ summary: "LLM 分析未返回结果,请检查 LLM 配置",
267
+ suggestions: [],
268
+ },
269
+ ];
270
+ }
271
+ const summary = this._llmResults
272
+ .map((r) => `${r.cmdId.substring(0, 30)}: ${r.severity}`)
273
+ .join(", ");
274
+ console.error(`[llm-analyzer] 分析完成: ${summary || "无异常"}`);
275
+ } catch (e) {
276
+ console.error(`[llm-analyzer] 异常: ${e.message}`);
277
+ this._llmResults = [
278
+ {
279
+ cmdId: "_status",
280
+ severity: "error",
281
+ summary: "LLM 分析异常: " + e.message.substring(0, 80),
282
+ suggestions: [],
283
+ },
284
+ ];
285
+ }
286
+ };
287
+
288
+ setTimeout(run, 60_000);
289
+ this.llmAnalyzerTimer = setInterval(run, 10 * 60_000);
290
+ }
291
+
188
292
  /**
189
293
  * 启动自动升级检测(每 10 分钟)
190
294
  * 仅全局安装模式下启用
@@ -0,0 +1,137 @@
1
+ /**
2
+ * LLM 日志分析模块 — 定期收集各进程日志,调用 LLM 分析错误并给出建议
3
+ *
4
+ * 每 10 分钟执行一次,分析结果通过心跳上报到 watch 服务端。
5
+ */
6
+
7
+ // ── 分析入口 ──
8
+
9
+ /**
10
+ * 对所有进程的日志调用 LLM 分析
11
+ * @param {Array<{cmdId:string, logs:string[], uptimeSeconds:number, restartCount:number}>} processLogs
12
+ * @returns {Promise<Array<{cmdId:string, severity:string, summary:string, suggestions:string[]}>>}
13
+ */
14
+ export async function analyzeProcessLogs(processLogs) {
15
+ if (!processLogs || processLogs.length === 0) return [];
16
+
17
+ try {
18
+ const { llmFetch } = await import("../watch/llm-config.js");
19
+ console.error("[llm-analyzer] llmFetch 模块加载成功");
20
+ const results = [];
21
+
22
+ for (const proc of processLogs) {
23
+ try {
24
+ console.error(
25
+ `[llm-analyzer] 分析 ${proc.cmdId.substring(0, 40)}... (${proc.logs.length} 行日志)`,
26
+ );
27
+ const analysis = await analyzeOneProcess(llmFetch, proc);
28
+ if (analysis) {
29
+ console.error(
30
+ `[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${analysis.severity}: ${analysis.summary}`,
31
+ );
32
+ results.push(analysis);
33
+ }
34
+ } catch (e) {
35
+ console.error(
36
+ `[llm-analyzer] ${proc.cmdId.substring(0, 30)} 分析失败: ${e.message}`,
37
+ );
38
+ }
39
+ }
40
+
41
+ return results;
42
+ } catch (e) {
43
+ console.error(`[llm-analyzer] llmFetch 加载失败: ${e.message}`);
44
+ return [];
45
+ }
46
+ }
47
+
48
+ // ── 单进程分析 ──
49
+
50
+ async function analyzeOneProcess(llmFetch, proc) {
51
+ const recentLogs = proc.logs.slice(-200).join("\n"); // 最多取最近 200 行
52
+ const prompt = buildPrompt(
53
+ proc.cmdId,
54
+ recentLogs,
55
+ proc.uptimeSeconds,
56
+ proc.restartCount,
57
+ );
58
+
59
+ const content = await llmFetch({
60
+ messages: [{ role: "user", content: prompt }],
61
+ max_tokens: 256,
62
+ temperature: 0.1,
63
+ enable_thinking: false,
64
+ });
65
+
66
+ const parsed = parseResponse(content);
67
+ return {
68
+ cmdId: proc.cmdId,
69
+ severity: parsed.severity || "ok",
70
+ summary: parsed.summary || "无异常",
71
+ suggestions: parsed.suggestions || [],
72
+ };
73
+ }
74
+
75
+ // ── Prompt 构建 ──
76
+
77
+ function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
78
+ const uptime =
79
+ uptimeSeconds >= 3600
80
+ ? Math.round(uptimeSeconds / 3600) + "h"
81
+ : uptimeSeconds + "s";
82
+
83
+ return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
84
+
85
+ 进程信息:
86
+ - 命令行: ${cmdId}
87
+ - 已运行: ${uptime}
88
+ - 历史重启: ${restartCount} 次
89
+
90
+ 最近日志:
91
+ ${logs || "(无日志)"}
92
+
93
+ 请返回 JSON(仅 JSON,无其他内容):
94
+ {"severity":"ok"|"warn"|"error","summary":"一句话中文总结,不超过 50 字","suggestions":["建议1","建议2"]}
95
+
96
+ 判断标准:
97
+ - ok: 无异常,正常运行
98
+ - warn: 有偶发错误(验证码、403、超时 < 3次)或内存偏高
99
+ - error: 严重错误(连续崩溃、登录失效、端口占用、大量 403)
100
+ - 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
101
+ }
102
+
103
+ // ── 响应解析 ──
104
+
105
+ function parseResponse(content) {
106
+ try {
107
+ const parsed = JSON.parse(content);
108
+ return {
109
+ severity: parsed.severity || "ok",
110
+ summary: parsed.summary || "",
111
+ suggestions: Array.isArray(parsed.suggestions)
112
+ ? parsed.suggestions.slice(0, 3)
113
+ : [],
114
+ };
115
+ } catch {
116
+ // 尝试提取 JSON 块
117
+ const match = content.match(/\{[\s\S]*\}/);
118
+ if (match) {
119
+ try {
120
+ const parsed = JSON.parse(match[0]);
121
+ return {
122
+ severity: parsed.severity || "ok",
123
+ summary: parsed.summary || "",
124
+ suggestions: Array.isArray(parsed.suggestions)
125
+ ? parsed.suggestions.slice(0, 3)
126
+ : [],
127
+ };
128
+ } catch {}
129
+ }
130
+ // 解析失败:将原始内容作为 summary
131
+ return {
132
+ severity: "warn",
133
+ summary: content.substring(0, 100),
134
+ suggestions: [],
135
+ };
136
+ }
137
+ }
@@ -11,10 +11,27 @@
11
11
 
12
12
  import { spawn, execSync } from "child_process";
13
13
  import { EventEmitter } from "events";
14
+ import { killEdgeProcesses } from "../lib/browser/cdp.js";
15
+ import { writeFileSync, unlinkSync } from "fs";
16
+ import { tmpdir } from "os";
17
+ import { join } from "path";
14
18
 
15
19
  const MAX_RESTART = 3;
20
+ const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
16
21
  const IS_WIN = process.platform === "win32";
17
22
 
23
+ // 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
24
+ function extractCdpPorts(cmdLine) {
25
+ const match = cmdLine.match(/--base-port\s+(\d+)/);
26
+ if (!match) return [];
27
+ const base = parseInt(match[1], 10);
28
+ const countMatch = cmdLine.match(/--port-count\s+(\d+)/);
29
+ const count = countMatch ? parseInt(countMatch[1], 10) : 1;
30
+ const ports = [];
31
+ for (let i = 0; i < count; i++) ports.push(base + i);
32
+ return ports;
33
+ }
34
+
18
35
  // 引号感知的命令拆分:"a b" "c d" → ["a b", "c d"]
19
36
  function splitCommand(cmdLine) {
20
37
  const parts = [];
@@ -46,27 +63,45 @@ function splitCommand(cmdLine) {
46
63
 
47
64
  function getProcessStats(pid) {
48
65
  try {
49
- const cmd = IS_WIN
50
- ? `powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).WorkingSet64"`
51
- : `ps -p ${pid} -o rss=,etime=`;
52
- const stdout = execSync(cmd, {
66
+ if (IS_WIN) {
67
+ // 与 tag.js handleScoreAll 一致:用临时 .ps1 + Get-CimInstance Win32_Process + WorkingSetSize
68
+ const ps1Path = join(tmpdir(), `wd-proc-${pid}-${Date.now()}.ps1`);
69
+ try {
70
+ const ps1Content =
71
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId='${pid}'" -ErrorAction SilentlyContinue; ` +
72
+ `if ($p) { Write-Output $p.WorkingSetSize } else { Write-Output 0 }`;
73
+ writeFileSync(ps1Path, ps1Content);
74
+ const stdout = execSync(
75
+ `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`,
76
+ {
77
+ encoding: "utf-8",
78
+ timeout: 5000,
79
+ stdio: ["ignore", "pipe", "ignore"],
80
+ },
81
+ ).trim();
82
+ try {
83
+ unlinkSync(ps1Path);
84
+ } catch {}
85
+ const bytes = parseInt(stdout, 10);
86
+ return {
87
+ memoryMB: bytes > 0 ? Math.round((bytes / 1048576) * 10) / 10 : 0,
88
+ elapsed: null,
89
+ };
90
+ } catch {
91
+ try {
92
+ unlinkSync(ps1Path);
93
+ } catch {}
94
+ return null;
95
+ }
96
+ }
97
+
98
+ // macOS/Linux
99
+ const stdout = execSync(`ps -p ${pid} -o rss=,etime=`, {
53
100
  encoding: "utf-8",
54
101
  timeout: 3000,
55
102
  stdio: ["ignore", "pipe", "ignore"],
56
103
  }).trim();
57
104
  if (!stdout) return null;
58
-
59
- if (IS_WIN) {
60
- // PowerShell 返回字节数,运行时长从内部 startTime 计算
61
- const memBytes = parseInt(stdout, 10);
62
- return {
63
- memoryMB: isNaN(memBytes)
64
- ? 0
65
- : Math.round((memBytes / 1048576) * 10) / 10,
66
- elapsed: null,
67
- };
68
- }
69
-
70
105
  const parts = stdout.split(/\s+/);
71
106
  const rssKB = parseInt(parts[0], 10);
72
107
  const elapsed = parts.slice(1).join(" ") || "0";
@@ -112,9 +147,9 @@ export class ProcessManager extends EventEmitter {
112
147
  command = parts[0] || command;
113
148
  args = parts.slice(1);
114
149
  }
115
- // Windows .cmd 文件必须 shell:true,macOS/Linux 直接 spawn
150
+ // Windows .cmd 必须 shell:true;为免 DEP0190 警告,传完整命令行字符串
116
151
  child = IS_WIN
117
- ? spawn(command, args, {
152
+ ? spawn([command, ...args].join(" "), {
118
153
  stdio: ["ignore", "pipe", "pipe"],
119
154
  detached: false,
120
155
  shell: true,
@@ -144,7 +179,14 @@ export class ProcessManager extends EventEmitter {
144
179
  child.stderr?.on("data", (data) => {
145
180
  const msg = data.toString().trim();
146
181
  if (msg) {
147
- console.error(`[${id}] ${msg.substring(0, 200)}`);
182
+ const short = msg.substring(0, 200);
183
+ console.error(`[${id}] ${short}`);
184
+ // 环形缓冲:保留最近 N 行供 LLM 分析
185
+ const info = this.processes.get(id);
186
+ if (info) {
187
+ info.logs.push(short);
188
+ if (info.logs.length > MAX_LOG_LINES) info.logs.shift();
189
+ }
148
190
  }
149
191
  });
150
192
 
@@ -195,6 +237,7 @@ export class ProcessManager extends EventEmitter {
195
237
  startTime,
196
238
  restartCount: 0,
197
239
  stopped: false,
240
+ logs: [],
198
241
  });
199
242
  }
200
243
 
@@ -210,6 +253,10 @@ export class ProcessManager extends EventEmitter {
210
253
 
211
254
  if (info.child) {
212
255
  try {
256
+ // 先从命令行提取 CDP 端口,用于关闭脱离进程树的浏览器
257
+ const cmdLine = info.cmdDef?.command || "";
258
+ const ports = extractCdpPorts(cmdLine);
259
+
213
260
  if (IS_WIN) {
214
261
  // Windows: shell:true 产生 cmd.exe 中间层,必须杀整个进程树
215
262
  execSync(`taskkill /PID ${info.child.pid} /T /F`, {
@@ -227,6 +274,11 @@ export class ProcessManager extends EventEmitter {
227
274
  } catch {}
228
275
  }, 2000);
229
276
  }
277
+
278
+ // 全平台:关闭脱离进程树的浏览器(open -a / start msedge 启动的)
279
+ for (const port of ports) {
280
+ killEdgeProcesses(null, port);
281
+ }
230
282
  } catch (e) {
231
283
  console.error(`[watchdog] 停止命令 ${cmdId} 失败: ${e.message}`);
232
284
  }
@@ -280,6 +332,26 @@ export class ProcessManager extends EventEmitter {
280
332
  return result;
281
333
  }
282
334
 
335
+ /**
336
+ * 获取所有进程的日志快照(供 LLM 分析)
337
+ * @returns {Array<{cmdId:string, logs:string[], uptimeSeconds:number, restartCount:number}>}
338
+ */
339
+ getProcessesLogs() {
340
+ const result = [];
341
+ for (const [cmdId, info] of this.processes) {
342
+ if (info.stopped || !info.logs || info.logs.length === 0) continue;
343
+ result.push({
344
+ cmdId,
345
+ logs: info.logs.slice(), // 快照,避免分析时被修改
346
+ uptimeSeconds: Math.round((Date.now() - info.startTime) / 1000),
347
+ restartCount: info.restartCount,
348
+ });
349
+ // 清空已读取的日志,避免重复分析
350
+ info.logs = [];
351
+ }
352
+ return result;
353
+ }
354
+
283
355
  /**
284
356
  * 与期望命令列表同步:
285
357
  * - 启动期望列表中已启用但未运行的命令