@mrrisega/dsh-remote 0.5.0 → 0.6.0-beta.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/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +337 -48
- package/packages/dsh-remote-web/lib/index.js +150 -14
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +211 -0
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +253 -0
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +32 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-beta.0",
|
|
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",
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
// factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
|
|
6
6
|
//
|
|
7
7
|
// 功能:
|
|
8
|
-
// - settings.section
|
|
8
|
+
// - settings.section:设置页「远程访问」栏目(位于「Agent 预设」下方,官方扩展点)
|
|
9
9
|
// - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
|
|
10
10
|
// · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
|
|
11
11
|
// · 登录要求图形验证码;注册要求两次密码 + 图形验证码
|
|
12
|
-
// ·
|
|
12
|
+
// · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
|
|
13
|
+
// · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
|
|
13
14
|
// - 首次安装引导:设置页栏目旁小红点(localStorage dsh-remote-seen-dot 控制)
|
|
14
15
|
// - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
|
|
15
16
|
// 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
|
|
@@ -40,7 +41,7 @@ window.__ModuleLoader__.load({
|
|
|
40
41
|
".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
|
|
41
42
|
".dru-settings-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,#8c959f);margin-top:2px}",
|
|
42
43
|
".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
|
|
43
|
-
//
|
|
44
|
+
// 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
|
|
44
45
|
".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,#fff);pointer-events:none;z-index:1}",
|
|
45
46
|
".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
|
|
46
47
|
".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
|
|
@@ -129,10 +130,26 @@ window.__ModuleLoader__.load({
|
|
|
129
130
|
".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
|
|
130
131
|
".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
|
|
131
132
|
".dru-up-log{margin-top:8px;background:#0d1117;color:#e6edf3;border-radius:8px;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}",
|
|
133
|
+
// ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
|
|
134
|
+
".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
|
|
135
|
+
".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
|
|
136
|
+
".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;object-fit:contain}",
|
|
137
|
+
".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed #d0d7de;background:#f6f8fa;color:#8c959f;font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
|
|
138
|
+
".dru-url.big{font-size:13.5px;font-weight:600}",
|
|
139
|
+
".dru-cd{font-size:12px;color:#9a6700;margin-top:2px}",
|
|
140
|
+
".dru-cd-ok{color:#1a7f37}",
|
|
141
|
+
".dru-key-note{font-size:12px;color:#57606a;line-height:1.6}",
|
|
142
|
+
".dru-dev{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:9px 11px;margin-bottom:8px}",
|
|
143
|
+
".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
144
|
+
".dru-dev-name{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:130px}",
|
|
145
|
+
".dru-dev-meta{font-size:11.5px;color:#57606a}",
|
|
146
|
+
".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
|
|
147
|
+
".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
|
|
148
|
+
".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
|
|
132
149
|
].join("\n");
|
|
133
150
|
document.head.appendChild(styleEl);
|
|
134
151
|
|
|
135
|
-
// ──
|
|
152
|
+
// ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
|
|
136
153
|
// 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
|
|
137
154
|
// 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
|
|
138
155
|
// 设置页由官方 shell 渲染(class 含 navCell 的导航按钮 + 栏目 label),
|
|
@@ -168,17 +185,17 @@ window.__ModuleLoader__.load({
|
|
|
168
185
|
});
|
|
169
186
|
} catch (e) {}
|
|
170
187
|
}
|
|
171
|
-
/**
|
|
188
|
+
/** 扫描设置页导航,找到「远程访问」栏目按钮后注入红点。 */
|
|
172
189
|
function dotScan() {
|
|
173
190
|
if (dotSeen()) return;
|
|
174
191
|
var cells = document.querySelectorAll("button");
|
|
175
192
|
for (var i = 0; i < cells.length; i++) {
|
|
176
193
|
var c = cells[i];
|
|
177
|
-
// 语义化匹配:设置页导航按钮(navCell)+
|
|
194
|
+
// 语义化匹配:设置页导航按钮(navCell)+ 栏目名「远程访问」。
|
|
178
195
|
// class 为 shell 的 hashed 类名,取子串匹配避免依赖具体 hash;
|
|
179
196
|
// 若未来 hash 变化导致匹配失败,仅红点不显示,栏目本身不受影响。
|
|
180
197
|
if (String(c.className || "").indexOf("navCell") === -1) continue;
|
|
181
|
-
if ((c.textContent || "").indexOf("
|
|
198
|
+
if ((c.textContent || "").indexOf("远程访问") === -1) continue;
|
|
182
199
|
dotInject(c);
|
|
183
200
|
}
|
|
184
201
|
}
|
|
@@ -524,7 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
524
541
|
var payload = {
|
|
525
542
|
kind: "rating",
|
|
526
543
|
category: "satisfaction",
|
|
527
|
-
content: note.trim() || (rating >= 4 ? "
|
|
544
|
+
content: note.trim() || (rating >= 4 ? "对远程访问功能满意" : "对远程访问功能不太满意"),
|
|
528
545
|
rating: rating,
|
|
529
546
|
recommend: recommend === true ? 1 : 0,
|
|
530
547
|
};
|
|
@@ -547,7 +564,7 @@ window.__ModuleLoader__.load({
|
|
|
547
564
|
step === "rate"
|
|
548
565
|
? h("div", null,
|
|
549
566
|
h("div", { className: "dru-popup-icon" }, "😊"),
|
|
550
|
-
h("div", { className: "dru-popup-title" }, "
|
|
567
|
+
h("div", { className: "dru-popup-title" }, "您对远程访问功能满意吗?"),
|
|
551
568
|
h("div", { className: "dru-popup-sub" }, "使用体验已满 1 小时,说说真实感受吧(1 分钟搞定)"),
|
|
552
569
|
h("div", { className: "dru-popup-rate" },
|
|
553
570
|
[ [5, "😄", "很满意"], [3, "😐", "一般"], [1, "😞", "不满意"] ].map(function (r) {
|
|
@@ -609,8 +626,8 @@ window.__ModuleLoader__.load({
|
|
|
609
626
|
}
|
|
610
627
|
|
|
611
628
|
// ── 图标 ────────────────────────────────────────────────────────────────
|
|
612
|
-
//
|
|
613
|
-
//
|
|
629
|
+
// 「远程访问」栏目图标沿用面板 emoji 图标体系:📱 更贴合“手机/另一台电脑远程访问”语义
|
|
630
|
+
// (原 🖥 已随入口迁移调整);栏目导航 label 与栏目头部均使用它。
|
|
614
631
|
|
|
615
632
|
// ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
|
|
616
633
|
// 数据来自 node 半新增的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
|
|
@@ -807,6 +824,18 @@ window.__ModuleLoader__.load({
|
|
|
807
824
|
var shArr = useState(""); var selfHost = shArr[0]; var setSelfHost = shArr[1];
|
|
808
825
|
var lkArr = useState(""); var localKey = lkArr[0]; var setLocalKey = lkArr[1];
|
|
809
826
|
|
|
827
|
+
// ── 📱 远程访问卡(一次性访问密钥 + 已授权设备管理)——放在全部既有字段之后,保持既有 hook 序号 ──
|
|
828
|
+
var akeyArr = useState(null); var akey = akeyArr[0]; var setAkey = akeyArr[1]; // {url,key,expires_at,ttl_ms,qr_data_url}
|
|
829
|
+
var akeyBusyArr = useState(false); var akeyBusy = akeyBusyArr[0]; var setAkeyBusy = akeyBusyArr[1];
|
|
830
|
+
var akeyMsgArr = useState(null); var akeyMsg = akeyMsgArr[0]; var setAkeyMsg = akeyMsgArr[1]; // {kind,text}
|
|
831
|
+
var nowTickArr = useState(function () { return Date.now(); }); var nowTick = nowTickArr[0]; var setNowTick = nowTickArr[1];
|
|
832
|
+
var copiedKeyArr = useState(false); var copiedKey = copiedKeyArr[0]; var setCopiedKey = copiedKeyArr[1];
|
|
833
|
+
var devSessArr = useState(null); var devSessions = devSessArr[0]; var setDevSessions = devSessArr[1]; // null=未加载
|
|
834
|
+
var devOpenArr = useState(false); var devOpen = devOpenArr[0]; var setDevOpen = devOpenArr[1];
|
|
835
|
+
var devBusyArr = useState(""); var devBusy = devBusyArr[0]; var setDevBusy = devBusyArr[1];
|
|
836
|
+
var devMsgArr = useState(null); var devMsg = devMsgArr[0]; var setDevMsg = devMsgArr[1];
|
|
837
|
+
var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id
|
|
838
|
+
|
|
810
839
|
var refresh = useCallback(function () {
|
|
811
840
|
setBusy("status");
|
|
812
841
|
api("/dsh-remote/status").then(function (body) {
|
|
@@ -848,6 +877,162 @@ window.__ModuleLoader__.load({
|
|
|
848
877
|
if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !lcap) loadCaptcha("login");
|
|
849
878
|
}, [st, loggedIn, mode, authTab, lcap]);
|
|
850
879
|
|
|
880
|
+
// ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
|
|
881
|
+
var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
|
|
882
|
+
var STATUS_POLL_MS = 5000; // 连接状态行轮询间隔
|
|
883
|
+
|
|
884
|
+
/** 轻量状态轮询:只更新 st,不改 mode(mode 由用户 Tab 选择决定,避免轮询把自建/云端来回切)。 */
|
|
885
|
+
function pollStatus() {
|
|
886
|
+
api("/dsh-remote/status").then(function (body) {
|
|
887
|
+
if (!body || !body.ok) return;
|
|
888
|
+
setSt(body);
|
|
889
|
+
}).catch(function () {});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// 时间工具(兼容 epoch 毫秒 / ISO 字符串 / 数字字符串)
|
|
893
|
+
function toMs(ts) {
|
|
894
|
+
if (ts === null || ts === undefined || ts === "") return 0;
|
|
895
|
+
var n = Number(ts);
|
|
896
|
+
if (String(ts).trim() !== "" && isFinite(n)) return n;
|
|
897
|
+
var d = new Date(ts);
|
|
898
|
+
return isNaN(d.getTime()) ? 0 : d.getTime();
|
|
899
|
+
}
|
|
900
|
+
function pad2(n) { return String(n).padStart(2, "0"); }
|
|
901
|
+
function fmtClock(ts) {
|
|
902
|
+
var ms = toMs(ts);
|
|
903
|
+
if (!ms) return "--:--:--";
|
|
904
|
+
var d = new Date(ms);
|
|
905
|
+
if (isNaN(d.getTime())) return "--:--:--";
|
|
906
|
+
return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
|
|
907
|
+
}
|
|
908
|
+
function fmtRemain(ms) {
|
|
909
|
+
var s = Math.max(0, Math.floor((ms || 0) / 1000));
|
|
910
|
+
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
911
|
+
return (h > 0 ? pad2(h) + ":" : "") + pad2(m) + ":" + pad2(sec);
|
|
912
|
+
}
|
|
913
|
+
function fmtDT(ts) {
|
|
914
|
+
var ms = toMs(ts);
|
|
915
|
+
if (!ms) return "—";
|
|
916
|
+
var d = new Date(ms);
|
|
917
|
+
if (isNaN(d.getTime())) return "—";
|
|
918
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes());
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。 */
|
|
922
|
+
function loadAccessKey() {
|
|
923
|
+
if (akeyBusy) return;
|
|
924
|
+
setAkeyBusy(true);
|
|
925
|
+
api("/dsh-remote/access-key").then(function (b) {
|
|
926
|
+
if (!b || !b.ok) {
|
|
927
|
+
var why = (b && (b.error || (b.body && b.body.error))) || "未知错误";
|
|
928
|
+
setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + why });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// 契约容错:url 必须有;qr_data_url 取不到时仍展示链接/复制/直接打开
|
|
932
|
+
if (!b.url) {
|
|
933
|
+
setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:企业端未返回可用链接" });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
setAkey({
|
|
937
|
+
url: b.url || "",
|
|
938
|
+
key: b.key != null ? b.key : null,
|
|
939
|
+
expires_at: b.expires_at != null ? b.expires_at : null,
|
|
940
|
+
ttl_ms: b.ttl_ms != null ? b.ttl_ms : null,
|
|
941
|
+
qr_data_url: b.qr_data_url != null ? b.qr_data_url : null
|
|
942
|
+
});
|
|
943
|
+
setAkeyMsg(null);
|
|
944
|
+
}).catch(function (e) {
|
|
945
|
+
setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + e.message });
|
|
946
|
+
}).finally(function () { setAkeyBusy(false); });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
var copyKeyUrl = function () {
|
|
950
|
+
if (!(akey && akey.url)) return;
|
|
951
|
+
try {
|
|
952
|
+
navigator.clipboard.writeText(akey.url).then(function () {
|
|
953
|
+
setCopiedKey(true);
|
|
954
|
+
setTimeout(function () { setCopiedKey(false); }, 1500);
|
|
955
|
+
});
|
|
956
|
+
} catch (e) {}
|
|
957
|
+
};
|
|
958
|
+
/** 「直接打开」:浏览器新标签打开一次性访问地址(打开即扫码/点击进入)。 */
|
|
959
|
+
var openKeyUrl = function () {
|
|
960
|
+
if (!(akey && akey.url)) return;
|
|
961
|
+
try { window.open(akey.url, "_blank", "noopener"); } catch (e) {}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
/** 加载已授权设备列表:GET /dsh-remote/mobile-sessions。 */
|
|
965
|
+
function loadDevices() {
|
|
966
|
+
if (devBusy !== "") return;
|
|
967
|
+
setDevBusy("list");
|
|
968
|
+
api("/dsh-remote/mobile-sessions").then(function (b) {
|
|
969
|
+
if (!b || !b.ok) throw new Error((b && b.error) || "加载已授权设备失败");
|
|
970
|
+
setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
|
|
971
|
+
setDevMsg(null);
|
|
972
|
+
}).catch(function (e) {
|
|
973
|
+
setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
|
|
974
|
+
}).finally(function () { setDevBusy(""); });
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
var toggleDevices = function () {
|
|
978
|
+
var next = !devOpen;
|
|
979
|
+
setDevOpen(next);
|
|
980
|
+
if (next && devSessions === null && devBusy === "") loadDevices();
|
|
981
|
+
if (!next) setArmedDev(null);
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
/** 取消配对:先点一次进入确认态,再点一次才 POST revoke(同 SelfManageCard 二次确认风格)。 */
|
|
985
|
+
var doRevokeDevice = function (id) {
|
|
986
|
+
if (!id) return;
|
|
987
|
+
if (armedDev !== id) { setArmedDev(id); return; }
|
|
988
|
+
setDevBusy("revoke:" + id);
|
|
989
|
+
post("/dsh-remote/mobile-sessions/revoke", { id: id }).then(function (b) {
|
|
990
|
+
if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "取消失败");
|
|
991
|
+
setArmedDev(null);
|
|
992
|
+
setDevMsg({ kind: "ok", text: "已取消,对方需重新扫码/登录" });
|
|
993
|
+
// 刷新列表(成功即重拉,行内状态随后由列表覆盖)
|
|
994
|
+
api("/dsh-remote/mobile-sessions").then(function (lb) {
|
|
995
|
+
if (lb && lb.ok) setDevSessions(Array.isArray(lb.sessions) ? lb.sessions : []);
|
|
996
|
+
}).catch(function () {});
|
|
997
|
+
}).catch(function (e) {
|
|
998
|
+
setArmedDev(null);
|
|
999
|
+
setDevMsg({ kind: "err", text: "取消配对失败:" + e.message });
|
|
1000
|
+
}).finally(function () { setDevBusy(""); });
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
/** 升级/续费带登录态打开:先取一次性访问 url,再 window.open(node 半用账号 JWT 换 key)。 */
|
|
1004
|
+
var openUpgradeAuth = function () {
|
|
1005
|
+
setBusy("upgrade");
|
|
1006
|
+
api("/dsh-remote/access-key").then(function (b) {
|
|
1007
|
+
if (!b || !b.ok || !b.url) throw new Error((b && b.error) || "生成访问链接失败");
|
|
1008
|
+
try { window.open(b.url, "_blank", "noopener"); } catch (e2) {}
|
|
1009
|
+
setMsg("ok", "✅ 升级/续费页已在新标签页打开(带登录态)");
|
|
1010
|
+
}).catch(function (e) {
|
|
1011
|
+
setMsg("err", "打开升级/续费页失败:" + e.message);
|
|
1012
|
+
}).finally(function () { setBusy(""); });
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// 停留主视图(home)时:每 5s 轻量轮询连接状态 + 每秒刷新倒计时
|
|
1016
|
+
useEffect(function () {
|
|
1017
|
+
if (view !== "home") return;
|
|
1018
|
+
var pollIv = setInterval(function () { pollStatus(); }, STATUS_POLL_MS);
|
|
1019
|
+
var tickIv = setInterval(function () { setNowTick(Date.now()); }, 1000);
|
|
1020
|
+
return function () {
|
|
1021
|
+
clearInterval(pollIv);
|
|
1022
|
+
clearInterval(tickIv);
|
|
1023
|
+
};
|
|
1024
|
+
}, [view]);
|
|
1025
|
+
|
|
1026
|
+
// 已登录云端主视图停留期间:打开即取一把新 key + 已授权设备;之后每 ~25s 自动轮换
|
|
1027
|
+
// (未登录不轮询,避免 401 空转;离开栏目/切视图/退出登录即清理定时器)
|
|
1028
|
+
useEffect(function () {
|
|
1029
|
+
if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
|
|
1030
|
+
loadAccessKey();
|
|
1031
|
+
loadDevices();
|
|
1032
|
+
var rotateIv = setInterval(function () { loadAccessKey(); }, KEY_AUTO_REFRESH_MS);
|
|
1033
|
+
return function () { clearInterval(rotateIv); };
|
|
1034
|
+
}, [view, mode, loggedIn]);
|
|
1035
|
+
|
|
851
1036
|
function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
|
|
852
1037
|
|
|
853
1038
|
// ---------- 登录(密码/短信) ----------
|
|
@@ -948,11 +1133,6 @@ window.__ModuleLoader__.load({
|
|
|
948
1133
|
.finally(function () { setBusy(""); });
|
|
949
1134
|
};
|
|
950
1135
|
|
|
951
|
-
var copyUrl = function () {
|
|
952
|
-
if (!st) return;
|
|
953
|
-
try { navigator.clipboard.writeText(st.remoteUrl).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
|
|
954
|
-
};
|
|
955
|
-
|
|
956
1136
|
var saveLocal = function () {
|
|
957
1137
|
if (!selfHost.trim() || !localKey.trim()) { setMsg("err", "请填写服务器地址与访问密钥"); return; }
|
|
958
1138
|
setBusy("local");
|
|
@@ -991,12 +1171,6 @@ window.__ModuleLoader__.load({
|
|
|
991
1171
|
if (!ts) return "—";
|
|
992
1172
|
try { var d = new Date(Number(ts)); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } catch (e) { return "—"; }
|
|
993
1173
|
}
|
|
994
|
-
/** 推广/升级页地址(/app/promo),取不到时返回空(按钮隐藏)。 */
|
|
995
|
-
function promoUrl() {
|
|
996
|
-
var base = (pub && pub.app_url) || (st && st.remoteUrl) || "";
|
|
997
|
-
if (!base) return "";
|
|
998
|
-
return base.replace(/\/+$/, "") + "/promo";
|
|
999
|
-
}
|
|
1000
1174
|
function renderAccount() {
|
|
1001
1175
|
var a = account;
|
|
1002
1176
|
var plan = a ? a.plan : "free";
|
|
@@ -1004,7 +1178,6 @@ window.__ModuleLoader__.load({
|
|
|
1004
1178
|
var isMember = plan === "pro" || plan === "pro_max";
|
|
1005
1179
|
var endsAt = a && (a.plan_ends_at || a.trial_expires_at) ? Number(a.plan_ends_at || a.trial_expires_at) : 0;
|
|
1006
1180
|
var quotaPct = quota && quota.limit_enabled ? quota.percent : null;
|
|
1007
|
-
var promo = promoUrl();
|
|
1008
1181
|
var planText;
|
|
1009
1182
|
if (!isMember) planText = "免费额度: 带宽 ≈1Mbps" + (quotaPct !== null ? " · 本月流量已用 " + quotaPct + "%" : " · 本月流量限额 1GB");
|
|
1010
1183
|
else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
|
|
@@ -1024,15 +1197,15 @@ window.__ModuleLoader__.load({
|
|
|
1024
1197
|
h("span", null, planText)
|
|
1025
1198
|
),
|
|
1026
1199
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
1027
|
-
|
|
1028
|
-
!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员"),
|
|
1200
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
|
|
1201
|
+
busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
|
|
1029
1202
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
|
|
1030
1203
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
|
|
1031
1204
|
h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
|
|
1032
1205
|
),
|
|
1033
|
-
|
|
1034
|
-
"
|
|
1035
|
-
|
|
1206
|
+
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
1207
|
+
"升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
|
|
1208
|
+
(endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : ""))
|
|
1036
1209
|
);
|
|
1037
1210
|
}
|
|
1038
1211
|
|
|
@@ -1087,6 +1260,128 @@ window.__ModuleLoader__.load({
|
|
|
1087
1260
|
);
|
|
1088
1261
|
}
|
|
1089
1262
|
|
|
1263
|
+
// ---------- 📱 远程访问卡:一次性访问地址 / 二维码 / 状态行 ----------
|
|
1264
|
+
function renderAccessCard() {
|
|
1265
|
+
var hasKey = !!(akey && akey.url);
|
|
1266
|
+
var expMs = akey ? toMs(akey.expires_at) : 0;
|
|
1267
|
+
var remainMs = expMs ? expMs - nowTick : 0;
|
|
1268
|
+
var loggedInSaaS = !!(st && st.config && st.config.phone);
|
|
1269
|
+
var statusTxt = st === null ? "查询中…" : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
|
|
1270
|
+
var dotCls = "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off");
|
|
1271
|
+
return card("📱 远程访问", [
|
|
1272
|
+
h("div", { className: "dru-status-line" },
|
|
1273
|
+
h("span", { className: dotCls }),
|
|
1274
|
+
h("span", null, statusTxt)
|
|
1275
|
+
),
|
|
1276
|
+
akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
|
|
1277
|
+
hasKey ? h("div", null, [
|
|
1278
|
+
h("div", { className: "dru-url big", style: { marginTop: 8 } },
|
|
1279
|
+
h("span", null, akey.url),
|
|
1280
|
+
h("button", { type: "button", onClick: copyKeyUrl }, copiedKey ? "已复制" : "复制")
|
|
1281
|
+
),
|
|
1282
|
+
h("div", { className: "dru-access-flex" },
|
|
1283
|
+
h("div", { className: "dru-access-col", style: { alignItems: "center" } },
|
|
1284
|
+
akey.qr_data_url
|
|
1285
|
+
? h("img", { className: "dru-qr-img", src: akey.qr_data_url, alt: "远程访问二维码" })
|
|
1286
|
+
: h("div", { className: "dru-qr-ph" }, "二维码生成中 / 暂不可用\n链接仍可复制或直接打开"),
|
|
1287
|
+
h("div", { className: "dru-cd" + (remainMs > 0 ? " dru-cd-ok" : "") },
|
|
1288
|
+
"有效至 " + fmtClock(akey.expires_at) + " · 剩余 " + fmtRemain(remainMs))
|
|
1289
|
+
),
|
|
1290
|
+
h("div", { className: "dru-access-col" },
|
|
1291
|
+
h("div", { className: "dru-key-note" },
|
|
1292
|
+
"扫码即进入远程访问;每次生成的链接 30 分钟有效、访问一次后失效,停留栏目期间会自动更新。"),
|
|
1293
|
+
h("div", { className: "dru-actions", style: { marginTop: 2 } },
|
|
1294
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: openKeyUrl }, "直接打开"),
|
|
1295
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: akeyBusy, onClick: loadAccessKey }, akeyBusy ? "生成中…" : "刷新二维码/访问链接")
|
|
1296
|
+
),
|
|
1297
|
+
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
1298
|
+
!serviceRunning
|
|
1299
|
+
? "本机 bridge 未运行:请先在下方「🖥 Bridge 服务」卡片启动,手机/另一台电脑才能连入本机。"
|
|
1300
|
+
: "手机上打开链接点「进入」即可像在本机一样使用 dsh web。")
|
|
1301
|
+
)
|
|
1302
|
+
)
|
|
1303
|
+
]) : h("div", null, [
|
|
1304
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1305
|
+
loggedInSaaS
|
|
1306
|
+
? "正在生成一次性访问链接…(手机或另一台电脑扫码/打开即可进入)"
|
|
1307
|
+
: "登录下方「🔑 账号」卡片中的手机号账号后,即可生成一次性访问链接,让手机或另一台电脑远程使用同一份 dsh web。"),
|
|
1308
|
+
h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
1309
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: loadAccessKey },
|
|
1310
|
+
akeyBusy ? "生成中…" : "生成访问链接")
|
|
1311
|
+
)
|
|
1312
|
+
])
|
|
1313
|
+
]);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ---------- 📲 已授权设备卡(展开列表 + 二次确认取消配对) ----------
|
|
1317
|
+
function deviceLabel(s) {
|
|
1318
|
+
if (s && s.label) return String(s.label);
|
|
1319
|
+
var parts = [];
|
|
1320
|
+
if (s && s.os) parts.push(String(s.os));
|
|
1321
|
+
if (s && s.browser) parts.push(String(s.browser));
|
|
1322
|
+
return parts.length ? parts.join(" ") : "未知设备";
|
|
1323
|
+
}
|
|
1324
|
+
function deviceMeta(s) {
|
|
1325
|
+
var parts = [];
|
|
1326
|
+
if (s && s.os) parts.push(String(s.os));
|
|
1327
|
+
if (s && s.browser) parts.push(String(s.browser));
|
|
1328
|
+
return parts.join(" · ");
|
|
1329
|
+
}
|
|
1330
|
+
function renderDeviceList() {
|
|
1331
|
+
if (devSessions === null) {
|
|
1332
|
+
return h("div", { className: "dru-hint" }, devBusy === "list" ? "正在加载已授权设备…" : "加载已授权设备中,请稍候或点上方按钮重试");
|
|
1333
|
+
}
|
|
1334
|
+
if (devSessions.length === 0) {
|
|
1335
|
+
return h("div", { className: "dru-fb-empty" }, "暂无已授权设备(手机扫码后出现)");
|
|
1336
|
+
}
|
|
1337
|
+
return h("div", null, [
|
|
1338
|
+
devSessions.map(function (s) {
|
|
1339
|
+
var revoked = !!(s && s.revoked_at);
|
|
1340
|
+
return h("div", { key: s && s.id, className: "dru-dev" },
|
|
1341
|
+
h("div", { className: "dru-dev-top" },
|
|
1342
|
+
h("span", { className: "dru-dev-name" }, deviceLabel(s)),
|
|
1343
|
+
deviceMeta(s) ? h("span", { className: "dru-dev-meta" }, deviceMeta(s)) : null,
|
|
1344
|
+
revoked ? h("span", { className: "dru-dev-tag dru-dev-tag-off" }, "已取消配对") : null
|
|
1345
|
+
),
|
|
1346
|
+
h("div", { className: "dru-dev-sub" },
|
|
1347
|
+
"首次配对 " + fmtDT(s && s.created_at) +
|
|
1348
|
+
(s && s.last_seen_at ? " · 最近活跃 " + fmtDT(s.last_seen_at) : "") +
|
|
1349
|
+
(revoked ? " · 取消于 " + fmtDT(s.revoked_at) : "")
|
|
1350
|
+
),
|
|
1351
|
+
revoked ? null : h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
1352
|
+
h("button", {
|
|
1353
|
+
type: "button",
|
|
1354
|
+
className: "dru-btn dru-btn-danger",
|
|
1355
|
+
disabled: devBusy !== "",
|
|
1356
|
+
onClick: function () { doRevokeDevice(s && s.id); }
|
|
1357
|
+
}, devBusy === "revoke:" + (s && s.id) ? "取消中…" : armedDev === (s && s.id) ? "⚠ 再点一次确认取消配对" : "取消配对")
|
|
1358
|
+
)
|
|
1359
|
+
);
|
|
1360
|
+
}),
|
|
1361
|
+
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
1362
|
+
"取消配对后,对方需重新扫码/登录才能再次远程访问本机。")
|
|
1363
|
+
]);
|
|
1364
|
+
}
|
|
1365
|
+
function renderDevicesCard() {
|
|
1366
|
+
var count = devSessions === null ? null : devSessions.length;
|
|
1367
|
+
var btnLabel = devOpen
|
|
1368
|
+
? "收起已授权设备列表"
|
|
1369
|
+
: "已授权设备 " + (count === null ? "…" : count) + "(点击展开管理)";
|
|
1370
|
+
return card("📲 已授权设备", [
|
|
1371
|
+
h("div", { className: "dru-actions", style: { marginTop: 2 } },
|
|
1372
|
+
h("button", {
|
|
1373
|
+
type: "button",
|
|
1374
|
+
className: "dru-btn dru-btn-ghost",
|
|
1375
|
+
style: { width: "100%" },
|
|
1376
|
+
disabled: devBusy !== "",
|
|
1377
|
+
onClick: toggleDevices
|
|
1378
|
+
}, devBusy === "list" ? "加载中…" : btnLabel)
|
|
1379
|
+
),
|
|
1380
|
+
devOpen ? renderDeviceList() : null,
|
|
1381
|
+
devMsg ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } }, devMsg.text) : null
|
|
1382
|
+
]);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1090
1385
|
// ---------- 主视图 ----------
|
|
1091
1386
|
function renderHome() {
|
|
1092
1387
|
var isLocal = mode === "local";
|
|
@@ -1096,16 +1391,10 @@ window.__ModuleLoader__.load({
|
|
|
1096
1391
|
h("div", { className: "dru-tab" + (!isLocal ? " active" : ""), onClick: function () { setMode("saas"); setMessage(null); } }, "☁️ 云端服务"),
|
|
1097
1392
|
h("div", { className: "dru-tab" + (isLocal ? " active" : ""), onClick: function () { setMode("local"); setMessage(null); } }, "🖥 自建服务")
|
|
1098
1393
|
),
|
|
1099
|
-
// 云端 tab
|
|
1394
|
+
// 云端 tab:📱 远程访问(一次性扫码访问 + 已授权设备)+ 账号(手机号登录,官方托管)
|
|
1100
1395
|
!isLocal ? h("div", null,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
h("span", null, st ? st.remoteUrl : "加载中…"),
|
|
1104
|
-
h("button", { type: "button", onClick: copyUrl }, copied ? "已复制" : "复制")
|
|
1105
|
-
),
|
|
1106
|
-
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1107
|
-
st && st.relayReachable === false ? "⚠ 云端服务不可达(地址可能不是最新)" : "手机浏览器打开此地址登录后即可远程控制本机。")
|
|
1108
|
-
),
|
|
1396
|
+
renderAccessCard(),
|
|
1397
|
+
renderDevicesCard(),
|
|
1109
1398
|
card("🔑 账号",
|
|
1110
1399
|
st === null
|
|
1111
1400
|
? h("div", { className: "dru-hint" }, "正在读取远控状态…")
|
|
@@ -1188,12 +1477,12 @@ window.__ModuleLoader__.load({
|
|
|
1188
1477
|
)
|
|
1189
1478
|
: null
|
|
1190
1479
|
),
|
|
1191
|
-
// 关于 dsh-remote
|
|
1480
|
+
// 关于 dsh-remote(v0.5+ 远程访问价值说明卡片)
|
|
1192
1481
|
card("📖 关于 dsh-remote", [
|
|
1193
|
-
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "
|
|
1194
|
-
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "
|
|
1195
|
-
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "
|
|
1196
|
-
h("div", { className: "dru-hint" }, "
|
|
1482
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
|
|
1483
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
|
|
1484
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
|
|
1485
|
+
h("div", { className: "dru-hint" }, "🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。")
|
|
1197
1486
|
]),
|
|
1198
1487
|
// 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
|
|
1199
1488
|
h(SelfManageCard, null),
|
|
@@ -1201,12 +1490,12 @@ window.__ModuleLoader__.load({
|
|
|
1201
1490
|
);
|
|
1202
1491
|
}
|
|
1203
1492
|
|
|
1204
|
-
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "
|
|
1493
|
+
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问" },
|
|
1205
1494
|
h("div", { className: "dru-settings-head" },
|
|
1206
|
-
h("span", { className: "dru-settings-icon" }, "
|
|
1495
|
+
h("span", { className: "dru-settings-icon" }, "📱"),
|
|
1207
1496
|
h("div", null,
|
|
1208
|
-
h("h2", { className: "dru-settings-title" }, "
|
|
1209
|
-
h("div", { className: "dru-settings-sub" }, "
|
|
1497
|
+
h("h2", { className: "dru-settings-title" }, "远程访问"),
|
|
1498
|
+
h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")
|
|
1210
1499
|
)
|
|
1211
1500
|
),
|
|
1212
1501
|
h("div", { className: "dru-settings-body" },
|
|
@@ -1220,13 +1509,13 @@ window.__ModuleLoader__.load({
|
|
|
1220
1509
|
function apply(ctx) {
|
|
1221
1510
|
// 面板入口迁移:从侧边栏(sidebar.footer.action)移入「设置」页官方扩展点
|
|
1222
1511
|
// settings.section(列表槽,由 ui-settings-general 在 sidebar.settings 下声明)。
|
|
1223
|
-
// order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label
|
|
1512
|
+
// order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(📱 远程访问)。
|
|
1224
1513
|
ctx.slots.inject("settings.section", function () {
|
|
1225
1514
|
return ctx.slots.register({
|
|
1226
1515
|
name: "settings.section",
|
|
1227
1516
|
id: "dsh-remote",
|
|
1228
1517
|
order: 30,
|
|
1229
|
-
label: function () { return "
|
|
1518
|
+
label: function () { return "📱 远程访问"; }
|
|
1230
1519
|
}, RemoteControlSection);
|
|
1231
1520
|
});
|
|
1232
1521
|
ctx.slots.inject("shell.overlay", function () {
|