@mrrisega/dsh-remote 0.6.13 → 0.6.15
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-bridge.mjs +124 -14
- package/clients/dsh-remote/dsh-events.mjs +22 -2
- package/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/clients/dsh-remote/test/boot-tip.test.mjs +158 -0
- package/clients/dsh-remote/test/dsh-events.test.mjs +89 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +73 -3
- package/clients/dsh-remote/test/e2ee-client.test.mjs +42 -1
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +10 -1
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +18 -1
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +325 -0
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +3 -1
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +196 -0
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +268 -1
- package/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/dsh-setup.mjs +225 -23
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +31 -1
- package/packages/dsh-remote-web/lib/index.js +464 -15
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +124 -14
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +22 -2
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +225 -23
- package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +16 -1
- package/packages/dsh-remote-web/test/doctor-cli.test.mjs +117 -0
- package/packages/dsh-remote-web/test/linux-bridge.test.mjs +346 -0
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +201 -0
- package/packages/dsh-remote-web/test/self-manage.test.mjs +12 -0
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +170 -0
package/dsh-setup.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import path from "node:path";
|
|
|
29
29
|
import { spawn, spawnSync, execSync } from "node:child_process";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { childStopped, stopChildGracefully } from "./clients/dsh-remote/src/lifecycle.mjs";
|
|
32
|
+
import { discoverUpstream, resolveUpstreamHint, FALLBACK_UPSTREAM } from "./clients/dsh-remote/upstream-discovery.mjs";
|
|
32
33
|
|
|
33
34
|
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url)); // 本包目录(仓库或 node_modules)
|
|
34
35
|
const IS_NPM_INSTALL = THIS_DIR.includes(`${path.sep}node_modules${path.sep}`);
|
|
@@ -169,6 +170,7 @@ function printHelp() {
|
|
|
169
170
|
dsh-remote status 查看配置与服务状态
|
|
170
171
|
dsh-remote plugin 手动安装 dsh web 远程访问插件(--uninstall 卸载)
|
|
171
172
|
dsh-remote repair 修复插件挂载(dsh web 起不来时用;只碰 profile,不联网)
|
|
173
|
+
dsh-remote doctor 一条命令自检「为什么连不上」(只读;含上游/进程/微信/日志尾部)
|
|
172
174
|
dsh-remote --help 显示本用法(等同 dsh-remote help)
|
|
173
175
|
|
|
174
176
|
自建模式(可选):
|
|
@@ -201,7 +203,11 @@ const KNOWN_FLAGS = new Set([
|
|
|
201
203
|
process.exit(1);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
|
-
|
|
206
|
+
// `doctor` 承诺"只读":它连运行时脚本同步都不做 —— 否则"自检"会先往用户磁盘写 9 个文件,
|
|
207
|
+
// 与承诺不符(也会把"我什么都没改却多了文件"变成新的困惑)。
|
|
208
|
+
if (process.argv[2] !== "doctor") {
|
|
209
|
+
try { ensureRuntimeCopy(); } catch (e) { console.warn(`⚠️ 运行时固化跳过: ${e.message}`); }
|
|
210
|
+
}
|
|
205
211
|
|
|
206
212
|
/** 自启动服务应指向的 dsh-setup.mjs:优先配置目录内的固化副本,否则当前执行文件。 */
|
|
207
213
|
function runtimeSetupPath() {
|
|
@@ -763,12 +769,32 @@ function restartBridgeService() {
|
|
|
763
769
|
return startBridgeDarwin(plistPath);
|
|
764
770
|
}
|
|
765
771
|
if (process.platform === "linux") {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
+
// 【0.6.15】飞牛 fnOS / 群晖这类 NAS 上**没有 user systemd 会话**(插件跑在系统服务里,
|
|
773
|
+
// 没有 XDG_RUNTIME_DIR)→ `systemctl --user` 只会报 "Failed to connect to bus",
|
|
774
|
+
// 于是这一支原本必然失败、且没有任何兜底。现在:有会话就用 systemd(可自愈),
|
|
775
|
+
// 没有就退到脱离进程(spawnDetachedBridge,与 macOS 的兜底同一条路径)。
|
|
776
|
+
const hasUserSystemd = Boolean(process.env.XDG_RUNTIME_DIR) && sh("systemctl --user show-environment").ok;
|
|
777
|
+
if (hasUserSystemd) {
|
|
778
|
+
const r = sh(`systemctl --user restart dsh-bridge`);
|
|
779
|
+
if (r.ok) {
|
|
780
|
+
const a = sh(`systemctl --user is-active dsh-bridge`);
|
|
781
|
+
if (a.ok && a.stdout.trim() === "active") return { ok: true, status: "running", pid: null };
|
|
782
|
+
return { ok: false, status: "failed", detail: (a.stdout || a.stderr).trim() };
|
|
783
|
+
}
|
|
784
|
+
// 有会话却重启失败:继续往下走脱离进程兜底,绝不留一个"重启失败但没有任何进程"的空档
|
|
785
|
+
}
|
|
786
|
+
const d = spawnDetachedBridge();
|
|
787
|
+
if (d.ok) {
|
|
788
|
+
return {
|
|
789
|
+
ok: true,
|
|
790
|
+
status: "degraded-detached",
|
|
791
|
+
pid: d.pid,
|
|
792
|
+
detail: hasUserSystemd
|
|
793
|
+
? "systemd --user 重启失败,已改为后台进程运行(现在可用;systemd 恢复后下一次重启会回到托管)"
|
|
794
|
+
: "本机没有 user systemd 会话(NAS/容器常见),已改为后台进程运行(现在可用,但不会开机自启)",
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return { ok: false, status: "failed", detail: d.error || "systemd 与后台进程均启动失败" };
|
|
772
798
|
}
|
|
773
799
|
return { ok: false, status: "unsupported", detail: `平台 ${process.platform} 不支持自启动` };
|
|
774
800
|
}
|
|
@@ -779,14 +805,21 @@ function restartBridgeService() {
|
|
|
779
805
|
* 【2026-09-23 用户实测事故】这里原本处处写死 `http://127.0.0.1:3080`:只要用户的 dsh web
|
|
780
806
|
* 不在默认端口(`dsh web --port 8090`、profile 里配了 port、或 `--port 0` 让系统分配空闲端口),
|
|
781
807
|
* watcher 的在线探测就**永远**失败 → bridge 永远不启动,而面板只会说「正在启动 Bridge…」。
|
|
782
|
-
* 优先级:显式环境变量 > 插件半留下的端口文件 > DSH_WEB_URL > 默认 3080。
|
|
783
|
-
*
|
|
784
|
-
*
|
|
808
|
+
* 优先级:显式环境变量 > **运行时动态发现** > 插件半留下的端口文件 > DSH_WEB_URL > 默认 3080。
|
|
809
|
+
*
|
|
810
|
+
* 为什么还要「动态发现」这一层(2026-09-23 第二起实测,DSH Desktop):
|
|
811
|
+
* DSH Desktop(Electron 壳 `dsh-plugin-desktop`)默认把 Web 服务放在 **43120**,被占用还会 +1;
|
|
812
|
+
* 而端口文件要等**插件半**加载后才有。于是「端口文件还没写」或「写的是上一轮的旧端口」时,
|
|
813
|
+
* 光靠文件仍会永久探错端口。发现逻辑(含 DSH 进程实际监听端口 + Desktop 端口区间 + 身份校验)
|
|
814
|
+
* 统一在 clients/dsh-remote/upstream-discovery.mjs,watcher 与 bridge 共用同一份实现。
|
|
785
815
|
*/
|
|
786
816
|
const UPSTREAM_FILE = ".dsh-upstream";
|
|
817
|
+
/** 运行时动态发现到的上游(优于可能过期的端口文件;进程内缓存)。 */
|
|
818
|
+
let discoveredUpstream = "";
|
|
787
819
|
function upstreamUrl() {
|
|
788
820
|
const explicit = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim();
|
|
789
821
|
if (explicit) return explicit.replace(/\/+$/, "");
|
|
822
|
+
if (discoveredUpstream) return discoveredUpstream;
|
|
790
823
|
try {
|
|
791
824
|
const fromFile = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim();
|
|
792
825
|
if (/^https?:\/\/[^\s]+$/i.test(fromFile)) return fromFile.replace(/\/+$/, "");
|
|
@@ -798,28 +831,69 @@ function upstreamUrl() {
|
|
|
798
831
|
if (u.port) return `http://127.0.0.1:${u.port}`; // bridge 的 loopback 围栏只认 127.0.0.1
|
|
799
832
|
} catch { /* 非 URL:忽略 */ }
|
|
800
833
|
}
|
|
801
|
-
return
|
|
834
|
+
return FALLBACK_UPSTREAM;
|
|
802
835
|
}
|
|
803
836
|
/** 上游端口(诊断/日志用;取不到就回退 3080)。 */
|
|
804
837
|
function upstreamPort() {
|
|
805
838
|
try { return Number(new URL(upstreamUrl()).port) || 3080; } catch { return 3080; }
|
|
806
839
|
}
|
|
807
840
|
|
|
841
|
+
/** 发现结果缓存与冷却:避免每轮轮询都去 lsof/pgrep + 逐个端口探测。 */
|
|
842
|
+
let upstreamDiscoveryAt = 0;
|
|
843
|
+
let upstreamDiscoveryInflight = null;
|
|
844
|
+
const UPSTREAM_DISCOVERY_COOLDOWN_MS = 15_000;
|
|
845
|
+
|
|
808
846
|
/**
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* 所以「装完当下 bridge 没在跑」是**正常状态**,不能当失败吓用户(见 install 汇总)。
|
|
847
|
+
* 动态发现上游(带身份校验),成功后写入 `discoveredUpstream` 供全局复用。
|
|
848
|
+
* @param {boolean} force 忽略冷却(探测失败后的重试用)
|
|
812
849
|
*/
|
|
813
|
-
async function
|
|
850
|
+
async function refreshUpstreamDiscovery(force = false) {
|
|
851
|
+
if (upstreamDiscoveryInflight) return upstreamDiscoveryInflight;
|
|
852
|
+
const now = Date.now();
|
|
853
|
+
if (!force && now - upstreamDiscoveryAt < UPSTREAM_DISCOVERY_COOLDOWN_MS) return upstreamUrl();
|
|
854
|
+
upstreamDiscoveryAt = now;
|
|
855
|
+
upstreamDiscoveryInflight = (async () => {
|
|
856
|
+
try {
|
|
857
|
+
const found = await discoverUpstream({ relayDir: CONFIG_DIR, log: (m) => console.log(`[dsh-remote] ${m}`) });
|
|
858
|
+
// 「提示地址验证通过」与「候选命中」都值得记下来;fallback(什么都没找到)不污染现有判断
|
|
859
|
+
if (found.url && found.source !== "fallback") discoveredUpstream = found.url;
|
|
860
|
+
return upstreamUrl();
|
|
861
|
+
} catch {
|
|
862
|
+
return upstreamUrl();
|
|
863
|
+
} finally {
|
|
864
|
+
upstreamDiscoveryInflight = null;
|
|
865
|
+
}
|
|
866
|
+
})();
|
|
867
|
+
return upstreamDiscoveryInflight;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** 上游是否**有响应**(判据刻意宽松:4xx/5xx 也算"端口在听",那种情况问题在别处)。 */
|
|
871
|
+
async function upstreamReachable(url = upstreamUrl(), timeoutMs = 1200) {
|
|
814
872
|
try {
|
|
815
873
|
const ac = new AbortController();
|
|
816
874
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
817
|
-
await fetch(
|
|
875
|
+
await fetch(url + "/", { signal: ac.signal });
|
|
818
876
|
clearTimeout(timer);
|
|
819
877
|
return true;
|
|
820
878
|
} catch { return false; }
|
|
821
879
|
}
|
|
822
880
|
|
|
881
|
+
/**
|
|
882
|
+
* dsh web 是否正在运行(端口以 upstreamUrl() 为准,**绝不写死 3080**)。
|
|
883
|
+
* bridge 本身依赖 dsh web 才工作:dsh web 没开时 bridge 起来也会立刻退出,
|
|
884
|
+
* 所以「装完当下 bridge 没在跑」是**正常状态**,不能当失败吓用户(见 install 汇总)。
|
|
885
|
+
*
|
|
886
|
+
* 探测失败时会**主动做一次动态发现**再试一次 —— 这一条正是 DSH Desktop 场景的关键:
|
|
887
|
+
* 上游在 43120(Desktop 默认,占用还会 +1),而端口文件可能还没写或写着上一轮的旧端口。
|
|
888
|
+
*/
|
|
889
|
+
async function isDshWebUp(timeoutMs = 1200) {
|
|
890
|
+
if (await upstreamReachable(upstreamUrl(), timeoutMs)) return true;
|
|
891
|
+
const before = upstreamUrl();
|
|
892
|
+
const after = await refreshUpstreamDiscovery(true);
|
|
893
|
+
if (after === before) return false;
|
|
894
|
+
return upstreamReachable(after, timeoutMs);
|
|
895
|
+
}
|
|
896
|
+
|
|
823
897
|
/**
|
|
824
898
|
* 解析 dsh web profile 目录(安装与插件管理共用同一口径)。
|
|
825
899
|
*/
|
|
@@ -1125,14 +1199,13 @@ async function runBridge() {
|
|
|
1125
1199
|
/** 限流提示只打一次,避免刷屏。 */
|
|
1126
1200
|
let warnedRateLimit = false;
|
|
1127
1201
|
|
|
1128
|
-
// watcher:检测 dsh web
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
fetch(upstreamUrl() + "/")
|
|
1132
|
-
.then(() => { clearTimeout(t); resolve(true); })
|
|
1133
|
-
.catch(() => { clearTimeout(t); resolve(false); });
|
|
1134
|
-
});
|
|
1202
|
+
// watcher:检测 dsh web 是否存活(端口动态解析,绝不写死 3080),存活才启动 bridge。
|
|
1203
|
+
// 走 isDshWebUp():探测失败会自动做一次动态发现(DSH Desktop 的 43120 / 自定义端口都靠它)。
|
|
1204
|
+
const checkUpstream = () => isDshWebUp(2000);
|
|
1135
1205
|
|
|
1206
|
+
// 先做一次动态发现再打印/派生:否则日志会长期写着错误的端口(用户实测就卡在
|
|
1207
|
+
// 「等待 dsh web(127.0.0.1:3080)启动...」而真实上游在 43120)。
|
|
1208
|
+
await refreshUpstreamDiscovery(true);
|
|
1136
1209
|
console.log(`[dsh-remote] 等待 dsh web(${upstreamUrl()})启动...`);
|
|
1137
1210
|
let bridgeProc = null;
|
|
1138
1211
|
let starting = false;
|
|
@@ -2284,6 +2357,134 @@ async function pluginCmd(argv) {
|
|
|
2284
2357
|
* resolve = dsh 的裸包名解析能不能走通(浏览器半注入面板就靠它)
|
|
2285
2358
|
* client = dsh.client.platform 声明在不在
|
|
2286
2359
|
*/
|
|
2360
|
+
/**
|
|
2361
|
+
* `dsh-remote doctor` —— **一条命令回答「为什么连不上」**(只读,不改任何东西)。
|
|
2362
|
+
*
|
|
2363
|
+
* 【为什么产品上必须有它】2026-09-25 排查两条用户反馈(fb_7b4ee6ec9862 / fb_4cc2c9df749c)时,
|
|
2364
|
+
* 判断"bridge 到底有没有被拉起来"花了大量时间,而且**只能靠翻日志行数估时间** ——
|
|
2365
|
+
* 因为当时的日志连时间戳都没有,用户端也没有任何一个入口能把
|
|
2366
|
+
* 「上游端口 / 守护状态 / 事件订阅 / 待补发队列」一次性说清楚。
|
|
2367
|
+
* 那两个用户都是 linux + 「starting / bridge 未运行」,而我们的自愈当时在 Linux 上**根本没有实现**
|
|
2368
|
+
* (见 0.6.15 的 startBridgeLinux):如果有一份这样的自检,用户自己 10 秒就能看出问题在哪。
|
|
2369
|
+
*
|
|
2370
|
+
* 输出四段:① 身份与版本 ② 上游(含来源与探测结论) ③ 进程与自启动 ④ 通道(微信/加密/待补发)
|
|
2371
|
+
* ⑤ 日志尾部(带时间戳)。任何一项失败都只影响那一行,绝不中断整份自检。
|
|
2372
|
+
*
|
|
2373
|
+
* @param {string[]} argv 额外参数(--profile <dir> 透传给插件挂载诊断)
|
|
2374
|
+
*/
|
|
2375
|
+
async function doctorCmd(argv = []) {
|
|
2376
|
+
const mask = (s) => (s ? String(s).slice(0, 3) + "****" + String(s).slice(-2) : "");
|
|
2377
|
+
const ms = (t) => (t ? new Date(Number(t)).toISOString().replace("T", " ").slice(0, 19) : "-");
|
|
2378
|
+
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; } };
|
|
2379
|
+
const readText = (p) => { try { return fs.readFileSync(p, "utf8"); } catch { return ""; } };
|
|
2380
|
+
const alive = (pid) => {
|
|
2381
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2382
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e && e.code === "EPERM"; }
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
console.log("dsh-remote 自检(doctor,只读,不改任何东西)");
|
|
2386
|
+
console.log("时间:", new Date().toISOString(), "| 平台:", `${process.platform}/${process.arch}`, "| node:", process.version);
|
|
2387
|
+
console.log("配置目录:", CONFIG_DIR);
|
|
2388
|
+
|
|
2389
|
+
// ── ① 插件与运行环境版本 ────────────────────────────────────────────────
|
|
2390
|
+
const pluginPkg = readJson(path.join(CONFIG_DIR, "node_modules", PLUGIN_ID, "package.json"))
|
|
2391
|
+
|| readJson(new URL("./packages/dsh-remote-web/package.json", import.meta.url));
|
|
2392
|
+
const setupVersion = readText(path.join(CONFIG_DIR, ".dsh-setup-version")).trim();
|
|
2393
|
+
console.log("\n① 版本");
|
|
2394
|
+
console.log(` 插件 : ${pluginPkg && pluginPkg.version ? "v" + pluginPkg.version : "(读不到)"}`);
|
|
2395
|
+
console.log(` 运行环境 : ${setupVersion ? "v" + setupVersion : "(未固化 / 未知)"}`);
|
|
2396
|
+
console.log(` 运行环境入口 : ${fs.existsSync(runtimeSetupPath()) ? runtimeSetupPath() : "缺失!"}`);
|
|
2397
|
+
try {
|
|
2398
|
+
const profileDir = resolveProfileDir(argv);
|
|
2399
|
+
if (fs.existsSync(path.join(profileDir, "package.json"))) printPluginDiagnostics(profileDir);
|
|
2400
|
+
else console.log(` [plugin] 未找到 profile(${profileDir})`);
|
|
2401
|
+
} catch (e) { console.log(` [plugin] 诊断失败:${e.message}`); }
|
|
2402
|
+
|
|
2403
|
+
// ── ② 上游 dsh web(bridge 要连的那个本机地址) ──────────────────────────
|
|
2404
|
+
console.log("\n② 上游 dsh web");
|
|
2405
|
+
const hint = resolveUpstreamHint({ relayDir: CONFIG_DIR });
|
|
2406
|
+
const probed = await discoverUpstream({ relayDir: CONFIG_DIR, log: (m) => console.log(` ${m}`) });
|
|
2407
|
+
console.log(` 提示来源 : ${hint.url ? `${hint.url}(${hint.source})` : "(无)"}`);
|
|
2408
|
+
console.log(` 最终采用 : ${probed.url}(${probed.source})`);
|
|
2409
|
+
console.log(` 身份校验 : ${probed.source === "fallback" ? "⚠️ 未找到在听的 dsh web(会一直重试)" : "✅ 已确认为本机 dsh web"}`);
|
|
2410
|
+
if (probed.probed && probed.probed.length) console.log(` 探测过的端口 : ${probed.probed.join(", ")}`);
|
|
2411
|
+
|
|
2412
|
+
// ── ③ 守护与 bridge 进程 ───────────────────────────────────────────────
|
|
2413
|
+
console.log("\n③ 进程与自启动");
|
|
2414
|
+
const watcherPid = readPidFile(WATCHER_PID_FILE);
|
|
2415
|
+
const bridgePid = readPidFile(BRIDGE_PID_FILE);
|
|
2416
|
+
console.log(` watcher pid : ${watcherPid || "-"}${watcherPid ? (alive(watcherPid) ? "(在跑)" : "(已死,pid 文件过期)") : ""}`);
|
|
2417
|
+
console.log(` bridge pid : ${bridgePid || "-"}${bridgePid ? (alive(bridgePid) ? "(在跑)" : "(已死,pid 文件过期)") : ""}`);
|
|
2418
|
+
// 进程扫描(平台各自的方式):只报数量,不猜
|
|
2419
|
+
try {
|
|
2420
|
+
const r = process.platform === "win32"
|
|
2421
|
+
? sh("tasklist /FI \"IMAGENAME eq node.exe\" /FO CSV 2>NUL")
|
|
2422
|
+
: sh("pgrep -fl 'dsh-setup.mjs|dsh-bridge.mjs' 2>/dev/null");
|
|
2423
|
+
const lines = String(r.stdout || "").split("\n").map((l) => l.trim()).filter((l) => l && !/pgrep/.test(l));
|
|
2424
|
+
const watchers = lines.filter((l) => /dsh-setup\.mjs/.test(l));
|
|
2425
|
+
const bridges = lines.filter((l) => /dsh-bridge\.mjs/.test(l)).filter((l) => !/dsh-setup/.test(l));
|
|
2426
|
+
console.log(` 实测进程 : watcher ${watchers.length} 个 / bridge ${bridges.length} 个`);
|
|
2427
|
+
if (watchers.length && !bridges.length) console.log(" ⚠️ 守护在跑、bridge 没起来 —— 这类情况看日志尾部(通常是在等 dsh web 或登录限流)");
|
|
2428
|
+
if (!watchers.length && !bridges.length) {
|
|
2429
|
+
console.log(" ⚠️ 一个进程都没在跑。手动起一次看报错:`dsh-remote run`(前台,能直接看到输出)");
|
|
2430
|
+
}
|
|
2431
|
+
} catch (e) { console.log(` 实测进程 : 扫描失败(${e.message})`); }
|
|
2432
|
+
if (process.platform === "linux") {
|
|
2433
|
+
const hasUser = Boolean(process.env.XDG_RUNTIME_DIR);
|
|
2434
|
+
console.log(` 自启动 : systemd --user ${hasUser ? "(有 runtime dir)" : "(无 XDG_RUNTIME_DIR → 本机没有 user systemd,插件会用后台进程兜底)"}`);
|
|
2435
|
+
if (hasUser) {
|
|
2436
|
+
const r = sh("systemctl --user is-active dsh-bridge");
|
|
2437
|
+
console.log(` systemd 状态 : ${r.ok ? r.stdout.trim() : (r.stderr || "").trim() || "查不到"}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// ── ④ 通道:E2EE / 微信 / 待补发 ───────────────────────────────────────
|
|
2442
|
+
console.log("\n④ 通道");
|
|
2443
|
+
const e2ee = readJson(path.join(CONFIG_DIR, ".e2ee-state.json"));
|
|
2444
|
+
console.log(` 端到端加密 : ${e2ee ? (e2ee.enabled ? `已启用(${e2ee.reason || "ok"})` : `未启用(${e2ee.reason || "?"})`) : "(无状态文件 = 明文)"}`);
|
|
2445
|
+
const wx = readJson(path.join(CONFIG_DIR, ".wechat-state.json"));
|
|
2446
|
+
if (!wx) {
|
|
2447
|
+
console.log(" 微信通道 : 未使用(无状态文件)");
|
|
2448
|
+
} else {
|
|
2449
|
+
const outbox = Array.isArray(wx.pending_outbox) ? wx.pending_outbox.length : 0;
|
|
2450
|
+
const faults = wx.last_fault && wx.last_fault.code ? `${wx.last_fault.code}(${ms(wx.last_fault.at)})` : "无";
|
|
2451
|
+
console.log(` 微信通道 : ${wx.bound ? "已绑定" : "未绑定"} | 最近成功推送 ${ms(wx.last_push_ok_at)}`);
|
|
2452
|
+
console.log(` 待补发通知 : ${outbox ? `⚠️ ${outbox} 条(下次在微信里发一句话就会补上)` : "0 条"}`);
|
|
2453
|
+
console.log(` 最近订阅异常 : ${faults}`);
|
|
2454
|
+
console.log(` 当前会话 : ${wx.current_session_title || "-"}(${wx.current_session_id || "-"})`);
|
|
2455
|
+
console.log(` 最近完成 : ${wx.last_completed_session_title || "-"}(${ms(wx.last_completed_at)})`);
|
|
2456
|
+
if (outbox) console.log(` ℹ️ 入站消息的形状(字段名,用于定位 context_token 缺失):${wx.inbound_shape || "(还没记到)"}`);
|
|
2457
|
+
}
|
|
2458
|
+
const cfg = loadConfig();
|
|
2459
|
+
console.log(` 账号 : ${mask(cfg.phone) || (cfg.local_key ? "自建(本地密钥)" : "(未登录)")}`);
|
|
2460
|
+
console.log(` 中继/API : ${cfg.tunnel_url || "-"} ${cfg.api_url ? `| ${cfg.api_url}` : ""}`);
|
|
2461
|
+
|
|
2462
|
+
// ── ⑤ 日志尾部(现在每行都有时间戳,直接能对时间) ─────────────────────
|
|
2463
|
+
console.log("\n⑤ 日志尾部(bridge + 安装)");
|
|
2464
|
+
for (const name of [".dsh-bridge.log", ".dsh-setup-install.log"]) {
|
|
2465
|
+
const p = path.join(CONFIG_DIR, name);
|
|
2466
|
+
if (!fs.existsSync(p)) { console.log(` ${name}: (无)`); continue; }
|
|
2467
|
+
let tail = "";
|
|
2468
|
+
try {
|
|
2469
|
+
const st = fs.statSync(p);
|
|
2470
|
+
const len = Math.min(st.size, 8 * 1024);
|
|
2471
|
+
const fd = fs.openSync(p, "r");
|
|
2472
|
+
const buf = Buffer.alloc(len);
|
|
2473
|
+
fs.readSync(fd, buf, 0, len, Math.max(0, st.size - len));
|
|
2474
|
+
fs.closeSync(fd);
|
|
2475
|
+
tail = buf.toString("utf8");
|
|
2476
|
+
} catch { /* 读不了就当没有 */ }
|
|
2477
|
+
const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean).slice(-12);
|
|
2478
|
+
console.log(` --- ${name}(末尾 ${lines.length} 行 / ${p})---`);
|
|
2479
|
+
for (const l of lines) console.log(" " + l.slice(0, 220));
|
|
2480
|
+
console.log("");
|
|
2481
|
+
}
|
|
2482
|
+
console.log("结论怎么读:① 版本对不上 → 面板点「一键修复 / 重装运行环境」;");
|
|
2483
|
+
console.log(" ② 上游 fallback → 把 dsh web 起在本机可回环访问的端口(插件会自动发现);");
|
|
2484
|
+
console.log(" ③ 没有任何进程 → 先手动 `dsh-remote run` 看前台输出;");
|
|
2485
|
+
console.log(" ④ 待补发 > 0 → 在微信里随便发一句话即可补收。");
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2287
2488
|
function printPluginDiagnostics(profileDir) {
|
|
2288
2489
|
const nmPlugin = path.join(profileDir, "node_modules", PLUGIN_ID);
|
|
2289
2490
|
const link = describePluginLink(profileDir);
|
|
@@ -2369,6 +2570,7 @@ else if (cmd === "settings") settingsHint();
|
|
|
2369
2570
|
else if (cmd === "run") await runBridge();
|
|
2370
2571
|
else if (cmd === "plugin") await pluginCmd(process.argv.slice(3));
|
|
2371
2572
|
else if (cmd === "repair") repairProfile(process.argv.slice(3));
|
|
2573
|
+
else if (cmd === "doctor") await doctorCmd(process.argv.slice(3));
|
|
2372
2574
|
else if (cmd === "status") {
|
|
2373
2575
|
const cfg = loadConfig();
|
|
2374
2576
|
const local = Boolean(cfg.local_key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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",
|
|
@@ -1949,12 +1949,22 @@ window.__ModuleLoader__.load({
|
|
|
1949
1949
|
"已 " + Math.round(idleShown / 1000) + " 秒没有新输出(总计 " + fmtDuration(elapsedShown) + "):安装过程偶尔会安静一会儿,超过 " + Math.round(cancelAfterMs / 1000) + " 秒没变化会出现「取消并重试」。")
|
|
1950
1950
|
: null,
|
|
1951
1951
|
// ③ 操作区(主操作 / 检查 / 取消并重试 / 彻底卸载)
|
|
1952
|
+
//
|
|
1953
|
+
// ★ 2026-09-25(用户反馈 fb_4cc2c9df749c 原话:「我的端口不是 3080,**没找到一键更新的按钮**」):
|
|
1954
|
+
// 已是最新版时按钮写的是「重新检查 / 修复」—— 用户按字面找「一键更新」,找不到就以为
|
|
1955
|
+
// 面板没有修复入口。现在:按钮直接自称「一键修复 / 重装运行环境」,并在下面补一句
|
|
1956
|
+
// 「已是最新版本」+ 该做什么,让"修不好"的出路不再藏在按钮文案里。
|
|
1957
|
+
!outdated
|
|
1958
|
+
? h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1959
|
+
"已是最新版本 v" + (ver && ver.version ? ver.version : "(未知)")
|
|
1960
|
+
+ "。若手机连不上或后台服务没起来,点「一键修复 / 重装运行环境」会重新补装并拉起后台服务,不需要其他操作。")
|
|
1961
|
+
: null,
|
|
1952
1962
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
1953
1963
|
outdated
|
|
1954
1964
|
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
1955
1965
|
upBusy ? "更新启动中…" : updating ? "正在更新…" : "一键更新到 v" + chk.latest)
|
|
1956
1966
|
: h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
1957
|
-
updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "
|
|
1967
|
+
updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "一键修复 / 重装运行环境"),
|
|
1958
1968
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: chkBusy || updating || upBusy, onClick: doCheck }, chkBusy ? "检查中…" : "检查更新"),
|
|
1959
1969
|
canCancel
|
|
1960
1970
|
? h("button", {
|
|
@@ -4780,6 +4790,26 @@ window.__ModuleLoader__.load({
|
|
|
4780
4790
|
!wx.channel_running
|
|
4781
4791
|
? h("div", { className: "dru-msg dru-msg-warn" }, "提示:后台服务的微信通道当前没有在运行(绑定状态不受影响)。到「📱 远程访问」面板重启后台服务后会自动恢复。")
|
|
4782
4792
|
: null,
|
|
4793
|
+
// ── 事件订阅 / 推送健康(2026-09-25 事故加的)────────────────────────────
|
|
4794
|
+
// 那次「任务跑完一条推送都没收到、日志里也没有任何报错」的两个可用信号:
|
|
4795
|
+
// ① 待补发队列非空 = 平台侧刚才发不出去(现在会补发,但要如实告诉用户);
|
|
4796
|
+
// ② 事件订阅没就绪 / 最近有订阅异常 = 任务通知根本不会来("没收到推送"的另一种成因,
|
|
4797
|
+
// 以前完全没有界面入口,只能翻日志)。
|
|
4798
|
+
// 只在**真有问题**时才显示,正常时不占地方。
|
|
4799
|
+
wx.events_running && wx.events_state && wx.events_state !== "ready"
|
|
4800
|
+
? h("div", { className: "dru-msg dru-msg-warn" },
|
|
4801
|
+
"提示:任务事件订阅未就绪(状态 " + String(wx.events_state) + "),这段时间任务跑完可能不会推给你。稍候会自动重连;持续如此请到「📱 远程访问」重启后台服务。")
|
|
4802
|
+
: null,
|
|
4803
|
+
(Number(wx.pending_outbox) || 0) > 0
|
|
4804
|
+
? h("div", { className: "dru-msg dru-msg-warn" },
|
|
4805
|
+
"有 " + Number(wx.pending_outbox) + " 条通知刚才没能发出去(微信侧发送失败),已排在待补发队列里 —— 你下次在微信里任意发一句话,就会自动补给你。")
|
|
4806
|
+
: null,
|
|
4807
|
+
wx.last_fault && wx.last_fault.code
|
|
4808
|
+
? h("div", { className: "dru-hint" },
|
|
4809
|
+
"最近一次事件订阅异常:" + String(wx.last_fault.code)
|
|
4810
|
+
+ "(" + String(wx.last_fault.message || "").slice(0, 80) + ")"
|
|
4811
|
+
+ (wx.last_fault.at ? " · " + new Date(Number(wx.last_fault.at)).toLocaleString("zh-CN") : ""))
|
|
4812
|
+
: null,
|
|
4783
4813
|
wx.disabled
|
|
4784
4814
|
? h("div", { className: "dru-msg dru-msg-warn" }, "提示:微信通道已按配置关闭(环境变量 DSH_WECHAT=0)。凭据仍在,重新开启后会继续推送。")
|
|
4785
4815
|
: null,
|