@mrrisega/dsh-remote 0.6.11 → 0.6.13

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.
Files changed (48) hide show
  1. package/clients/dsh-remote/dsh-bridge.mjs +123 -2
  2. package/clients/dsh-remote/mobile-adapter.mjs +108 -4
  3. package/clients/dsh-remote/test/bridge-gzip.test.mjs +68 -1
  4. package/clients/dsh-remote/test/mobile-adapter-late-frame.test.mjs +388 -0
  5. package/dsh-setup.mjs +55 -15
  6. package/package.json +4 -4
  7. package/packages/dsh-remote-web/lib/client.js +137 -0
  8. package/packages/dsh-remote-web/lib/fix-prompt.js +160 -0
  9. package/packages/dsh-remote-web/lib/index.js +748 -18
  10. package/packages/dsh-remote-web/lib/offline-update.mjs +66 -0
  11. package/packages/dsh-remote-web/lib/tarball-update.js +442 -0
  12. package/packages/dsh-remote-web/package.json +5 -1
  13. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +1572 -0
  14. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +1991 -0
  15. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +728 -0
  16. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +767 -0
  17. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim.mjs +163 -0
  18. package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +1679 -0
  19. package/packages/dsh-remote-web/runtime/clients/dsh-remote/package.json +11 -0
  20. package/packages/dsh-remote-web/runtime/clients/dsh-remote/src/lifecycle.mjs +45 -0
  21. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +2825 -0
  22. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +1984 -0
  23. package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +2526 -0
  24. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +2390 -0
  25. package/packages/dsh-remote-web/runtime/node_modules/ws/LICENSE +20 -0
  26. package/packages/dsh-remote-web/runtime/node_modules/ws/README.md +548 -0
  27. package/packages/dsh-remote-web/runtime/node_modules/ws/browser.js +8 -0
  28. package/packages/dsh-remote-web/runtime/node_modules/ws/index.js +22 -0
  29. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/buffer-util.js +131 -0
  30. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/constants.js +19 -0
  31. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/event-target.js +292 -0
  32. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/extension.js +203 -0
  33. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/limiter.js +55 -0
  34. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/permessage-deflate.js +530 -0
  35. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/receiver.js +743 -0
  36. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/sender.js +607 -0
  37. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/stream.js +161 -0
  38. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/subprotocol.js +62 -0
  39. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/validation.js +152 -0
  40. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/websocket-server.js +562 -0
  41. package/packages/dsh-remote-web/runtime/node_modules/ws/lib/websocket.js +1407 -0
  42. package/packages/dsh-remote-web/runtime/node_modules/ws/package.json +74 -0
  43. package/packages/dsh-remote-web/runtime/node_modules/ws/wrapper.mjs +21 -0
  44. package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +396 -0
  45. package/packages/dsh-remote-web/test/diag-report.test.mjs +236 -0
  46. package/packages/dsh-remote-web/test/fix-prompt.test.mjs +170 -0
  47. package/packages/dsh-remote-web/test/runtime-bundle.test.mjs +198 -0
  48. package/packages/dsh-remote-web/test/tarball-update.test.mjs +460 -0
