dsh-remote-web 0.5.0 → 0.6.0
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/lib/client.js +930 -119
- package/lib/index.js +285 -20
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// dsh-remote-web — node half (host plugin)(2026-09 由 dsh-remote-web
|
|
1
|
+
// dsh-remote-web — node half (host plugin)(2026-09 由 dsh-remote-ui 更名 dsh-remote-web;卸载/清理兼容旧名)
|
|
2
2
|
//
|
|
3
|
-
// 提供 /dsh-remote/* 同源 HTTP
|
|
4
|
-
// -
|
|
3
|
+
// 提供 /dsh-remote/* 同源 HTTP 路由,供浏览器半的「远程访问」设置面板调用:
|
|
4
|
+
// - 读写配置目录下 .dsh-config.json(0600)
|
|
5
5
|
// - 查询/启停 bridge(launchctl,plist 缺失时自动生成,逻辑与 dsh-setup.mjs 一致)
|
|
6
|
-
// - 代理 relay API(captcha / register / login / public-config
|
|
6
|
+
// - 代理 relay API(captcha / register / login / public-config),直连、不走系统代理;
|
|
7
|
+
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke、
|
|
8
|
+
// DELETE …/:id、POST …/purge,Bearer)供面板「📱 远程访问」卡使用
|
|
7
9
|
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
8
|
-
// 面板内即官方管理入口;更新=后台 npx
|
|
10
|
+
// 面板内即官方管理入口;更新=后台 npx 按 dist-tag(默认 latest,DSH_UPDATE_TAG 可切 beta/alpha)(幂等补齐运行环境并重启 bridge);
|
|
9
11
|
// 彻底卸载=profile 插件清理(uninstallSelf)+ 运行时清理(uninstallRuntime:停 bridge 自启动 /
|
|
10
12
|
// 删 plist|unit / 杀残留进程 / 清空配置目录 ~/.dsh-remote),0.4.7 起回归真正「未安装」状态
|
|
11
13
|
// - 运行时自愈:缺运行环境自动后台安装、登录后自动拉起 bridge(0.4.2 起)
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, constants as fsConstants } from "node:fs";
|
|
16
18
|
import { join, dirname, sep } from "node:path";
|
|
17
19
|
import { execSync, spawn } from "node:child_process";
|
|
18
|
-
import { homedir,
|
|
20
|
+
import { homedir, platform } from "node:os";
|
|
19
21
|
import { fileURLToPath } from "node:url";
|
|
20
22
|
|
|
21
23
|
/** 本插件在 host 侧的服务依赖。 */
|
|
@@ -378,7 +380,7 @@ function ensureRuntime(relayDir) {
|
|
|
378
380
|
try {
|
|
379
381
|
mkdirSync(relayDir, { recursive: true });
|
|
380
382
|
const log = join(relayDir, AUTO_INSTALL_LOG);
|
|
381
|
-
const child = spawn(npxCommand(), ["--yes",
|
|
383
|
+
const child = spawn(npxCommand(), ["--yes", UPDATE_SPEC], {
|
|
382
384
|
detached: true,
|
|
383
385
|
env: spawnEnv({ npm_config_registry: "https://registry.npmjs.org" }),
|
|
384
386
|
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")]
|
|
@@ -651,7 +653,7 @@ async function relayAccount(relayDir) {
|
|
|
651
653
|
if (!r.ok || !r.body || !r.body.user) return null;
|
|
652
654
|
const u = r.body.user;
|
|
653
655
|
return {
|
|
654
|
-
phone: u.phone || "",
|
|
656
|
+
phone: maskPhone(u.phone || ""),
|
|
655
657
|
plan: u.plan || "free",
|
|
656
658
|
plan_source: u.plan_source || "plan",
|
|
657
659
|
plan_ends_at: u.plan_ends_at ?? null,
|
|
@@ -698,8 +700,190 @@ async function relayInviteRecords(relayDir) {
|
|
|
698
700
|
return { records: r.body.records || [], rewards: r.body.rewards || [] };
|
|
699
701
|
}
|
|
700
702
|
|
|
703
|
+
// ---------- 一次性访问密钥 / 已授权设备代理(E1 企业端新增 auth-key / mobile-sessions) ----------
|
|
704
|
+
|
|
705
|
+
/** 从 relay 响应里尽量提取人类可读错误信息(兼容 {error:{message}} / {error:".."} / {message} / 纯文本)。 */
|
|
706
|
+
function relayErrorMessage(r) {
|
|
707
|
+
const b = r && typeof r === "object" ? r.body : null;
|
|
708
|
+
if (b && typeof b === "object") {
|
|
709
|
+
if (typeof b.error === "string" && b.error) return b.error;
|
|
710
|
+
if (b.error && typeof b.error === "object") {
|
|
711
|
+
if (typeof b.error.message === "string" && b.error.message) return b.error.message;
|
|
712
|
+
}
|
|
713
|
+
if (typeof b.message === "string" && b.message) return b.message;
|
|
714
|
+
}
|
|
715
|
+
if (typeof b === "string" && b.trim()) return b.trim();
|
|
716
|
+
const st = r && r.status;
|
|
717
|
+
return st ? `企业端请求失败(HTTP ${st})` : "企业端不可达,请稍后重试";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** 未登录(无账号/自建密钥)时的统一返回文案。 */
|
|
721
|
+
function notLoggedInJson() {
|
|
722
|
+
return { ok: false, error: "尚未登录:请先在「账号」卡片登录手机号账号(或切换到自建服务)后重试", hint: "login_required" };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* 透传企业端响应体:契约字段可能在顶层或 data 子对象里(容错)。
|
|
727
|
+
* 返回扁平对象;数组字段只取首层数组。
|
|
728
|
+
*/
|
|
729
|
+
function flattenRelayBody(r) {
|
|
730
|
+
const b = r && typeof r === "object" && r.body && typeof r.body === "object" ? r.body : {};
|
|
731
|
+
const d = b.data && typeof b.data === "object" ? { ...b.data, ...b } : b;
|
|
732
|
+
return d;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* GET /dsh-remote/access-key → 创建一次性访问密钥(企业端 POST /api/auth-key,Bearer device-login token)。
|
|
737
|
+
* 契约容错:url 必须可用;qr_data_url 取不到时返回 null(UI 只展示链接并说明“二维码暂不可用”,不报错)。
|
|
738
|
+
*/
|
|
739
|
+
async function proxyCreateAccessKey(relayDir, res) {
|
|
740
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
741
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
742
|
+
const r = await relayFetch(relayDir, "/api/auth-key", {
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
745
|
+
body: JSON.stringify({}),
|
|
746
|
+
});
|
|
747
|
+
const d = flattenRelayBody(r);
|
|
748
|
+
const url = typeof d.url === "string" ? d.url.trim() : "";
|
|
749
|
+
if (!r.ok || !url) {
|
|
750
|
+
// url 不可用是硬失败(企业端契约缺陷);错误码/状态透传给 UI 展示
|
|
751
|
+
const err = r.ok && !url ? "企业端未返回可用的访问地址(缺 url)" : relayErrorMessage(r);
|
|
752
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: err, relayStatus: (r && r.status) || 0 });
|
|
753
|
+
}
|
|
754
|
+
return sendJson(res, 200, {
|
|
755
|
+
ok: true,
|
|
756
|
+
url,
|
|
757
|
+
key: d.key ?? null,
|
|
758
|
+
expires_at: d.expires_at ?? null,
|
|
759
|
+
ttl_ms: d.ttl_ms ?? null,
|
|
760
|
+
qr_data_url: d.qr_data_url ?? null,
|
|
761
|
+
relayStatus: (r && r.status) || 200,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* GET /dsh-remote/mobile-sessions → 已授权设备列表(企业端 GET /api/mobile-sessions,Bearer)。
|
|
767
|
+
*/
|
|
768
|
+
async function proxyMobileSessions(relayDir, res) {
|
|
769
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
770
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
771
|
+
const r = await relayFetch(relayDir, "/api/mobile-sessions", {
|
|
772
|
+
method: "GET",
|
|
773
|
+
headers: { authorization: `Bearer ${token}` },
|
|
774
|
+
});
|
|
775
|
+
const d = flattenRelayBody(r);
|
|
776
|
+
const sessions = Array.isArray(d.sessions) ? d.sessions : [];
|
|
777
|
+
if (!r.ok) {
|
|
778
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0, sessions });
|
|
779
|
+
}
|
|
780
|
+
return sendJson(res, 200, { ok: true, sessions, relayStatus: (r && r.status) || 200 });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* POST /dsh-remote/mobile-sessions/revoke(body {id})→ 取消配对(企业端 POST /api/mobile-sessions/:id/revoke,Bearer)。
|
|
785
|
+
*/
|
|
786
|
+
async function proxyRevokeMobileSession(relayDir, req, res) {
|
|
787
|
+
const body = await readJsonBody(req);
|
|
788
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
789
|
+
const id = String(body.id ?? "").trim();
|
|
790
|
+
if (!id) return sendJson(res, 400, { ok: false, error: "缺少参数 id(会话 ID)" });
|
|
791
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
792
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
793
|
+
const r = await relayFetch(relayDir, `/api/mobile-sessions/${encodeURIComponent(id)}/revoke`, {
|
|
794
|
+
method: "POST",
|
|
795
|
+
headers: { authorization: `Bearer ${token}` },
|
|
796
|
+
});
|
|
797
|
+
const d = flattenRelayBody(r);
|
|
798
|
+
// 兼容两种成功形态:HTTP ok,或 body.ok === true(允许企业端 200 + ok:false 表示业务失败)
|
|
799
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
800
|
+
if (!ok) {
|
|
801
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
802
|
+
}
|
|
803
|
+
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* DELETE /dsh-remote/mobile-sessions/delete(body {id})→ 删除本机该设备的授权记录
|
|
808
|
+
* (企业端 DELETE /api/mobile-sessions/:id,Bearer:本人整行删除并拉黑 jti)。
|
|
809
|
+
*/
|
|
810
|
+
async function proxyDeleteMobileSession(relayDir, req, res) {
|
|
811
|
+
const body = await readJsonBody(req);
|
|
812
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
813
|
+
const id = String(body.id ?? "").trim();
|
|
814
|
+
if (!id) return sendJson(res, 400, { ok: false, error: "缺少参数 id(会话 ID)" });
|
|
815
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
816
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
817
|
+
const r = await relayFetch(relayDir, `/api/mobile-sessions/${encodeURIComponent(id)}`, {
|
|
818
|
+
method: "DELETE",
|
|
819
|
+
headers: { authorization: `Bearer ${token}` },
|
|
820
|
+
});
|
|
821
|
+
const d = flattenRelayBody(r);
|
|
822
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
823
|
+
if (!ok) {
|
|
824
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
825
|
+
}
|
|
826
|
+
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* POST /dsh-remote/mobile-sessions/purge → 清理本人所有已解绑(revoked)记录
|
|
831
|
+
* (企业端 POST /api/mobile-sessions/purge,Bearer)。
|
|
832
|
+
*/
|
|
833
|
+
async function proxyPurgeMobileSessions(relayDir, res) {
|
|
834
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
835
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
836
|
+
const r = await relayFetch(relayDir, "/api/mobile-sessions/purge", {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: { authorization: `Bearer ${token}` },
|
|
839
|
+
});
|
|
840
|
+
const d = flattenRelayBody(r);
|
|
841
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
842
|
+
if (!ok) {
|
|
843
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
844
|
+
}
|
|
845
|
+
const removed = Number.isInteger(d.removed) ? d.removed : null;
|
|
846
|
+
const payload = { ok: true, relayStatus: (r && r.status) || 200 };
|
|
847
|
+
if (removed !== null) payload.removed = removed;
|
|
848
|
+
return sendJson(res, 200, payload);
|
|
849
|
+
}
|
|
850
|
+
|
|
701
851
|
// ---------- 综合状态 ----------
|
|
702
852
|
|
|
853
|
+
/**
|
|
854
|
+
* 读取 bridge 写入的 E2EE 开关状态(<relayDir>/.e2ee-state.json,写入方见
|
|
855
|
+
* clients/dsh-remote/e2ee-client.mjs 的 writeE2eeStateFile:{enabled, reason, profile, epoch, caps, at})。
|
|
856
|
+
* 归一化后只透出固定展示字段(enabled/reason/profile/epoch/caps),at 等调试字段不外泄;
|
|
857
|
+
* 文件缺失/损坏一律视为「未启用(明文回退)」——面板据此显示普通安全连接。
|
|
858
|
+
*/
|
|
859
|
+
export function readE2eeStateFile(relayDir) {
|
|
860
|
+
const none = () => ({ enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] });
|
|
861
|
+
try {
|
|
862
|
+
const f = join(relayDir, ".e2ee-state.json");
|
|
863
|
+
if (!existsSync(f)) return none();
|
|
864
|
+
const s = JSON.parse(readFileSync(f, "utf8"));
|
|
865
|
+
if (!s || typeof s !== "object") return none();
|
|
866
|
+
return {
|
|
867
|
+
enabled: s.enabled === true,
|
|
868
|
+
reason: typeof s.reason === "string" && s.reason ? s.reason : "",
|
|
869
|
+
profile: typeof s.profile === "string" ? s.profile : "",
|
|
870
|
+
epoch: Number.isInteger(s.epoch) && s.epoch >= 0 ? s.epoch : 0,
|
|
871
|
+
caps: Array.isArray(s.caps) ? s.caps.filter((c) => typeof c === "string") : [],
|
|
872
|
+
};
|
|
873
|
+
} catch {
|
|
874
|
+
return none();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* 手机号脱敏(隐私审计 2026-09):面板/镜像 UI 一律不下发明文手机号。
|
|
880
|
+
* 明文只在 bridge 本机 config / 服务端账号体系内流转,浏览器侧仅见掩码。
|
|
881
|
+
*/
|
|
882
|
+
function maskPhone(p) {
|
|
883
|
+
const s = String(p || "");
|
|
884
|
+
return s.length >= 7 ? s.slice(0, 3) + "****" + s.slice(-4) : (s ? s.slice(0, 1) + "****" : "");
|
|
885
|
+
}
|
|
886
|
+
|
|
703
887
|
async function composeStatus(relayDir) {
|
|
704
888
|
const cfg = loadConfig(relayDir);
|
|
705
889
|
const launchd = launchdStatus();
|
|
@@ -718,7 +902,8 @@ async function composeStatus(relayDir) {
|
|
|
718
902
|
return {
|
|
719
903
|
ok: true,
|
|
720
904
|
config: {
|
|
721
|
-
phone: cfg.phone || "",
|
|
905
|
+
phone: maskPhone(cfg.phone || ""),
|
|
906
|
+
hasPhone: Boolean(cfg.phone),
|
|
722
907
|
hasPassword: Boolean(cfg.password),
|
|
723
908
|
deviceId: cfg.device_id || "",
|
|
724
909
|
apiUrl,
|
|
@@ -734,8 +919,11 @@ async function composeStatus(relayDir) {
|
|
|
734
919
|
manual,
|
|
735
920
|
running: launchd.running || manual.bridge.length > 0,
|
|
736
921
|
bindError,
|
|
922
|
+
// E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
|
|
923
|
+
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
924
|
+
e2ee: readE2eeStateFile(relayDir),
|
|
737
925
|
},
|
|
738
|
-
|
|
926
|
+
// 隐私审计(2026-09):不再下发真实 hostname(移除 host 字段)——设备标识统一走 deviceId/服务端登记名
|
|
739
927
|
};
|
|
740
928
|
}
|
|
741
929
|
|
|
@@ -842,27 +1030,35 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
842
1030
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
843
1031
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
844
1032
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
845
|
-
const PLUGIN_VERSION = "0.
|
|
1033
|
+
const PLUGIN_VERSION = "0.6.0";
|
|
846
1034
|
const UPDATE_LOG = ".dsh-update.log";
|
|
847
1035
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
848
1036
|
|
|
849
|
-
/**
|
|
1037
|
+
/**
|
|
1038
|
+
* 更新通道(发布策略):普通用户只拉稳定 dist-tag `latest`;预发(alpha/beta)由作者/内测
|
|
1039
|
+
* 通过 `DSH_UPDATE_TAG=beta`(或显式版本号)拉取。迭代一律先发 beta/alpha,稳定后才升 latest。
|
|
1040
|
+
*/
|
|
1041
|
+
const UPDATE_TAG = (process.env.DSH_UPDATE_TAG || "latest").replace(/^@/, "");
|
|
1042
|
+
const UPDATE_SPEC = `@mrrisega/dsh-remote@${UPDATE_TAG}`;
|
|
1043
|
+
|
|
1044
|
+
/** 查询所选通道(npm dist-tag)最新版(官方源优先,失败回退 npmmirror;纯服务端无 CORS 限制)。 */
|
|
850
1045
|
async function npmLatestVersion() {
|
|
851
1046
|
for (const reg of ["https://registry.npmjs.org/@mrrisega/dsh-remote", "https://registry.npmmirror.com/@mrrisega/dsh-remote"]) {
|
|
852
1047
|
try {
|
|
853
1048
|
const res = await fetch(reg, { signal: AbortSignal.timeout(8000) });
|
|
854
1049
|
if (!res.ok) continue;
|
|
855
1050
|
const j = await res.json();
|
|
856
|
-
|
|
1051
|
+
const tags = j && j["dist-tags"] ? j["dist-tags"] : {};
|
|
1052
|
+
if (typeof tags[UPDATE_TAG] === "string") return tags[UPDATE_TAG];
|
|
857
1053
|
} catch { /* 试下一个源 */ }
|
|
858
1054
|
}
|
|
859
1055
|
return "";
|
|
860
1056
|
}
|
|
861
1057
|
|
|
862
|
-
/** 以 detached 子进程执行 `npx --yes
|
|
1058
|
+
/** 以 detached 子进程执行 `npx --yes <UPDATE_SPEC>`(env 可覆盖 npm 源/更新通道)。 */
|
|
863
1059
|
function spawnUpdater(relayDir, extraEnv) {
|
|
864
1060
|
const log = join(relayDir, UPDATE_LOG);
|
|
865
|
-
return spawn(npxCommand(), ["--yes",
|
|
1061
|
+
return spawn(npxCommand(), ["--yes", UPDATE_SPEC], {
|
|
866
1062
|
detached: true,
|
|
867
1063
|
cwd: homedir(),
|
|
868
1064
|
env: spawnEnv(extraEnv), // PATH 补 node 目录:App 最小 PATH 下也能跑 npx
|
|
@@ -871,7 +1067,7 @@ function spawnUpdater(relayDir, extraEnv) {
|
|
|
871
1067
|
}
|
|
872
1068
|
|
|
873
1069
|
/**
|
|
874
|
-
* 后台执行在线一键更新:npx
|
|
1070
|
+
* 后台执行在线一键更新:npx 按 dist-tag(默认 latest)(幂等自愈:补运行环境/更新 bridge/收敛 include)。
|
|
875
1071
|
* 稳健性:
|
|
876
1072
|
* - npx 用绝对路径 + PATH 补全解析(App 拉起的 dsh web PATH 最小化时不再 ENOENT 静默失败);
|
|
877
1073
|
* - 【官方源优先】镜像(npmmirror)滞后时会把旧版(如 0.4.4)当成最新安装,旧 pluginCmd 会把
|
|
@@ -884,7 +1080,7 @@ function runOnlineUpdate(relayDir) {
|
|
|
884
1080
|
mkdirSync(relayDir, { recursive: true });
|
|
885
1081
|
const marker = join(relayDir, UPDATE_MARKER);
|
|
886
1082
|
if (existsSync(marker)) return { ok: false, detail: "已有更新在进行中,请稍候" };
|
|
887
|
-
appendLogLine(relayDir, UPDATE_LOG, `[update] 开始在线更新
|
|
1083
|
+
appendLogLine(relayDir, UPDATE_LOG, `[update] 开始在线更新 ${UPDATE_SPEC} (${new Date().toISOString()})`);
|
|
888
1084
|
|
|
889
1085
|
let retried = false;
|
|
890
1086
|
const clear = () => { try { rmSync(marker, { force: true }); } catch { /* ignore */ } };
|
|
@@ -1030,7 +1226,7 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1030
1226
|
if (rt.removedPlist) bits.push("自启动项已删除");
|
|
1031
1227
|
if (rt.killedPids.length) bits.push(`已结束 ${rt.killedPids.length} 个残留进程`);
|
|
1032
1228
|
if (rt.removedDir) bits.push("配置目录已清空(账号/密钥/固化运行时等)");
|
|
1033
|
-
bits.push("请重启 dsh web
|
|
1229
|
+
bits.push("请重启 dsh web 后完全卸载生效(本插件与「远程访问」面板将消失);如需再次使用,在插件市场重新安装即可。");
|
|
1034
1230
|
sendJson(res, 200, {
|
|
1035
1231
|
ok: true,
|
|
1036
1232
|
...prof, // removedPatch / removedDep / removedBundle / removedDir(profile 插件目录)
|
|
@@ -1086,6 +1282,46 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1086
1282
|
});
|
|
1087
1283
|
},
|
|
1088
1284
|
},
|
|
1285
|
+
// 一次性访问密钥(📱 远程访问卡):GET 即创建新 key,企业端 POST /api/auth-key(Bearer)
|
|
1286
|
+
{
|
|
1287
|
+
method: "GET",
|
|
1288
|
+
path: "/dsh-remote/access-key",
|
|
1289
|
+
handler: async (_req, res) => {
|
|
1290
|
+
await proxyCreateAccessKey(relayDir, res);
|
|
1291
|
+
},
|
|
1292
|
+
},
|
|
1293
|
+
// 已授权设备列表(企业端 POST /api/mobile-sessions,Bearer)
|
|
1294
|
+
{
|
|
1295
|
+
method: "GET",
|
|
1296
|
+
path: "/dsh-remote/mobile-sessions",
|
|
1297
|
+
handler: async (_req, res) => {
|
|
1298
|
+
await proxyMobileSessions(relayDir, res);
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
// 取消已授权设备配对(企业端 POST /api/mobile-sessions/:id/revoke,Bearer)
|
|
1302
|
+
{
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
path: "/dsh-remote/mobile-sessions/revoke",
|
|
1305
|
+
handler: async (req, res) => {
|
|
1306
|
+
await proxyRevokeMobileSession(relayDir, req, res);
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
// 删除已授权设备记录(整行删除并拉黑 jti;企业端 DELETE /api/mobile-sessions/:id,Bearer)
|
|
1310
|
+
{
|
|
1311
|
+
method: "DELETE",
|
|
1312
|
+
path: "/dsh-remote/mobile-sessions/delete",
|
|
1313
|
+
handler: async (req, res) => {
|
|
1314
|
+
await proxyDeleteMobileSession(relayDir, req, res);
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
// 清理本人全部已解绑(revoked)记录(企业端 POST /api/mobile-sessions/purge,Bearer)
|
|
1318
|
+
{
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
path: "/dsh-remote/mobile-sessions/purge",
|
|
1321
|
+
handler: async (_req, res) => {
|
|
1322
|
+
await proxyPurgeMobileSessions(relayDir, res);
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1089
1325
|
{
|
|
1090
1326
|
method: "POST",
|
|
1091
1327
|
path: "/dsh-remote/config",
|
|
@@ -1156,7 +1392,11 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1156
1392
|
handler: async (req, res) => {
|
|
1157
1393
|
const body = await readJsonBody(req);
|
|
1158
1394
|
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1159
|
-
|
|
1395
|
+
let phone = String(body.phone ?? "").trim();
|
|
1396
|
+
if (!phone) {
|
|
1397
|
+
// 隐私审计(2026-09):账号卡修改密码不再下发明文手机号——服务端以本机账号为准
|
|
1398
|
+
phone = String((loadConfig(relayDir).phone) || "").trim();
|
|
1399
|
+
}
|
|
1160
1400
|
if (!phone) return sendJson(res, 400, { ok: false, error: "手机号必填" });
|
|
1161
1401
|
const r = await relayFetch(relayDir, "/api/sms-code", {
|
|
1162
1402
|
method: "POST", headers: { "content-type": "application/json" },
|
|
@@ -1165,6 +1405,31 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1165
1405
|
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1166
1406
|
},
|
|
1167
1407
|
},
|
|
1408
|
+
{
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
path: "/dsh-remote/password/reset",
|
|
1411
|
+
handler: async (req, res) => {
|
|
1412
|
+
// 公开 POST /api/password/reset(无需 Bearer):短信验证码重置密码。
|
|
1413
|
+
// 成功 → 企业端使该账号全部授权设备/会话失效(含 E2EE 派生口令)。
|
|
1414
|
+
const body = await readJsonBody(req);
|
|
1415
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1416
|
+
let phone = String(body.phone ?? "").trim();
|
|
1417
|
+
if (!phone) {
|
|
1418
|
+
// 隐私审计(2026-09):同上,账号卡改密不传明文手机号,服务端回填本机账号
|
|
1419
|
+
phone = String((loadConfig(relayDir).phone) || "").trim();
|
|
1420
|
+
}
|
|
1421
|
+
const smsCode = String(body.sms_code ?? "").trim();
|
|
1422
|
+
const newPassword = String(body.new_password ?? body.password ?? "");
|
|
1423
|
+
if (!phone || !smsCode || !newPassword) return sendJson(res, 400, { ok: false, error: "手机号、短信验证码与新密码必填" });
|
|
1424
|
+
const r = await relayFetch(relayDir, "/api/password/reset", {
|
|
1425
|
+
method: "POST",
|
|
1426
|
+
headers: { "content-type": "application/json" },
|
|
1427
|
+
body: JSON.stringify({ phone, sms_code: smsCode, new_password: newPassword }),
|
|
1428
|
+
});
|
|
1429
|
+
// 透传企业端 ok/error(成功 200 {ok:true};失败保留 status 与错误体)
|
|
1430
|
+
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1168
1433
|
{
|
|
1169
1434
|
method: "GET",
|
|
1170
1435
|
path: "/dsh-remote/captcha",
|
|
@@ -1257,7 +1522,7 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1257
1522
|
feedbackUrl: api,
|
|
1258
1523
|
reachable,
|
|
1259
1524
|
deviceId: cfg.device_id || "",
|
|
1260
|
-
phone: cfg.phone || "",
|
|
1525
|
+
phone: maskPhone(cfg.phone || ""), // 隐私:浏览器端只见掩码(代理提交时服务端另附真号)
|
|
1261
1526
|
// 登录态(已配置账号或自建密钥)→ 节点半自动附加 JWT,免图形验证码
|
|
1262
1527
|
auth: cfg.local_key || (cfg.phone && cfg.password) ? "account" : "anonymous"
|
|
1263
1528
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://github.com/mrRisega/dsh-remote/tree/main/packages/dsh-remote-web#readme",
|
|
42
42
|
"license": "PolyForm-Noncommercial-1.0.0"
|
|
43
|
-
}
|
|
43
|
+
}
|