@mrrisega/dsh-remote 0.6.10 → 0.6.12
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/clients/dsh-remote/dsh-events.mjs +11 -1
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +79 -0
- package/clients/dsh-remote/wechat-runtime.mjs +72 -4
- package/dsh-setup.mjs +55 -15
- package/package.json +3 -3
- package/packages/dsh-remote-web/lib/client.js +137 -0
- package/packages/dsh-remote-web/lib/fix-prompt.js +160 -0
- package/packages/dsh-remote-web/lib/index.js +748 -18
- package/packages/dsh-remote-web/lib/offline-update.mjs +66 -0
- package/packages/dsh-remote-web/lib/tarball-update.js +442 -0
- package/packages/dsh-remote-web/package.json +5 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +1451 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +1991 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +728 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +767 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim.mjs +163 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +1575 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/package.json +11 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/src/lifecycle.mjs +45 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +2825 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +1984 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +2526 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +2390 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/LICENSE +20 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/README.md +548 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/browser.js +8 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/index.js +22 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/buffer-util.js +131 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/constants.js +19 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/event-target.js +292 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/extension.js +203 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/limiter.js +55 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/receiver.js +743 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/sender.js +607 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/stream.js +161 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/subprotocol.js +62 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/validation.js +152 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/websocket-server.js +562 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/lib/websocket.js +1407 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/package.json +74 -0
- package/packages/dsh-remote-web/runtime/node_modules/ws/wrapper.mjs +21 -0
- package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +396 -0
- package/packages/dsh-remote-web/test/diag-report.test.mjs +236 -0
- package/packages/dsh-remote-web/test/fix-prompt.test.mjs +170 -0
- package/packages/dsh-remote-web/test/runtime-bundle.test.mjs +198 -0
- package/packages/dsh-remote-web/test/tarball-update.test.mjs +460 -0
|
@@ -591,7 +591,17 @@ class EventSubscriber extends EventEmitter {
|
|
|
591
591
|
if (ws !== null) {
|
|
592
592
|
try {
|
|
593
593
|
ws.removeAllListeners();
|
|
594
|
-
|
|
594
|
+
// ⚠️ 先挂一个吞掉的 error 监听:**握手中的**连接被 close() 时,ws 会**异步** emit
|
|
595
|
+
// 'error'("WebSocket was closed before the connection was established")。
|
|
596
|
+
// 上面刚 removeAllListeners(),于是它成了**未捕获错误** —— 真机表现是「解绑」
|
|
597
|
+
// 这一步偶发把流程打挂(测试里是一条 e2e 偶发红,2026-09-23 定位)。
|
|
598
|
+
ws.on("error", () => { /* 关闭途中的错误不该冒泡到业务 */ });
|
|
599
|
+
if (ws.readyState === this.#WebSocketImpl.CONNECTING) {
|
|
600
|
+
// 还没握手完 → terminate():立刻中止,且不产生上面那个错误
|
|
601
|
+
ws.terminate();
|
|
602
|
+
} else {
|
|
603
|
+
ws.close(1000, "subscriber closed");
|
|
604
|
+
}
|
|
595
605
|
} catch {
|
|
596
606
|
/* 关不掉就等 GC */
|
|
597
607
|
}
|
|
@@ -1666,3 +1666,82 @@ test("★ /help 必须逐行分隔:付费档也不许退化成一坨(无换行/
|
|
|
1666
1666
|
await ilink.close();
|
|
1667
1667
|
}
|
|
1668
1668
|
});
|
|
1669
|
+
|
|
1670
|
+
// ---------------------------------------------------------------------------
|
|
1671
|
+
// 日报**重复推送**的根治(2026-09-23 业主实测「每日简报推送了两次」)
|
|
1672
|
+
//
|
|
1673
|
+
// 根因:`#maybeDigest` 是「读 state → 等网络发送 → 写 state」的**跨 await 读-改-写**。
|
|
1674
|
+
// 发送要等网络,这段窗口里第二个执行者会读到"今天还没发" → 两条都发出去。
|
|
1675
|
+
// 真实撞车场景有两个:① 更新期间新旧 bridge 短暂共存;② 一次发送超过 60s,下一次 tick 叠上来。
|
|
1676
|
+
// 修法:`openSync(..., "wx")`(不存在才创建,内核保证原子)占位 —— 只有一个执行者拿得到。
|
|
1677
|
+
// ---------------------------------------------------------------------------
|
|
1678
|
+
|
|
1679
|
+
/** 两个 runtime **共用同一个 relayDir**(模拟更新期间新旧 bridge 短暂共存)。 */
|
|
1680
|
+
async function twoRuntimesSameDir(ilink) {
|
|
1681
|
+
const relayDir = tmp();
|
|
1682
|
+
saveAccount(relayDir, { token: "tok-abc", accountId: "bot-1", baseUrl: ilink.baseUrl, userId: "user-1", boundAt: 1 });
|
|
1683
|
+
const mk = () => {
|
|
1684
|
+
const rt = createWeChatRuntime({
|
|
1685
|
+
relayDir,
|
|
1686
|
+
upstream: "http://127.0.0.1:1",
|
|
1687
|
+
secret: SECRET,
|
|
1688
|
+
tier: "pro",
|
|
1689
|
+
logger: quietLogger()
|
|
1690
|
+
});
|
|
1691
|
+
rt.subscriber = makeFakeSubscriber();
|
|
1692
|
+
rt.digestHour = new Date().getHours(); // 让真实路径通过时刻闸门
|
|
1693
|
+
return rt;
|
|
1694
|
+
};
|
|
1695
|
+
return { rtA: mk(), rtB: mk(), relayDir };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
test("★★ 两个实例共存时日报只能发一条(原子占位 —— 真机曾发两条)", async () => {
|
|
1699
|
+
const ilink = await fakeIlink();
|
|
1700
|
+
try {
|
|
1701
|
+
const { rtA, rtB } = await twoRuntimesSameDir(ilink);
|
|
1702
|
+
const before = ilink.state.sends.length;
|
|
1703
|
+
// 并发触发:模拟两个实例在同一分钟内各自 tick(旧实现会两条都发)
|
|
1704
|
+
await Promise.all([rtA.runMaybeDigestNow(), rtB.runMaybeDigestNow()]);
|
|
1705
|
+
const sent = ilink.state.sends.length - before;
|
|
1706
|
+
assert.equal(sent, 1, `★同一天只能有一条简报,实际发出 ${sent} 条(旧实现:两个执行者都读到"今天还没发")`);
|
|
1707
|
+
} finally {
|
|
1708
|
+
await ilink.close();
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
test("★ 日报占位是陈旧的(>10 分钟)时必须接管,不能把那天彻底吞掉", async () => {
|
|
1713
|
+
const ilink = await fakeIlink();
|
|
1714
|
+
try {
|
|
1715
|
+
const { rt, relayDir } = await boundRuntime(ilink, { subscriber: makeFakeSubscriber(), tier: "pro" });
|
|
1716
|
+
rt.digestHour = new Date().getHours();
|
|
1717
|
+
const day = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}-${String(new Date().getDate()).padStart(2, "0")}`;
|
|
1718
|
+
const lock = path.join(relayDir, `.digest-${day}.sent`);
|
|
1719
|
+
// 造一个 20 分钟前的占位(模拟"上个实例占位后、发送途中被杀")
|
|
1720
|
+
fs.writeFileSync(lock, "");
|
|
1721
|
+
const old = new Date(Date.now() - 20 * 60_000);
|
|
1722
|
+
fs.utimesSync(lock, old, old);
|
|
1723
|
+
|
|
1724
|
+
const before = ilink.state.sends.length;
|
|
1725
|
+
await rt.runMaybeDigestNow();
|
|
1726
|
+
assert.equal(ilink.state.sends.length - before, 1,
|
|
1727
|
+
"★陈旧占位必须被接管并补发 —— 否则进程一被杀,那天就再也不会发简报");
|
|
1728
|
+
} finally {
|
|
1729
|
+
await ilink.close();
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
test("★ 日报占位一天一个,换天时会清掉旧占位(不攒文件)", async () => {
|
|
1734
|
+
const ilink = await fakeIlink();
|
|
1735
|
+
try {
|
|
1736
|
+
const { rt, relayDir } = await boundRuntime(ilink, { subscriber: makeFakeSubscriber(), tier: "pro" });
|
|
1737
|
+
rt.digestHour = new Date().getHours();
|
|
1738
|
+
// 放两个"昨天/前天"的占位
|
|
1739
|
+
fs.writeFileSync(path.join(relayDir, ".digest-2020-01-01.sent"), "");
|
|
1740
|
+
fs.writeFileSync(path.join(relayDir, ".digest-2020-01-02.sent"), "");
|
|
1741
|
+
await rt.runMaybeDigestNow();
|
|
1742
|
+
const left = fs.readdirSync(relayDir).filter((n) => n.startsWith(".digest-") && n.endsWith(".sent"));
|
|
1743
|
+
assert.equal(left.length, 1, `旧占位必须被清掉,只剩今天的,实际:${left.join(", ")}`);
|
|
1744
|
+
} finally {
|
|
1745
|
+
await ilink.close();
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
@@ -1679,25 +1679,93 @@ export class WeChatRuntime {
|
|
|
1679
1679
|
/**
|
|
1680
1680
|
* 每日简报(兼作 24h 推送窗口的心跳)。
|
|
1681
1681
|
*
|
|
1682
|
-
* ⚠️
|
|
1682
|
+
* ⚠️ 旧实现有三个问题,一起修了:
|
|
1683
1683
|
* ① 只把 `{notified, answered}` 交给下游,**没有 lines** → 用户每天收到的是兜底句
|
|
1684
1684
|
* 「今天暂时没有要做的事。」—— 那不是"今天干了啥",等于白发(业主:"日报发的内容有点问题")。
|
|
1685
1685
|
* ② 先把 `last_digest_day` 落盘**再**发送 → 一旦这次没发出去(网络抖动/节点被权限关掉),
|
|
1686
1686
|
* 当天的简报就被**烧掉**、当天再也不会补发。
|
|
1687
|
+
* ③ 「读 state → 发送 → 写 state」是一段**跨 await 的读-改-写**:发送要等网络,
|
|
1688
|
+
* 这段窗口里第二个执行者会读到"今天还没发"→ **两条都发出去**
|
|
1689
|
+
* (2026-09-23 真机:业主收到两条简报)。现在用**原子占位**把"今天这条归我发"锁死。
|
|
1687
1690
|
*/
|
|
1688
1691
|
async #maybeDigest() {
|
|
1689
1692
|
if (this.stopping || !this.channel.account || !this.subscriber) return;
|
|
1690
1693
|
const now = this.clock();
|
|
1691
1694
|
const hour = new Date(now).getHours();
|
|
1692
1695
|
if (hour !== this.digestHour) return;
|
|
1696
|
+
const day = this.#today(now);
|
|
1693
1697
|
const state = loadState(this.relayDir);
|
|
1694
|
-
if (state.last_digest_day ===
|
|
1698
|
+
if (state.last_digest_day === day) return; // 今天已发
|
|
1699
|
+
// ★ 先原子占位再发送:两个实例/两次 tick 并存时,只有一个能拿到(见 #claimDigest)
|
|
1700
|
+
const claim = this.#claimDigest(day);
|
|
1701
|
+
if (!claim.ok) return;
|
|
1695
1702
|
const s = this.todayStats;
|
|
1696
1703
|
const lines = this.#digestLines();
|
|
1697
1704
|
const node = this.subscriber.digestDue({ lines, notified: s.notified, answered: s.answered });
|
|
1698
|
-
|
|
1705
|
+
let r = null;
|
|
1706
|
+
try {
|
|
1707
|
+
r = await this.notify(node);
|
|
1708
|
+
} finally {
|
|
1709
|
+
// 没真发出去 → 释放占位,留给下一 tick 补发(见上面 ②:不能把当天烧掉)
|
|
1710
|
+
if (!(r && r.ok)) this.#releaseDigest(claim);
|
|
1711
|
+
}
|
|
1699
1712
|
// ★ 只有**真的发出去了**才记"今天已发"(见上面 ②)
|
|
1700
|
-
if (r && r.ok) this.channel.writeState({ last_digest_day:
|
|
1713
|
+
if (r && r.ok) this.channel.writeState({ last_digest_day: day });
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* 原子占位「今天这条简报由我来发」。
|
|
1718
|
+
*
|
|
1719
|
+
* 🔴 为什么光看 state 里的 `last_digest_day` 不够:那是**读-改-写**,中间隔着一次网络发送。
|
|
1720
|
+
* 典型撞车场景有两个,都真实发生过:
|
|
1721
|
+
* · 更新期间新旧 bridge **短暂共存**(旧的还没退、新的已起);
|
|
1722
|
+
* · 一次发送耗时超过 60s,下一次定时器 tick **叠**上来。
|
|
1723
|
+
* 两者都会让第二个执行者在写盘之前读到"今天还没发" → 用户收到两条。
|
|
1724
|
+
* `openSync(..., "wx")`(不存在才创建)由内核保证原子,只有一个执行者能拿到 ✅
|
|
1725
|
+
*
|
|
1726
|
+
* 生命期:发成功 → 占位**留着**(它比 state 更强,重启/换实例也不会丢);
|
|
1727
|
+
* 发失败 → 立刻由 `#releaseDigest` 释放,让下一 tick 补发;
|
|
1728
|
+
* 占位超过 10 分钟 = **陈旧**(进程在发送途中被杀)→ 接管它。
|
|
1729
|
+
* 宁可极小概率重发一次,也不要出现"那天彻底不发了"。
|
|
1730
|
+
*/
|
|
1731
|
+
#claimDigest(day) {
|
|
1732
|
+
const file = path.join(this.relayDir, `.digest-${day}.sent`);
|
|
1733
|
+
const STALE_MS = 10 * 60_000;
|
|
1734
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1735
|
+
try {
|
|
1736
|
+
fs.closeSync(fs.openSync(file, "wx"));
|
|
1737
|
+
this.#pruneDigestClaims(day);
|
|
1738
|
+
return { ok: true, file };
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
if (!e || e.code !== "EEXIST") {
|
|
1741
|
+
// 目录不可写等:退回"这次不发",绝不因此把 bridge 打挂(简报是锦上添花)
|
|
1742
|
+
return { ok: false, file };
|
|
1743
|
+
}
|
|
1744
|
+
let age = 0;
|
|
1745
|
+
try { age = Date.now() - fs.statSync(file).mtimeMs; } catch { age = 0; }
|
|
1746
|
+
if (age < STALE_MS) return { ok: false, file }; // 别人正在发 / 今天已经发过
|
|
1747
|
+
try { fs.rmSync(file, { force: true }); } catch { return { ok: false, file }; }
|
|
1748
|
+
// 删掉陈旧占位后重抢一次
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return { ok: false, file };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/** 发送失败时释放占位(交给下一 tick 补发)。 */
|
|
1755
|
+
#releaseDigest(claim) {
|
|
1756
|
+
if (!claim || !claim.file) return;
|
|
1757
|
+
try { fs.rmSync(claim.file, { force: true }); } catch { /* ignore */ }
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/** 清掉别的日期留下的占位文件(一天一个,不清理会一年攒 365 个)。 */
|
|
1761
|
+
#pruneDigestClaims(today) {
|
|
1762
|
+
try {
|
|
1763
|
+
for (const name of fs.readdirSync(this.relayDir)) {
|
|
1764
|
+
if (!name.startsWith(".digest-") || !name.endsWith(".sent")) continue;
|
|
1765
|
+
if (name === `.digest-${today}.sent`) continue;
|
|
1766
|
+
try { fs.rmSync(path.join(this.relayDir, name), { force: true }); } catch { /* ignore */ }
|
|
1767
|
+
}
|
|
1768
|
+
} catch { /* 目录读不了就算了:占位本身仍生效 */ }
|
|
1701
1769
|
}
|
|
1702
1770
|
|
|
1703
1771
|
/** 简报正文:回答"**今天**干了啥"(业主口径:日报是回顾,不是待办清单)。 */
|
package/dsh-setup.mjs
CHANGED
|
@@ -88,8 +88,13 @@ function findDepWs(startDir) {
|
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* 同步运行时文件时一并覆盖的客户端脚本(bridge 会把这些直接发给手机/桌面,必须与装置器一致)。
|
|
93
|
+
* ⚠️ `src/lifecycle.mjs` 也在表里:本安装器**自己**import 它(进程优雅停机的共用实现),
|
|
94
|
+
* 漏同步就会出现「入口脚本在、依赖不在」的半装运行时(插件半据此判定「运行环境已就绪」,
|
|
95
|
+
* 却永远起不来 bridge —— 2026-09-23 用户反馈的现场)。
|
|
96
|
+
*/
|
|
97
|
+
const RUNTIME_CLIENT_FILES = ["mobile-adapter.mjs", "dsh-bridge.mjs", "e2ee-shim.mjs", "e2ee-client.mjs", "e2ee-shim-script.js", "mobile-adapter.test.mjs", "wechat-channel.mjs", "dsh-events.mjs", "wechat-runtime.mjs", "src/lifecycle.mjs"];
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
100
|
* 把"客户端脚本"补齐到配置目录。
|
|
@@ -769,7 +774,39 @@ function restartBridgeService() {
|
|
|
769
774
|
}
|
|
770
775
|
|
|
771
776
|
/**
|
|
772
|
-
* dsh web
|
|
777
|
+
* 上游 dsh web 地址(bridge 的转发目标 + watcher 的「dsh web 在线吗」判据**同一个来源**)。
|
|
778
|
+
*
|
|
779
|
+
* 【2026-09-23 用户实测事故】这里原本处处写死 `http://127.0.0.1:3080`:只要用户的 dsh web
|
|
780
|
+
* 不在默认端口(`dsh web --port 8090`、profile 里配了 port、或 `--port 0` 让系统分配空闲端口),
|
|
781
|
+
* watcher 的在线探测就**永远**失败 → bridge 永远不启动,而面板只会说「正在启动 Bridge…」。
|
|
782
|
+
* 优先级:显式环境变量 > 插件半留下的端口文件 > DSH_WEB_URL > 默认 3080。
|
|
783
|
+
* 为什么要有「端口文件」这一层:开机自启(登录任务 / launchd)拉起 watcher 时**没有**任何环境变量,
|
|
784
|
+
* 只有插件半知道 dsh web 真实监听在哪个端口,所以它会把地址落盘(见插件 half 的 publishUpstream)。
|
|
785
|
+
*/
|
|
786
|
+
const UPSTREAM_FILE = ".dsh-upstream";
|
|
787
|
+
function upstreamUrl() {
|
|
788
|
+
const explicit = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim();
|
|
789
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
790
|
+
try {
|
|
791
|
+
const fromFile = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim();
|
|
792
|
+
if (/^https?:\/\/[^\s]+$/i.test(fromFile)) return fromFile.replace(/\/+$/, "");
|
|
793
|
+
} catch { /* 没写过(旧版/首次)→ 往下回退 */ }
|
|
794
|
+
const web = String(process.env.DSH_WEB_URL || "").trim();
|
|
795
|
+
if (web) {
|
|
796
|
+
try {
|
|
797
|
+
const u = new URL(web);
|
|
798
|
+
if (u.port) return `http://127.0.0.1:${u.port}`; // bridge 的 loopback 围栏只认 127.0.0.1
|
|
799
|
+
} catch { /* 非 URL:忽略 */ }
|
|
800
|
+
}
|
|
801
|
+
return "http://127.0.0.1:3080";
|
|
802
|
+
}
|
|
803
|
+
/** 上游端口(诊断/日志用;取不到就回退 3080)。 */
|
|
804
|
+
function upstreamPort() {
|
|
805
|
+
try { return Number(new URL(upstreamUrl()).port) || 3080; } catch { return 3080; }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* dsh web 是否正在运行(默认 127.0.0.1:3080,端口以 upstreamUrl() 为准)。
|
|
773
810
|
* bridge 本身依赖 dsh web 才工作:dsh web 没开时 bridge 起来也会立刻退出,
|
|
774
811
|
* 所以「装完当下 bridge 没在跑」是**正常状态**,不能当失败吓用户(见 install 汇总)。
|
|
775
812
|
*/
|
|
@@ -777,7 +814,7 @@ async function isDshWebUp(timeoutMs = 1200) {
|
|
|
777
814
|
try {
|
|
778
815
|
const ac = new AbortController();
|
|
779
816
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
780
|
-
await fetch("
|
|
817
|
+
await fetch(upstreamUrl() + "/", { signal: ac.signal });
|
|
781
818
|
clearTimeout(timer);
|
|
782
819
|
return true;
|
|
783
820
|
} catch { return false; }
|
|
@@ -794,13 +831,14 @@ function resolveProfileDir(argv = []) {
|
|
|
794
831
|
}
|
|
795
832
|
|
|
796
833
|
/**
|
|
797
|
-
*
|
|
834
|
+
* 监听本机 dsh web 端口的进程 pid(= dsh web)。端口取 upstreamPort()(不再写死 3080)。
|
|
798
835
|
* macOS/Linux:lsof → pgrep;Windows:netstat -ano 取 LISTENING 行的最后一列
|
|
799
836
|
* (netstat 是系统自带,比每次都起 PowerShell 快得多)。
|
|
800
837
|
*/
|
|
801
838
|
function dshWebPid() {
|
|
839
|
+
const port = upstreamPort();
|
|
802
840
|
if (IS_WIN) {
|
|
803
|
-
const r = sh(
|
|
841
|
+
const r = sh(`netstat -ano | findstr ":${port}"`);
|
|
804
842
|
for (const line of String(r.stdout || "").split("\n")) {
|
|
805
843
|
if (!/LISTENING/i.test(line)) continue;
|
|
806
844
|
const m = line.trim().match(/(\d+)\s*$/);
|
|
@@ -808,7 +846,7 @@ function dshWebPid() {
|
|
|
808
846
|
}
|
|
809
847
|
return null;
|
|
810
848
|
}
|
|
811
|
-
const l = sh(
|
|
849
|
+
const l = sh(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`);
|
|
812
850
|
const pid = String(l.stdout || "").trim().split(/\s+/).filter(Boolean)[0];
|
|
813
851
|
if (pid && /^\d+$/.test(pid)) return Number(pid);
|
|
814
852
|
const p = sh("pgrep -f 'dsh web' 2>/dev/null | head -1");
|
|
@@ -884,7 +922,7 @@ function restartDshWeb() {
|
|
|
884
922
|
*/
|
|
885
923
|
function restartDshWebWindows() {
|
|
886
924
|
const pid = dshWebPid();
|
|
887
|
-
if (!pid) return { ok: false, how: "", detail:
|
|
925
|
+
if (!pid) return { ok: false, how: "", detail: `未找到监听 ${upstreamPort()} 的进程(dsh web 没在运行?)` };
|
|
888
926
|
const cmdline = windowsProcessCommandLine(pid);
|
|
889
927
|
if (!cmdline) return { ok: false, how: "", detail: "无法获取 dsh web 的启动命令行,请手动重启" };
|
|
890
928
|
if (/_npx|node_modules\\\.bin|node_modules\/\.bin/.test(cmdline)) {
|
|
@@ -1087,15 +1125,15 @@ async function runBridge() {
|
|
|
1087
1125
|
/** 限流提示只打一次,避免刷屏。 */
|
|
1088
1126
|
let warnedRateLimit = false;
|
|
1089
1127
|
|
|
1090
|
-
// watcher:检测 dsh web
|
|
1128
|
+
// watcher:检测 dsh web 是否存活(端口以 upstreamUrl() 为准,绝不写死 3080),存活才启动 bridge
|
|
1091
1129
|
const checkUpstream = () => new Promise((resolve) => {
|
|
1092
1130
|
const t = setTimeout(() => resolve(false), 2000);
|
|
1093
|
-
fetch("
|
|
1131
|
+
fetch(upstreamUrl() + "/")
|
|
1094
1132
|
.then(() => { clearTimeout(t); resolve(true); })
|
|
1095
1133
|
.catch(() => { clearTimeout(t); resolve(false); });
|
|
1096
1134
|
});
|
|
1097
1135
|
|
|
1098
|
-
console.log(
|
|
1136
|
+
console.log(`[dsh-remote] 等待 dsh web(${upstreamUrl()})启动...`);
|
|
1099
1137
|
let bridgeProc = null;
|
|
1100
1138
|
let starting = false;
|
|
1101
1139
|
|
|
@@ -1158,6 +1196,8 @@ async function runBridge() {
|
|
|
1158
1196
|
...process.env,
|
|
1159
1197
|
DSH_BRIDGE_CONFIG: CONFIG_PATH,
|
|
1160
1198
|
DSH_BRIDGE_TUNNEL_URL: cfg.tunnel_url,
|
|
1199
|
+
// 上游端口显式传给 bridge:它自己在旧版本里也回退到写死的 3080(见 dsh-bridge.mjs 的 UPSTREAM)。
|
|
1200
|
+
DSH_BRIDGE_UPSTREAM: upstreamUrl(),
|
|
1161
1201
|
// 安装口径透传给 bridge(bridge 把它随设备登记请求一起上报给账号 API)
|
|
1162
1202
|
DSH_BRIDGE_INSTALL_SOURCE: INSTALL_SOURCE,
|
|
1163
1203
|
DSH_BRIDGE_INSTALL_VERSION: INSTALL_VERSION,
|
|
@@ -1335,7 +1375,7 @@ async function setup(argv) {
|
|
|
1335
1375
|
fs.writeFileSync(patchPath, cur);
|
|
1336
1376
|
} catch { /* 忽略:轮询会给出结论 */ }
|
|
1337
1377
|
for (let i = 0; i < 45; i += 1) {
|
|
1338
|
-
const r = await fetch("
|
|
1378
|
+
const r = await fetch(upstreamUrl() + "/dsh-remote/self", { signal: AbortSignal.timeout(900) })
|
|
1339
1379
|
.catch(() => null);
|
|
1340
1380
|
if (r && r.ok) {
|
|
1341
1381
|
const j = await r.json().catch(() => null);
|
|
@@ -1448,7 +1488,7 @@ async function setup(argv) {
|
|
|
1448
1488
|
L.push(" 再打开 设置 → 「远程访问」→ 注册/登录手机号即可。");
|
|
1449
1489
|
} else if (needRestart && restartResult && restartResult.ok) {
|
|
1450
1490
|
L.push("");
|
|
1451
|
-
L.push(" 下一步: dsh web 已重启,直接打开
|
|
1491
|
+
L.push(" 下一步: dsh web 已重启,直接打开 " + upstreamUrl() + " → 设置 → 「远程访问」→ 注册/登录手机号。");
|
|
1452
1492
|
} else if (needRestart && pluginResult && pluginResult.hotPatch && pluginResult.changed) {
|
|
1453
1493
|
// patch 已写入但探测窗口内没等到面板接口:热加载很可能还在进行(插件节点半启动+注册路由)。
|
|
1454
1494
|
// 这种情况**不能**直接让用户重启 —— 先让他等几秒刷新,再给兜底方案。
|
|
@@ -1466,8 +1506,8 @@ async function setup(argv) {
|
|
|
1466
1506
|
L.push("⚠️ 还需要重启一次 dsh web,插件面板才会出现(插件只在进程启动时装载)。");
|
|
1467
1507
|
L.push(" 请手动执行(任选其一):");
|
|
1468
1508
|
L.push(" launchctl kickstart -k gui/$(id -u)/com.dshweb.dev # 用 launchd 托管时");
|
|
1469
|
-
L.push(
|
|
1470
|
-
L.push(
|
|
1509
|
+
L.push(` kill $(lsof -ti tcp:${upstreamPort()} -sTCP:LISTEN) && dsh web --no-open # 手动启动时`);
|
|
1510
|
+
L.push(` 重启后:打开 ${upstreamUrl()} → 设置 → 「远程访问」→ 注册/登录手机号。`);
|
|
1471
1511
|
} else {
|
|
1472
1512
|
L.push("");
|
|
1473
1513
|
L.push(" 下一步: 打开 dsh web → 设置 → 「远程访问」→ 注册/登录手机号即可(无需任何命令)。");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"test": "npm run test:router && npm run test:plugin && npm run test:bridge",
|
|
54
54
|
"test:router": "NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/native-app-entry.test.mjs packages/relay-router/test/presence-report.test.mjs packages/relay-router/test/logger.test.mjs",
|
|
55
|
-
"_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)。DSH_RELAY_DEFAULT_API= 死端口:用例拆掉临时目录后,仍在飞的后台请求(装机上报重试等)会读到空配置并回落「默认云端地址」—— 指到本地死端口才不会打生产。",
|
|
56
|
-
"test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
55
|
+
"_comment:test:plugin": "DSH_RELAY_BUNDLED_RUNTIME=/nonexistent = 强制走 npx 兜底分支(否则本机跑过 npm pack 后 packages/dsh-remote-web/runtime 存在,用例会改成走本地拷贝)。DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)。DSH_RELAY_DEFAULT_API= 死端口:用例拆掉临时目录后,仍在飞的后台请求(装机上报重试等)会读到空配置并回落「默认云端地址」—— 指到本地死端口才不会打生产。",
|
|
56
|
+
"test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 DSH_RELAY_BUNDLED_RUNTIME=/nonexistent-dsh-bundled NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
57
57
|
"_comment:test:bridge": "⚠️ 必须 --test-concurrency=1(与 test:plugin 对齐):tunnel-auth 有一条「bridge 5 秒内必须重连」的硬期限,并行跑时新增用例抢 CPU 会把它撞破 → 偶发假红(实测单独跑 2/2 通过、并发跑偶发 0/2)。假红比慢更贵。",
|
|
58
58
|
"test:bridge": "DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 clients/dsh-remote/test/*.test.mjs",
|
|
59
59
|
"check": "node --check scripts/check-profile-activation.mjs && node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/wechat-channel.mjs && node --check clients/dsh-remote/wechat-runtime.mjs && node --check clients/dsh-remote/dsh-events.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && node --check packages/relay-router/src/index.mjs && node --check packages/relay-router/src/logger.mjs && bash -n deploy/install-open.sh && grep -q 'command -v setsid' deploy/install-open.sh && grep -q 'command -v ss' deploy/install-open.sh",
|
|
@@ -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 一律不渲染
|