@mrrisega/dsh-remote 0.7.6-beta.1 → 0.7.6-beta.10
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 +678 -50
- package/clients/dsh-remote/e2ee-client.mjs +256 -0
- package/clients/dsh-remote/mobile-adapter.mjs +261 -0
- package/clients/dsh-remote/test/bridge-control-priority.test.mjs +214 -0
- package/clients/dsh-remote/test/bridge-heartbeat-adaptive.test.mjs +110 -0
- package/clients/dsh-remote/test/bridge-upstream-refresh.test.mjs +26 -8
- package/clients/dsh-remote/test/bridge-ws-open-fast-ack.test.mjs +213 -0
- package/clients/dsh-remote/test/e2ee-session-store.test.mjs +251 -0
- package/clients/dsh-remote/test/mobile-adapter-single-page.test.mjs +328 -0
- package/clients/dsh-remote/test/pending-plugin-update.test.mjs +96 -0
- package/dsh-setup.mjs +109 -10
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +257 -13
- package/packages/dsh-remote-web/lib/index.js +172 -2
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +678 -50
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +256 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +261 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +109 -10
- package/packages/dsh-remote-web/test/community-invite-proxy.test.mjs +343 -0
- package/packages/dsh-remote-web/test/community-invite-ui.test.mjs +375 -0
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +7 -5
- package/packages/dsh-remote-web/test/purchase-agreement-proxy.test.mjs +198 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +336 -0
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +4 -2
- package/packages/dsh-remote-web/test/setup-output.test.mjs +17 -8
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +2 -2
|
@@ -2817,6 +2817,29 @@ function writeRestartState(relayDir, state) {
|
|
|
2817
2817
|
* 标记「需要用户动一下」的状态。kind = refresh(插件被运行时改写,刷新页面即可)。
|
|
2818
2818
|
* 历史 kind(first-install / update)已废弃:热加载落地后,装插件与在线更新都不再需要重启 harness。
|
|
2819
2819
|
*/
|
|
2820
|
+
/** 桌面端「暫存插件更新」marker(dsh-setup 在应用运行时暂存;bridge 在应用退出后自动安装)。 */
|
|
2821
|
+
function readDesktopUpdatePending(relayDir) {
|
|
2822
|
+
try {
|
|
2823
|
+
const m = JSON.parse(readFileSync(join(relayDir, ".plugin-update-pending.json"), "utf8"));
|
|
2824
|
+
return { version: String(m.version || ""), stagedAt: Number(m.stagedAt) || 0 };
|
|
2825
|
+
} catch {
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/** 更新进程成功收尾后写「待生效」提示:桌面端暂存更新 → 退出应用后自动安装;其它 → 重启 dsh web。 */
|
|
2831
|
+
function markUpdateRestartPending(relayDir) {
|
|
2832
|
+
const staged = readDesktopUpdatePending(relayDir);
|
|
2833
|
+
if (staged) {
|
|
2834
|
+
return markRestartPending(
|
|
2835
|
+
relayDir,
|
|
2836
|
+
"restart-app",
|
|
2837
|
+
`新版本${staged.version ? ` ${staged.version}` : ""} 已下载:退出并重新打开 DeepSeek Harness 后自动生效(无需终端)`
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
return markRestartPending(relayDir, "restart", "插件已更新,重启 dsh web 后生效");
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2820
2843
|
function markRestartPending(relayDir, kind, reason) {
|
|
2821
2844
|
const cur = readRestartState(relayDir);
|
|
2822
2845
|
if (cur.pending && cur.kind === kind) return cur; // 幂等:同一原因不反复重写
|
|
@@ -3417,6 +3440,51 @@ async function communityInfo(relayDir) {
|
|
|
3417
3440
|
return data;
|
|
3418
3441
|
}
|
|
3419
3442
|
|
|
3443
|
+
// ---------- 📄 会员服务购买协议 URL(购买入口的《会员服务购买协议》链接) ----------
|
|
3444
|
+
|
|
3445
|
+
/**
|
|
3446
|
+
* 购买入口必须带《会员服务购买协议》链接(业主要求,2026-10-23 客户端 0.7.5-beta.1)。
|
|
3447
|
+
*
|
|
3448
|
+
* URL 来源优先级:
|
|
3449
|
+
* 1) 企业端 `public-config` 的 `agreement_url`(**新字段**;企业端还没上线时没有 → 忽略);
|
|
3450
|
+
* 2) 固定兜底 `DEFAULT_PURCHASE_AGREEMENT_URL`(正式协议由企业端提供,未就绪也能点开)。
|
|
3451
|
+
*
|
|
3452
|
+
* 安全:只允许 **http(s) 绝对地址**;`javascript:` / `data:` / 协议相对(`//host`)/ 相对路径
|
|
3453
|
+
* 一律回兜底 —— 这个值最终会进 `<a href>`,绝不能吃进可执行协议(与 qr_url 同一套纪律)。
|
|
3454
|
+
*
|
|
3455
|
+
* 软着陆:public-config 不可达 / 非对象 / 字段缺失全部回兜底并 `ok:true`。
|
|
3456
|
+
* 协议链接**永远不该**成为购买流程的故障点(打不开也不影响旁边的「升级 / 续费」按钮)。
|
|
3457
|
+
*/
|
|
3458
|
+
export const DEFAULT_PURCHASE_AGREEMENT_URL = "https://n.risegao.cn:13443/relay-api/api/purchase-agreement";
|
|
3459
|
+
|
|
3460
|
+
/** 从 public-config 解析协议 URL(纯函数,便于单测:优先级 + 安全过滤 + 降级一次看完)。 */
|
|
3461
|
+
export function purchaseAgreementUrlFrom(publicConfig) {
|
|
3462
|
+
const raw = publicConfig && typeof publicConfig === "object"
|
|
3463
|
+
? String(publicConfig.agreement_url == null ? "" : publicConfig.agreement_url).trim()
|
|
3464
|
+
: "";
|
|
3465
|
+
if (/^https?:\/\/[^\s]+$/i.test(raw)) return raw;
|
|
3466
|
+
return DEFAULT_PURCHASE_AGREEMENT_URL;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
/** GET /dsh-remote/purchase-agreement 的实现:读公开配置 → 安全过滤 → 兜底。 */
|
|
3470
|
+
async function purchaseAgreementInfo(relayDir) {
|
|
3471
|
+
let body = null;
|
|
3472
|
+
try {
|
|
3473
|
+
const r = await relayFetch(relayDir, "/api/public-config");
|
|
3474
|
+
body = r && r.ok && r.body && typeof r.body === "object" ? r.body : null;
|
|
3475
|
+
} catch {
|
|
3476
|
+
body = null; // relayFetch 本身不抛;这里只是防御(协议链接不能把面板打挂)
|
|
3477
|
+
}
|
|
3478
|
+
const raw = body && typeof body === "object" ? String(body.agreement_url == null ? "" : body.agreement_url).trim() : "";
|
|
3479
|
+
const url = purchaseAgreementUrlFrom(body);
|
|
3480
|
+
return {
|
|
3481
|
+
ok: true,
|
|
3482
|
+
url,
|
|
3483
|
+
// 便于排障:这个 URL 是服务端配的,还是走兜底(面板不展示,只出现在响应里)
|
|
3484
|
+
source: url !== DEFAULT_PURCHASE_AGREEMENT_URL ? "public_config" : (raw ? "public_config_invalid" : "fallback"),
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3420
3488
|
// ---------- 一次性访问密钥 / 已授权设备代理(E1 企业端新增 auth-key / mobile-sessions) ----------
|
|
3421
3489
|
|
|
3422
3490
|
/** 从 relay 响应里尽量提取人类可读错误信息(兼容 {error:{message}} / {error:".."} / {message} / 纯文本)。 */
|
|
@@ -3703,6 +3771,8 @@ async function composeStatus(relayDir) {
|
|
|
3703
3771
|
// 是否需要重启 DeepSeek harness(首次安装 / 在线更新后必须重启才加载插件本体):
|
|
3704
3772
|
// {pending, kind: first-install|update, reason, at},供面板顶部醒目提示 + 重启按钮。
|
|
3705
3773
|
restart: readRestartState(relayDir),
|
|
3774
|
+
// 桌面端暂存的插件更新(存在 = 新版本已下载,退出应用后由 bridge 自动安装并生效)
|
|
3775
|
+
desktopUpdatePending: readDesktopUpdatePending(relayDir),
|
|
3706
3776
|
// 安装/配对自检(2026-10-17 事故):运行时(clients/)与配对(.dsh-config.json)是否齐全、
|
|
3707
3777
|
// 缺什么、下一步该做什么;配对主配置缺失但备份存在时给出可一键恢复的 backup.action。
|
|
3708
3778
|
// 字段契约见 setupStateOf;connect.setup 是同一份对象(bridge-status 短轮询也能拿到)。
|
|
@@ -3930,6 +4000,84 @@ async function proxyAnnouncements(relayDir, req, res, pathname) {
|
|
|
3930
4000
|
}
|
|
3931
4001
|
}
|
|
3932
4002
|
|
|
4003
|
+
// ---------- 🎉 企微交流群引导代理(实际使用满 2 小时弹一次;2026-10-23 业主需求) ----------
|
|
4004
|
+
/**
|
|
4005
|
+
* 桌面端插件面板的「加入企业微信群」引导数据来自企业端**用户级**接口:
|
|
4006
|
+
* GET /api/community-invite/status → { ok, show, reason, used_seconds, required_seconds, qr_url }
|
|
4007
|
+
* POST /api/community-invite/shown → { ok:true }(幂等)
|
|
4008
|
+
* 浏览器半没有企业端用户 token(它跑在 dsh web 同源页里,只认本机 node 半的路由),
|
|
4009
|
+
* 所以由 node 半用 **device-login token** 代调(企业端 device token 的 sub 就是 user,
|
|
4010
|
+
* 与 /api/me、/api/mobile-sessions 走的是同一条身份链)。
|
|
4011
|
+
*
|
|
4012
|
+
* 三条纪律:
|
|
4013
|
+
* ① **不透传浏览器带来的 Authorization**:这是纯用户级接口,身份只应该是"本机登录账号"。
|
|
4014
|
+
* 面板浏览器手里没有任何企业端用户 token,透传只会让"谁的身份"变得含混
|
|
4015
|
+
* (与 announcements 的 user/install 二选一不同,那个接口两种维度都合法)。
|
|
4016
|
+
* ② **判定与记账全在服务端**:show=false(未满 2h / 已弹过 / 后台没配二维码)原样透传,
|
|
4017
|
+
* 前端绝不自己算时长;`shown` 幂等,展示时与关闭时各调一次是安全超集。
|
|
4018
|
+
* ③ **失败一律软着陆**:未登录/自建(没有企业端账号体系)→ 200 + ok:false + reason
|
|
4019
|
+
* (面板静默、不弹不报错);上游 4xx/5xx → 透传状态码 + 可读 error(排障能看见,
|
|
4020
|
+
* 面板依旧静默);网络不可达 → 502 JSON,绝不抛回 webServer。
|
|
4021
|
+
*/
|
|
4022
|
+
async function proxyCommunityInvite(relayDir, req, res, pathname) {
|
|
4023
|
+
const method = String(req.method || "GET").toUpperCase();
|
|
4024
|
+
const suffix = pathname.replace(/^\/dsh-remote\/community-invite/, "") || "/";
|
|
4025
|
+
const isStatus = method === "GET" && suffix === "/status";
|
|
4026
|
+
const isShown = method === "POST" && suffix === "/shown";
|
|
4027
|
+
if (!isStatus && !isShown) {
|
|
4028
|
+
return sendJson(res, 404, { ok: false, show: false, reason: "unknown_route", error: "unknown community-invite route" });
|
|
4029
|
+
}
|
|
4030
|
+
const cfg = loadConfig(relayDir);
|
|
4031
|
+
// 自建部署:没有企业端账号体系,交流群引导不适用 —— 不发请求、也不让面板弹
|
|
4032
|
+
if (cfg.local_key) return sendJson(res, 200, { ok: false, show: false, reason: "local_mode" });
|
|
4033
|
+
// 没配账号:面板登录前也可能误调 —— 静默拒绝(前端本来就只在 loggedIn 后调)
|
|
4034
|
+
if (!(cfg.phone && cfg.password)) return sendJson(res, 200, { ok: false, show: false, reason: "not_logged_in" });
|
|
4035
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
4036
|
+
if (!token) return sendJson(res, 200, { ok: false, show: false, reason: "not_logged_in" });
|
|
4037
|
+
|
|
4038
|
+
const upstreamPath = isShown ? "/api/community-invite/shown" : "/api/community-invite/status";
|
|
4039
|
+
const init = { method: isShown ? "POST" : "GET", headers: { authorization: `Bearer ${token}` } };
|
|
4040
|
+
if (isShown) {
|
|
4041
|
+
init.headers["content-type"] = "application/json";
|
|
4042
|
+
init.body = "{}";
|
|
4043
|
+
}
|
|
4044
|
+
const r = await relayFetch(relayDir, upstreamPath, init);
|
|
4045
|
+
const d = flattenRelayBody(r);
|
|
4046
|
+
if (!r.ok) {
|
|
4047
|
+
// 透传状态码 + 可读错误:面板仍然静默(它只看 ok/show),但排障时能直接看到 401/404/500
|
|
4048
|
+
return sendJson(res, (r && r.status) || 502, {
|
|
4049
|
+
ok: false,
|
|
4050
|
+
show: false,
|
|
4051
|
+
reason: isShown ? "report_failed" : "status_failed",
|
|
4052
|
+
error: relayErrorMessage(r),
|
|
4053
|
+
relayStatus: (r && r.status) || 0
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
if (isShown) {
|
|
4057
|
+
// 幂等上报:不关心上游回什么(只认 ok:false 才报失败),面板两个按钮都会调
|
|
4058
|
+
return sendJson(res, 200, { ok: d.ok !== false, relayStatus: r.status || 200 });
|
|
4059
|
+
}
|
|
4060
|
+
// status:只挑面板需要的字段(不整包透传,避免把内部字段带进浏览器;字段容错见契约)
|
|
4061
|
+
// qr_url 归一化:企业端后台配的可能是站内相对路径(如 /qr/group-qr.png),
|
|
4062
|
+
// 与 /dsh-remote/community 同一约定拼成**企业端绝对地址**再下发 —— 面板跑在 dsh web 源上,
|
|
4063
|
+
// 相对路径会被浏览器解析到 127.0.0.1,图片必然挂。其它协议(data:/javascript:/协议相对)一律丢弃,
|
|
4064
|
+
// 前端按"二维码不可用"降级(保留文案与按钮)。
|
|
4065
|
+
const apiBase = String(cfg.api_url || DEFAULT_API).replace(/\/+$/, "");
|
|
4066
|
+
const rawQr = typeof d.qr_url === "string" ? d.qr_url.trim() : "";
|
|
4067
|
+
let qrUrl = "";
|
|
4068
|
+
if (/^https?:\/\//i.test(rawQr)) qrUrl = rawQr;
|
|
4069
|
+
else if (rawQr.charAt(0) === "/" && rawQr.charAt(1) !== "/") qrUrl = apiBase + rawQr;
|
|
4070
|
+
return sendJson(res, 200, {
|
|
4071
|
+
ok: d.ok !== false,
|
|
4072
|
+
show: d.show === true, // fail closed:非严格 true 一律不弹
|
|
4073
|
+
reason: typeof d.reason === "string" ? d.reason : "",
|
|
4074
|
+
used_seconds: Number(d.used_seconds) || 0,
|
|
4075
|
+
required_seconds: Number(d.required_seconds) || 0,
|
|
4076
|
+
qr_url: qrUrl,
|
|
4077
|
+
relayStatus: r.status || 200
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
|
|
3933
4081
|
// ---------- 自管理:版本 / 在线更新 / 彻底卸载(面板内“版本与更新”卡片) ----------
|
|
3934
4082
|
|
|
3935
4083
|
/** 插件 id / 包名(2026-09 由 dsh-remote-ui 更名)。 */
|
|
@@ -4463,7 +4611,7 @@ async function runOnlineUpdate(relayDir) {
|
|
|
4463
4611
|
updateFailures.delete(String(relayDir));
|
|
4464
4612
|
sweepIfSucceeded(); // C8:装完清残留(按更新前的 pid 点名 + 命令行复核)
|
|
4465
4613
|
// 更新成功 → 落一条「必须重启 dsh web」的强提醒(见 run() 成功分支的同款注释)
|
|
4466
|
-
|
|
4614
|
+
markUpdateRestartPending(relayDir);
|
|
4467
4615
|
}
|
|
4468
4616
|
});
|
|
4469
4617
|
child.unref();
|
|
@@ -4553,7 +4701,7 @@ async function runOnlineUpdate(relayDir) {
|
|
|
4553
4701
|
// 客户端按 kind 一一对应地换文案与主按钮(见 client.js 的 restartAlert),
|
|
4554
4702
|
// 混用一个 kind 就会再次出现"文案说刷新、状态其实是重启"。
|
|
4555
4703
|
// settleRestartState() 靠 bootId 自动结清 —— 用户真的重启后这条提示自己消失,不需要额外清理。
|
|
4556
|
-
|
|
4704
|
+
markUpdateRestartPending(relayDir);
|
|
4557
4705
|
}
|
|
4558
4706
|
});
|
|
4559
4707
|
child.unref();
|
|
@@ -7285,6 +7433,15 @@ function registerRoutes(ctx, relayDir) {
|
|
|
7285
7433
|
sendJson(res, 200, { ok: true, ...(await communityInfo(relayDir)) });
|
|
7286
7434
|
},
|
|
7287
7435
|
},
|
|
7436
|
+
// 📄 会员服务购买协议 URL(购买入口的《会员服务购买协议》链接;public-config.agreement_url → 兜底)
|
|
7437
|
+
// 永远 200 ok:true + 一个可点 URL:协议链接不得成为购买流程的故障点。
|
|
7438
|
+
{
|
|
7439
|
+
method: "GET",
|
|
7440
|
+
path: "/dsh-remote/purchase-agreement",
|
|
7441
|
+
handler: async (_req, res) => {
|
|
7442
|
+
sendJson(res, 200, await purchaseAgreementInfo(relayDir));
|
|
7443
|
+
},
|
|
7444
|
+
},
|
|
7288
7445
|
// 一次性访问密钥(📱 远程访问卡):GET 即创建新 key,企业端 POST /api/auth-key(Bearer)
|
|
7289
7446
|
{
|
|
7290
7447
|
method: "GET",
|
|
@@ -7654,6 +7811,19 @@ function registerRoutes(ctx, relayDir) {
|
|
|
7654
7811
|
await proxyAnnouncements(relayDir, req, res, url.pathname);
|
|
7655
7812
|
},
|
|
7656
7813
|
},
|
|
7814
|
+
// 🎉 企微交流群引导(桌面端面板使用满 2h 后弹一次;与手机 Web 共用同一套企业端接口):
|
|
7815
|
+
// GET /dsh-remote/community-invite/status → 上游 GET {api}/api/community-invite/status
|
|
7816
|
+
// POST /dsh-remote/community-invite/shown → 上游 POST {api}/api/community-invite/shown(幂等)
|
|
7817
|
+
// 与 feedback/announcements 同一形态(prefix + handler 内判别),以后加子接口不用动路由表。
|
|
7818
|
+
{
|
|
7819
|
+
method: "ALL",
|
|
7820
|
+
path: "/dsh-remote/community-invite",
|
|
7821
|
+
prefix: true,
|
|
7822
|
+
handler: async (req, res) => {
|
|
7823
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
7824
|
+
await proxyCommunityInvite(relayDir, req, res, url.pathname);
|
|
7825
|
+
},
|
|
7826
|
+
},
|
|
7657
7827
|
];
|
|
7658
7828
|
|
|
7659
7829
|
const disposers = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.7.6-beta.
|
|
3
|
+
"version": "0.7.6-beta.10",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|