@@ -2103,6 +2103,16 @@ window.__ModuleLoader__.load({
2103
2103
  var mktFoldArr = useState(mktMoreWasFolded() || inviteTipWasDismissed()); var mktFolded = mktFoldArr[0]; var setMktFolded = mktFoldArr[1];
2104
2104
  // 「更多」这一层当前是否展开:纯界面态,不持久化(下次进面板回到收起,主界面保持干净)。
2105
2105
  var moreOpenArr = useState(false); var moreOpen = moreOpenArr[0]; var setMoreOpen = moreOpenArr[1];
2106
+ // 🧾 故障上报 / 修复提示词(0.6.12):全部 hook 追加在末尾,不动既有 hook 序号。
2107
+ // diagView: null=不展开 | "report"=上报(预览+确认) | "fix"=让 DSH 自己修(提示词+回传)
2108
+ var diagViewArr = useState(null); var diagView = diagViewArr[0]; var setDiagView = diagViewArr[1];
2109
+ var diagBundleArr = useState(null); var diagBundle = diagBundleArr[0]; var setDiagBundle = diagBundleArr[1];
2110
+ var diagNoteArr = useState(""); var diagNote = diagNoteArr[0]; var setDiagNote = diagNoteArr[1];
2111
+ var diagAttachArr = useState(true); var diagAttachLogs = diagAttachArr[0]; var setDiagAttachLogs = diagAttachArr[1];
2112
+ var diagBusyArr = useState(""); var diagBusy = diagBusyArr[0]; var setDiagBusy = diagBusyArr[1];
2113
+ var diagMsgArr = useState(null); var diagMsg = diagMsgArr[0]; var setDiagMsg = diagMsgArr[1];
2114
+ var fixPromptArr = useState(null); var fixPrompt = fixPromptArr[0]; var setFixPrompt = fixPromptArr[1];
2115
+ var fixReportArr = useState(""); var fixReport = fixReportArr[0]; var setFixReport = fixReportArr[1];
2106
2116
 
2107
2117
  var refresh = useCallback(function () {
2108
2118
  setBusy("status");
@@ -2589,6 +2599,67 @@ window.__ModuleLoader__.load({
2589
2599
  });
2590
2600
  };
2591
2601
 
2602
+ // ── 🧾 故障上报(用户主动、可预览、可编辑)与「让 DeepSeek 帮我修」(0.6.12) ──
2603
+ // 产品原则(2026-09-23 定的):**绝不**后台自动上传日志。开发者要看得见故障现场,
2604
+ // 但必须由用户自己点、并且能看见"将要发出去的每一个字"。于是:
2605
+ // ① 上报道:预览全文(正文+日志分片)→ 用户可写描述、可取消附带日志 → 确认才发;
2606
+ // ② 修复道:给用户一段提示词,让他本机的 DSH 自己修并产出结论报告,再把报告回传。
2607
+ // 这两条把"客服来回问"变成"一次到位",而且不需要我们发版。
2608
+
2609
+ /** 打开「上报故障」:先拉预览(= 确认后真正会发出去的全文)。 */
2610
+ var openDiagReport = function () {
2611
+ setDiagView("report"); setDiagMsg(null); setDiagBundle(null);
2612
+ setDiagBusy("bundle");
2613
+ api("/dsh-remote/diag/bundle").then(function (b) {
2614
+ if (b && b.ok) setDiagBundle(b);
2615
+ else setDiagMsg({ kind: "err", text: "读取诊断信息失败,请稍后重试。" });
2616
+ }).catch(function (e) {
2617
+ setDiagMsg({ kind: "err", text: "读取诊断信息失败:" + e.message });
2618
+ }).finally(function () { setDiagBusy(""); });
2619
+ };
2620
+
2621
+ /** 提交上报:正文(含用户描述)+ 日志分片,全部走既有反馈通道。 */
2622
+ var submitDiagReport = function (extra) {
2623
+ var body = {
2624
+ note: extra && extra.note !== undefined ? extra.note : diagNote,
2625
+ attachLogs: diagAttachLogs,
2626
+ report: (extra && extra.report) || "",
2627
+ };
2628
+ setDiagBusy("report"); setDiagMsg(null);
2629
+ post("/dsh-remote/diag/report", body).then(function (b) {
2630
+ if (b && b.ok) {
2631
+ var warn = (b.warnings && b.warnings.length) ? "(" + b.warnings.join(";") + ")" : "";
2632
+ setDiagMsg({ kind: "ok", text: "✅ 已上报给开发者(编号 " + (b.id || "?") + ")" + warn + "。可在「用户反馈 → 我的反馈」里看到后续回复。" });
2633
+ setFixReport(""); setDiagNote("");
2634
+ } else {
2635
+ setDiagMsg({ kind: "err", text: (b && b.error) || "上报失败,请稍后重试。" });
2636
+ }
2637
+ }).catch(function (e) {
2638
+ setDiagMsg({ kind: "err", text: "上报失败:" + e.message });
2639
+ }).finally(function () { setDiagBusy(""); });
2640
+ };
2641
+
2642
+ /** 打开「让 DeepSeek 帮我修」:拉提示词(含当前故障上下文)。 */
2643
+ var openFixPrompt = function () {
2644
+ setDiagView("fix"); setDiagMsg(null); setFixPrompt(null);
2645
+ setDiagBusy("prompt");
2646
+ api("/dsh-remote/diag/fix-prompt").then(function (b) {
2647
+ if (b && b.ok && b.prompt) setFixPrompt(b);
2648
+ else setDiagMsg({ kind: "err", text: "读取修复提示词失败,请稍后重试。" });
2649
+ }).catch(function (e) {
2650
+ setDiagMsg({ kind: "err", text: "读取修复提示词失败:" + e.message });
2651
+ }).finally(function () { setDiagBusy(""); });
2652
+ };
2653
+
2654
+ var copyFixPrompt = function () {
2655
+ var text = (fixPrompt && fixPrompt.prompt) || "";
2656
+ if (!text) return;
2657
+ copyText(text).then(function (done) {
2658
+ if (done) setDiagMsg({ kind: "ok", text: "✅ 提示词已复制。粘到 DeepSeek Harness 里发送,它会自己检查并修复;跑完把它的结论报告贴回下面。" });
2659
+ else setDiagMsg({ kind: "warn", text: "复制失败(浏览器可能限制了剪贴板):可手动选中下面的文本复制。" });
2660
+ });
2661
+ };
2662
+
2592
2663
  function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
2593
2664
 
2594
2665
  // ---------- 登录(密码/短信) ----------
@@ -3652,6 +3723,68 @@ window.__ModuleLoader__.load({
3652
3723
  * 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
3653
3724
  * 只有失败(error)才给可操作项:重试 / 复制诊断信息 / 日志路径,绝不出现死胡同。
3654
3725
  */
3726
+ /**
3727
+ * 🧾 故障上报 / 让 DeepSeek 帮我修(0.6.12)
3728
+ * 产品原则:绝不后台自动上传日志 —— 这里全部由用户主动触发,且**先把要发的内容摊开给他看**。
3729
+ */
3730
+ function renderDiagTools() {
3731
+ var rows = [];
3732
+ if (diagMsg) {
3733
+ rows.push(h("div", { className: "dru-msg" + (diagMsg.kind ? " dru-msg-" + diagMsg.kind : ""), style: { marginTop: 6 } }, diagMsg.text));
3734
+ }
3735
+ if (diagView === null) {
3736
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
3737
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "" || diagBusy !== "", onClick: openDiagReport },
3738
+ diagBusy === "bundle" ? "准备中…" : "🧾 上报故障给开发者"),
3739
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "" || diagBusy !== "", onClick: openFixPrompt },
3740
+ diagBusy === "prompt" ? "准备中…" : "🛠 让 DeepSeek 帮我修")));
3741
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
3742
+ "「上报」会先让你看到将要发送的全部内容(可写描述、可取消附带日志),你确认后才发给我们;"
3743
+ + "「帮我修」会给你一段提示词,让电脑上的 DeepSeek Harness 自己排查并修复。"));
3744
+ return h("div", null, rows);
3745
+ }
3746
+ if (diagView === "report") {
3747
+ var preview = diagBundle ? (diagBundle.summary + (diagBundle.parts && diagBundle.parts.length ? "\n\n" + diagBundle.parts.join("\n\n") : "")) : "";
3748
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
3749
+ "下面就是**将要发送**的全部内容(可先改成你愿意分享的样子;手机号与密钥已自动隐去):"));
3750
+ rows.push(h("pre", {
3751
+ style: { margin: "6px 0 0", padding: "8px 10px", maxHeight: 220, overflow: "auto", whiteSpace: "pre-wrap", wordBreak: "break-all", fontSize: 12, lineHeight: 1.5, background: "var(--dru-surface-2)", border: "1px solid var(--dru-border-ctl)", borderRadius: 8 },
3752
+ }, diagBusy === "bundle" ? "正在准备…" : (preview || "(暂无内容)")));
3753
+ rows.push(h("textarea", {
3754
+ className: "dru-fb-textarea", style: { marginTop: 6 }, maxLength: 600, value: diagNote,
3755
+ placeholder: "用一句话说说你遇到的问题(可选,但很有帮助)",
3756
+ onChange: function (e) { setDiagNote(e.target.value); },
3757
+ }));
3758
+ rows.push(h("label", { className: "dru-hint", style: { display: "block", marginTop: 6, cursor: "pointer" } },
3759
+ h("input", { type: "checkbox", checked: diagAttachLogs, onChange: function (e) { setDiagAttachLogs(e.target.checked); } }),
3760
+ " 附带两个日志的末尾(不含手机号/密码/令牌)"));
3761
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
3762
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: diagBusy !== "" || !diagBundle, onClick: function () { submitDiagReport(); } },
3763
+ diagBusy === "report" ? "上报中…" : "确认上报"),
3764
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: diagBusy !== "", onClick: function () { setDiagView(null); setDiagMsg(null); } }, "取消")));
3765
+ return h("div", null, rows);
3766
+ }
3767
+ // fix:复制提示词 → 本机 DSH 自己修 → 把结论报告贴回来(可选回传给我们)
3768
+ var promptText = (fixPrompt && fixPrompt.prompt) || "";
3769
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
3770
+ "① 复制下面的提示词;② 粘到电脑上的 DeepSeek Harness 里发送,它会自己检查并修复;③ 把它给出的结论报告贴回下面,回传给我们(可选)。"));
3771
+ rows.push(h("pre", {
3772
+ style: { margin: "6px 0 0", padding: "8px 10px", maxHeight: 200, overflow: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 12, lineHeight: 1.5, background: "var(--dru-surface-2)", border: "1px solid var(--dru-border-ctl)", borderRadius: 8 },
3773
+ }, diagBusy === "prompt" ? "正在准备…" : (promptText || "(暂无内容)")));
3774
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 6 } },
3775
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: !promptText, onClick: copyFixPrompt }, "📋 复制提示词")));
3776
+ rows.push(h("textarea", {
3777
+ className: "dru-fb-textarea", style: { marginTop: 6 }, maxLength: 20000, value: fixReport,
3778
+ placeholder: "把 DeepSeek Harness 给出的「结论报告」整段贴到这里,我们一起看",
3779
+ onChange: function (e) { setFixReport(e.target.value); },
3780
+ }));
3781
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
3782
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: diagBusy !== "" || !fixReport.trim(), onClick: function () { submitDiagReport({ report: fixReport, note: diagNote || "(来自「让 DeepSeek 帮我修」的回传报告)" }); } },
3783
+ diagBusy === "report" ? "回传中…" : "回传结论报告"),
3784
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: diagBusy !== "", onClick: function () { setDiagView(null); setDiagMsg(null); } }, "取消")));
3785
+ return h("div", null, rows);
3786
+ }
3787
+
3655
3788
  function renderConnectBlock() {
3656
3789
  if (!loggedInSaaS || !connInfo) return null;
3657
3790
  var phase = connInfo.phase || "";
@@ -3675,6 +3808,8 @@ window.__ModuleLoader__.load({
3675
3808
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
3676
3809
  rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
3677
3810
  "查看日志:后台服务 " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
3811
+ var diagToolsErr = renderDiagTools();
3812
+ if (diagToolsErr) rows.push(h("div", { style: { marginTop: 8, borderTop: "1px dashed var(--dru-border-ctl)", paddingTop: 8 } }, diagToolsErr));
3678
3813
  return h("div", null, rows);
3679
3814
  }
3680
3815
  // 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
@@ -3697,6 +3832,8 @@ window.__ModuleLoader__.load({
3697
3832
  rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
3698
3833
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
3699
3834
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
3835
+ var diagTools = renderDiagTools();
3836
+ if (diagTools) rows.push(h("div", { style: { marginTop: 8, borderTop: "1px dashed var(--dru-border-ctl)", paddingTop: 8 } }, diagTools));
3700
3837
  return h("div", null, rows);
3701
3838
  }
3702
3839
  // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 「连接卡住」一键修复提示词 + 固定格式结论报告(纯文本,中文)。
3
+ *
4
+ * 为什么要有这段提示词(真实事故,2026-09-23,Windows 用户):
5
+ * 用户电脑上插件面板一直停在「连接阶段: starting(正在启动 Bridge…)」,手机端看不到任何设备,
6
+ * 反复重装都没用。事后定位到三种机制,它们在面板上**长得一模一样**,光看面板诊断分不开:
7
+ * ① 半装运行环境:~/.dsh-remote/dsh-setup.mjs 在,但它 import 的
8
+ * clients/dsh-remote/src/lifecycle.mjs 不在 → 拉起守护进程时秒退(Cannot find module);
9
+ * ② 守护(watcher,`node <relayDir>/dsh-setup.mjs run`)活着却一直没拉起 bridge,
10
+ * 最常见原因是上游 dsh web 不在默认端口 3080(代码里曾写死 3080);
11
+ * ③ bridge 子进程起来就崩(缺 ws、端口冲突等),崩因只写在 ~/.dsh-remote/.dsh-bridge.log。
12
+ * 这三种机制都只能在用户**本机的真实环境**里取证。所以产品决定:面板给用户一段可一键复制的
13
+ * 提示词,让他在自己电脑的 DeepSeek Harness 里跑,由本机 agent 读日志、验环境、查端口、
14
+ * 重启守护、验证结果,并产出一份固定格式的报告回传给我们。
15
+ *
16
+ * 本文件只产出文本,不做任何 IO;面板侧「复制修复提示词」直接用 buildFixPrompt(ctx)。
17
+ */
18
+
19
+ /** 默认运行环境目录(与 lib/index.js 的 DEFAULT_RELAY_DIR 一致;可被 DSH_RELAY_DIR 覆盖)。 */
20
+ export const DEFAULT_RELAY_DIR = "~/.dsh-remote";
21
+ /** 默认上游端口(与 index.js 的 FALLBACK_UPSTREAM 一致:仅在读不到实际监听端口时使用)。 */
22
+ export const DEFAULT_UPSTREAM_PORT = 3080;
23
+ /** 相对 relayDir 的固定文件名(与 index.js / dsh-bridge.mjs 保持一致)。 */
24
+ export const BRIDGE_LOG_FILE = ".dsh-bridge.log";
25
+ export const INSTALL_LOG_FILE = ".dsh-setup-install.log";
26
+ export const BRIDGE_STATE_FILE = ".dsh-bridge-state.json";
27
+ export const UPSTREAM_FILE = ".dsh-upstream";
28
+
29
+ /** 报告第一行(固定标题,机器解析靠它定位)。 */
30
+ export const REPORT_TITLE = "=== dsh-remote 诊断结论 ===";
31
+
32
+ /** 报告必须出现的六个字段(顺序即报告里的顺序)。 */
33
+ export const REPORT_KEYS = ["症状", "机制判定", "关键证据", "已做的修复", "修复结果", "仍需人工处理"];
34
+
35
+ /**
36
+ * 结论报告模板。首行固定为 REPORT_TITLE,随后是六个字段;「机制判定」要求四选一,
37
+ * 「修复结果」要求明确写「已修复」或「未修复」。
38
+ */
39
+ export const REPORT_FORMAT = `${REPORT_TITLE}
40
+ 症状: <一句话说清现象,例如「面板停在 starting(正在启动 Bridge…),手机端无任何设备」>
41
+ 机制判定: ①半装运行环境 | ②守护活着没起作用 | ③bridge 起来就崩 | 其它 —— 四选一,并写一句理由
42
+ 关键证据: <日志/命令输出的原文(手机号等凭据已掩码),每条一行;确实没有时写明「文件不存在」或「末尾 N 行无错误」>
43
+ 已做的修复: <按顺序列出你实际执行的命令与改动;没动过任何东西就写「未做修改」>
44
+ 修复结果: 已修复 | 未修复(必须明确写其中之一)—— 再补一句验证依据
45
+ 仍需人工处理: <需要用户或我们继续做的事;没有就写「无」>`;
46
+
47
+ /**
48
+ * 提示词模板:`{{字段名}}` 是占位符,由 buildFixPrompt(ctx) 替换(缺省时用默认值)。
49
+ * 模板里同时写出关键路径的**默认值**,即使占位符没被替换也能看懂。
50
+ */
51
+ export const FIX_PROMPT = `你是这台电脑上的 DeepSeek Harness 本机 agent。用户用 dsh-remote 插件从手机远程控制这台电脑上的 dsh web;现在插件面板卡在「连接阶段: {{phaseText}}」,手机端看不到任何设备。请在**本机**按下面 6 步顺序诊断并修复,**每一步都要自己验证**(真的跑命令、看输出)之后再进入下一步,不要跳步、不要凭猜下结论。
52
+
53
+ 【现场信息】(面板已上报;下面的缺省值已在括号里写明)
54
+ - 插件版本: {{pluginVersion}}
55
+ - 运行环境版本: {{runtimeVersion}}
56
+ - 运行环境目录 relayDir: {{relayDir}}(默认 ~/.dsh-remote,可被环境变量 DSH_RELAY_DIR 覆盖)
57
+ - 面板当前阶段: {{phaseText}}
58
+ - 上游 dsh web 地址: {{upstreamUrl}}
59
+ - bridge 日志: {{bridgeLogPath}}
60
+ - 安装日志: {{installLogPath}}
61
+ - 状态文件: {{statePath}}(默认 ~/.dsh-remote/.dsh-bridge-state.json;字段 device_id / phase)
62
+ - 上游端口文件: {{upstreamFilePath}}(默认 ~/.dsh-remote/.dsh-upstream)
63
+ - 两个日志的默认位置(上面的路径读不到时先看这里): ~/.dsh-remote/.dsh-bridge.log 、 ~/.dsh-remote/.dsh-setup-install.log
64
+
65
+ 第 1 步 · 读两个日志的末尾,摘出真实报错原文
66
+ - 读 {{bridgeLogPath}} 与 {{installLogPath}} 的最后 80 行(Windows: \`powershell -Command "Get-Content -Tail 80 '<路径>'"\`;macOS/Linux: \`tail -n 80 '<路径>'\`)。
67
+ - 把**真实报错原文**摘进报告:Cannot find module、EADDRINUSE、ECONNREFUSED、缺 ws、退出码等。日志为空时要写明「文件不存在」或「末尾 N 行无错误」,**不要只写「无错误」**。
68
+ - 验证: 确认你读到的最后一行日志的时间,与面板卡住的时间对得上。
69
+
70
+ 第 2 步 · 检查运行环境是否完整(半装 = ① 号机制)
71
+ - 逐个确认存在: {{relayDir}}/dsh-setup.mjs 、 {{relayDir}}/clients/dsh-remote/src/lifecycle.mjs 、 {{relayDir}}/clients/dsh-remote/dsh-bridge.mjs 、 {{relayDir}}/node_modules/ws 。
72
+ (Windows: \`dir\` 或 \`Test-Path\`;macOS/Linux: \`ls -l\`)
73
+ - 任一缺失就是「半装运行环境」,用一键安装器重新补装: \`npx @mrrisega/dsh-remote\`(网络受限时用 \`npx @mrrisega/dsh-remote@latest\`)。
74
+ - 验证: 补装后上述 4 个路径必须全部存在;再跑 \`node --check "{{relayDir}}/clients/dsh-remote/src/lifecycle.mjs"\` 与 \`node --check "{{relayDir}}/clients/dsh-remote/dsh-bridge.mjs"\`,语法检查必须无输出。
75
+
76
+ 第 3 步 · 核对上游端口(② 号机制最常见的成因)
77
+ - 先查这台电脑上 dsh web 到底监听哪个端口: Windows \`netstat -ano | findstr LISTENING\`(再按 PID 找出 dsh/node 那行);macOS/Linux \`lsof -nP -iTCP -sTCP:LISTEN\`;也可以直接看启动 dsh web 的命令行里有没有 \`--port\`。
78
+ - 再读 {{upstreamFilePath}}(文件不存在就按默认 {{defaultUpstreamUrl}},即默认端口 3080 判断)。
79
+ - 两者不一致(例如实际监听 8090,而文件/默认是 3080)→ 按**实际端口**修正: 把正确地址写进 {{upstreamFilePath}}(形如 http://127.0.0.1:8090),或给守护显式设置 DSH_BRIDGE_UPSTREAM,然后重启 bridge。
80
+ - 验证: 确认 bridge 日志里不再出现连不上上游的报错,且 bridge 进程真的活下来。
81
+
82
+ 第 4 步 · 检查守护进程是否在跑、是不是「活着但没起作用」
83
+ - 找到守护进程(命令行形如 \`node {{relayDir}}/dsh-setup.mjs run\`): Windows \`wmic process where "name='node.exe'" get ProcessId,CommandLine\` 或任务管理器;macOS/Linux \`ps aux | grep dsh-setup.mjs\`。
84
+ - 「活着但没起作用」的判据: 守护进程在,但 bridge 进程没被拉起,且 {{bridgeLogPath}} 长时间没有新行。命中就**先结束卡死的守护,再重新拉起**:
85
+ Windows \`taskkill /PID <pid> /T /F\`(/T 连子进程一起收);macOS/Linux \`kill <pid>\`,不退出再用 \`kill -9 <pid>\`。
86
+ - 重新拉起: \`node "{{relayDir}}/dsh-setup.mjs run"\`(后台方式运行,别阻塞你自己的会话);装了自启动服务的也可以重启那个服务。
87
+ - 验证: 确认守护进程确实在以新 PID 运行,并且它把 bridge 拉起来了。
88
+
89
+ 第 5 步 · 验证结果(必须如实)
90
+ - 读 {{statePath}}: 出现 \`device_id\` 且 \`phase\` 为 \`online\` 就算通了;若还是 connecting / bound / error,把 \`last_error\` 原文摘出来。
91
+ - 同时看面板是否从「正在启动 Bridge…」变成「已连接 ✅」(手机上能看到本机设备)。
92
+ - 如实说明是否成功: **没修好也要照实写**,并把你判断的下一个可疑点写进「仍需人工处理」。
93
+ - 验证: 上面两项(状态文件字段 + 面板文案)都要真的看过;只看进程在就宣布成功不算。
94
+
95
+ 第 6 步 · 输出结论报告(固定格式,我们会机器解析)
96
+ 把下面模板**原样**放进一个 \`\`\` 围栏里(Markdown 代码块),作为你回复的最后一段;围栏内第一行必须是 ${REPORT_TITLE},六个字段一个都不能少:
97
+
98
+ \`\`\`
99
+ ${REPORT_FORMAT}
100
+ \`\`\`
101
+
102
+ - 「机制判定」只能四选一,并给出理由:
103
+ · ① 半装运行环境 —— {{relayDir}}/dsh-setup.mjs 在,但 clients/... 里的文件(尤其 clients/dsh-remote/src/lifecycle.mjs)缺失,或日志里出现 \`Cannot find module\` 且守护一拉起就秒退;
104
+ · ② 守护活着没起作用 —— 守护进程在跑、bridge 却从未被拉起,且端口核对发现 dsh web 的实际监听端口与 {{upstreamFilePath}}(或默认 3080)不一致;
105
+ · ③ bridge 起来就崩 —— bridge 进程确实起来过又退出,且 {{bridgeLogPath}} 里有明确崩因(缺 ws、EADDRINUSE 端口冲突等);
106
+ · 三条都对不上时才写「其它」,并把你的证据写清楚。
107
+ - 「修复结果」必须明确写 \`已修复\` 或 \`未修复\`(二者之一),后面补一句验证依据。
108
+ - 验证: 自己核对一遍——围栏内第一行是标题、六个字段一个不少、没有任何凭据与未脱敏的手机号。
109
+
110
+ 【安全与隐私红线(必须遵守)】
111
+ 1. 报告里**不得**出现手机号、密码、token、JWT、bridge_secret、cookie、访问密钥等任何凭据;日志摘录里的手机号必须掩码,例如 138****0000,token / 密钥一律写成 \`<已隐去>\`。
112
+ 2. **不要删除** {{relayDir}} 目录(默认 ~/.dsh-remote)——删了会丢账号与设备身份;也不要为了「重来一次」卸载自启动服务,除非用户明确要求。
113
+ 3. 只做本机修复与取证: 不要把用户的文件内容、会话内容、业务数据贴进报告,报告只写与本次连接问题有关的命令、日志原文(脱敏后)与结论。
114
+ 4. 改任何配置前先看原值(要能改回去);不要执行 rm -rf、格式化磁盘等破坏性操作。`;
115
+
116
+ /** 提示词里使用的占位符: {{字段名}}(双花括号,避免与 JSON/shell 里的单花括号冲突)。 */
117
+ const TOKEN_RE = /\{\{\s*(\w+)\s*\}\}/g;
118
+
119
+ /** 展示用路径拼接: 统一用正斜杠(Windows 的反斜杠放在提示词里更难读)。 */
120
+ function displayJoin(dir, name) {
121
+ return String(dir).replace(/[\\/]+$/, "") + "/" + name;
122
+ }
123
+
124
+ /** 取一个「有内容的字符串」;空白、非字符串一律当缺省处理(避免 undefined / null 漏进文案)。 */
125
+ function text(value) {
126
+ return typeof value === "string" && value.trim() ? value.trim() : "";
127
+ }
128
+
129
+ /**
130
+ * 把 ctx 归一化成占位符表: 缺省字段一律回退到可读的默认文案,
131
+ * 且日志/状态/端口文件路径都能从 relayDir 推导出来(所以给一个 relayDir 就够用)。
132
+ */
133
+ function contextValues(ctx) {
134
+ const src = ctx && typeof ctx === "object" ? ctx : {};
135
+ const relayDir = text(src.relayDir) || DEFAULT_RELAY_DIR;
136
+ const defaultUpstreamUrl = `http://127.0.0.1:${DEFAULT_UPSTREAM_PORT}`;
137
+ return {
138
+ relayDir,
139
+ bridgeLogPath: text(src.bridgeLogPath) || displayJoin(relayDir, BRIDGE_LOG_FILE),
140
+ installLogPath: text(src.installLogPath) || displayJoin(relayDir, INSTALL_LOG_FILE),
141
+ statePath: displayJoin(relayDir, BRIDGE_STATE_FILE),
142
+ upstreamFilePath: displayJoin(relayDir, UPSTREAM_FILE),
143
+ pluginVersion: text(src.pluginVersion) || "未知(面板未上报)",
144
+ runtimeVersion: text(src.runtimeVersion) || "未知(未读到 .dsh-setup-version)",
145
+ upstreamUrl: text(src.upstreamUrl) || `${defaultUpstreamUrl}(默认端口 ${DEFAULT_UPSTREAM_PORT},需按第 3 步核实)`,
146
+ phaseText: text(src.phaseText) || "starting(正在启动 Bridge…)",
147
+ defaultUpstreamUrl,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * 用 ctx 渲染完整提示词。
153
+ * @param {object} [ctx] { relayDir, bridgeLogPath, installLogPath, pluginVersion, runtimeVersion, upstreamUrl, phaseText }
154
+ * @returns {string} 可直接复制给用户本机 agent 的提示词全文
155
+ */
156
+ export function buildFixPrompt(ctx = {}) {
157
+ const values = contextValues(ctx);
158
+ // 未识别的占位符原样保留(宁可留下 {{x}} 也不写出 "undefined")
159
+ return FIX_PROMPT.replace(TOKEN_RE, (raw, key) => (key in values ? String(values[key]) : raw));
160
+ }