dsh-remote-web 0.7.11-beta.3 → 0.7.12-beta.2
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 +703 -115
- package/lib/index.js +487 -31
- package/package.json +1 -1
- package/runtime/clients/dsh-remote/dingtalk-bot.mjs +1318 -0
- package/runtime/clients/dsh-remote/docs/feishu-bot-contract.md +177 -0
- package/runtime/clients/dsh-remote/dsh-bridge.mjs +398 -80
- package/runtime/clients/dsh-remote/feishu-bot.mjs +1668 -0
- package/runtime/clients/dsh-remote/im-format.mjs +372 -0
- package/runtime/clients/dsh-remote/mirror-diag-script.js +53 -14
- package/runtime/clients/dsh-remote/mobile-adapter.mjs +355 -40
- package/runtime/clients/dsh-remote/upstream-discovery.mjs +495 -43
- package/runtime/clients/dsh-remote/wecom-bot.mjs +238 -12
- package/runtime/dsh-setup.mjs +78 -7
package/lib/client.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// 排在「连接微信机器人」按钮之前 —— 它是绑定的理由,又不把按钮挤出首屏。
|
|
31
31
|
// ✨ 动效引导:未绑定时「连接」按钮做很慢的呼吸(.dru-wx-attn),
|
|
32
32
|
// prefers-reduced-motion:reduce 下关闭;状态从不靠动效单独表达。
|
|
33
|
-
// · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
|
|
33
|
+
// · bridge 状态与启停开关 + 关于 dsh-remote-web 说明卡片
|
|
34
34
|
// · 🧰 账号卡「更多」:只收纳(用户关掉邀请引导之后的)「升级 / 带新用户」两个入口 ——
|
|
35
35
|
// 主界面只剩一个入口,减少营销露出;**档位规格数字(Mbps / GB / 价格)一律不展示**
|
|
36
36
|
// (0.6.11 用户约定:「插件面板里面不要展示『更多』里的 PRO 版本流量带宽」)
|
|
@@ -667,6 +667,29 @@ window.__ModuleLoader__.load({
|
|
|
667
667
|
".dru-ia-iconbtn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));cursor:pointer}",
|
|
668
668
|
".dru-ia-iconbtn:hover{color:var(--dru-accent);border-color:var(--dru-accent)}",
|
|
669
669
|
".dru-ia-iconbtn:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
|
|
670
|
+
// 会员档位条 + 「会员功能」徽章(2026-11-07 桥侧会员判定;只做展示,不做门禁)
|
|
671
|
+
".dru-ia-tier{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:1.5;background:var(--dru-surface-3);color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));border:1px solid var(--dru-border-soft)}",
|
|
672
|
+
".dru-ia-tier.is-member{color:var(--dru-accent-fg);background:var(--dru-accent-tint);border-color:var(--dru-accent-tint-border)}",
|
|
673
|
+
".dru-ia-tier-name{font-weight:600;min-width:0}",
|
|
674
|
+
".dru-ia-tier-link{flex:none;font-size:12px;color:var(--dru-accent);text-decoration:none}",
|
|
675
|
+
".dru-ia-tier-link:hover{text-decoration:underline}",
|
|
676
|
+
".dru-ia-badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:11px;font-weight:600;line-height:1.4;color:var(--dru-accent-fg);background:var(--dru-accent-tint);border:1px solid var(--dru-accent-tint-border);cursor:help}",
|
|
677
|
+
"select.dru-ia-input[disabled]{opacity:.6;cursor:not-allowed;background:var(--dru-surface-3)}",
|
|
678
|
+
// 飞书 8 步向导:编号列表紧凑可读(一屏内可滚,不撑破弹层)
|
|
679
|
+
".dru-ia-steps{margin:6px 0 8px;padding-left:20px;font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary,var(--dru-fg-muted))}",
|
|
680
|
+
".dru-ia-steps li{margin:2px 0}",
|
|
681
|
+
// A3/A6(2026-11-07 业主逐条 UI 修正):状态头右侧按钮组 / 后台服务横向紧凑布局 / 卸载反馈单选行
|
|
682
|
+
".dru-ia-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}",
|
|
683
|
+
".dru-bridge-keep{display:flex;align-items:center;gap:6px;margin:8px 0 10px;padding:7px 10px;border-radius:8px;font-size:12px;line-height:1.5;color:var(--dru-warn-fg);background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border)}",
|
|
684
|
+
".dru-bridge-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 16px;align-items:center}",
|
|
685
|
+
"@media (max-width:520px){.dru-bridge-grid{grid-template-columns:minmax(0,1fr)}}",
|
|
686
|
+
".dru-bridge-cell{min-width:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted))}",
|
|
687
|
+
".dru-bridge-cell .dru-status-line{margin:0}",
|
|
688
|
+
".dru-bridge-cell-action{display:flex;justify-content:flex-end}",
|
|
689
|
+
"@media (max-width:520px){.dru-bridge-cell-action{justify-content:flex-start}}",
|
|
690
|
+
".dru-actions-wrap{flex-wrap:wrap}",
|
|
691
|
+
".dru-ia-row-compact{min-height:40px;padding:6px 10px}",
|
|
692
|
+
".dru-ia-row-compact input[type=radio]{flex:none;margin:0}",
|
|
670
693
|
// 配置页的「为什么要填这些」折叠按钮 + 原生 select 统一外观(业主:别文字堆砌,多余信息收起来)
|
|
671
694
|
".dru-ia-disclose{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:32px;padding:0;border:0;background:none;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));font:inherit;font-size:12px;cursor:pointer;text-align:left}",
|
|
672
695
|
".dru-ia-disclose:hover{color:var(--dru-accent)}",
|
|
@@ -1956,7 +1979,7 @@ window.__ModuleLoader__.load({
|
|
|
1956
1979
|
return {
|
|
1957
1980
|
bucket: "permission",
|
|
1958
1981
|
title: "没有写入权限(装不进运行目录)",
|
|
1959
|
-
hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote 写入用户目录后重试。",
|
|
1982
|
+
hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote-web 写入用户目录后重试。",
|
|
1960
1983
|
repair: true, retry: true
|
|
1961
1984
|
};
|
|
1962
1985
|
}
|
|
@@ -1990,7 +2013,7 @@ window.__ModuleLoader__.load({
|
|
|
1990
2013
|
return {
|
|
1991
2014
|
bucket: "permission",
|
|
1992
2015
|
title: "没有写入权限(装不进运行目录)",
|
|
1993
|
-
hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote 写入用户目录后重试。",
|
|
2016
|
+
hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote-web 写入用户目录后重试。",
|
|
1994
2017
|
repair: true, retry: true
|
|
1995
2018
|
};
|
|
1996
2019
|
}
|
|
@@ -2804,6 +2827,16 @@ window.__ModuleLoader__.load({
|
|
|
2804
2827
|
// 最近一次拉到的日志尾巴(**与 log 分开存**的原因见 applyUpdLog 收尾分支:
|
|
2805
2828
|
// log 只服务于"正在更新"的黑框,收尾即清空;lastLog 留给用户主动回看)。
|
|
2806
2829
|
var lastLogArr = useState(""); var lastLog = lastLogArr[0]; var setLastLog = lastLogArr[1];
|
|
2830
|
+
// A7(2026-11-07 业主):彻底卸载前先做一次**挽留反馈**("为什么要卸载?" + 建议),
|
|
2831
|
+
// 提交走现有反馈通道(POST /dsh-remote/feedback,kind=uninstall),然后再继续卸载。
|
|
2832
|
+
var unrOpenArr = useState(false); var unrOpen = unrOpenArr[0]; var setUnrOpen = unrOpenArr[1];
|
|
2833
|
+
var unrReasonArr = useState(""); var unrReason = unrReasonArr[0]; var setUnrReason = unrReasonArr[1];
|
|
2834
|
+
var unrNoteArr = useState(""); var unrNote = unrNoteArr[0]; var setUnrNote = unrNoteArr[1];
|
|
2835
|
+
var unrBusyArr = useState(false); var unrBusy = unrBusyArr[0]; var setUnrBusy = unrBusyArr[1];
|
|
2836
|
+
var unrErrArr = useState(null); var unrErr = unrErrArr[0]; var setUnrErr = unrErrArr[1];
|
|
2837
|
+
// 2026-11-07 业主:版本卡里给出可复制的安装/更新命令(npm 包名保持不变:@mrrisega/dsh-remote)。
|
|
2838
|
+
var INSTALL_CMD = "npx @mrrisega/dsh-remote@latest";
|
|
2839
|
+
var instCopyArr = useState(false); var instCopied = instCopyArr[0]; var setInstCopied = instCopyArr[1];
|
|
2807
2840
|
|
|
2808
2841
|
var loadVer = useCallback(function () {
|
|
2809
2842
|
api("/dsh-remote/self").then(function (b) {
|
|
@@ -2971,8 +3004,94 @@ window.__ModuleLoader__.load({
|
|
|
2971
3004
|
}).finally(function () { setCxBusy(false); });
|
|
2972
3005
|
};
|
|
2973
3006
|
|
|
3007
|
+
/** A7 弹层:为什么要卸载?+ 原因单选 + 建议 + 「仍要卸载 / 我再想想」。 */
|
|
3008
|
+
function renderUninstallFeedback() {
|
|
3009
|
+
var close = function () { if (!unrBusy) setUnrOpen(false); };
|
|
3010
|
+
return iaDialog(Object.assign({
|
|
3011
|
+
title: "卸载前,想听听你的想法",
|
|
3012
|
+
sub: "提交后会继续卸载;也可以点「我再想想」退出",
|
|
3013
|
+
onClose: close,
|
|
3014
|
+
foot: [
|
|
3015
|
+
h("button", { type: "button", className: "dru-ia-btn", disabled: unrBusy, onClick: close }, "我再想想"),
|
|
3016
|
+
h("button", {
|
|
3017
|
+
type: "button", className: "dru-ia-btn is-danger", disabled: unrBusy,
|
|
3018
|
+
onClick: function () { submitUninstallFeedback(true); }
|
|
3019
|
+
}, unrBusy ? "提交中…" : "仍要卸载")
|
|
3020
|
+
],
|
|
3021
|
+
children: [
|
|
3022
|
+
h("div", { className: "dru-ia-field dru-field" },
|
|
3023
|
+
h("div", { className: "dru-ia-label" }, "为什么要卸载?"),
|
|
3024
|
+
h("div", { className: "dru-ia-list" }, UNINSTALL_REASONS.map(function (r) {
|
|
3025
|
+
return h("label", { key: r.id, className: "dru-ia-row dru-ia-row-compact", style: { cursor: "pointer" } },
|
|
3026
|
+
h("input", {
|
|
3027
|
+
type: "radio", name: "dru-uninstall-reason", value: r.id,
|
|
3028
|
+
checked: unrReason === r.id, disabled: unrBusy,
|
|
3029
|
+
onChange: function () { setUnrReason(r.id); setUnrErr(null); }
|
|
3030
|
+
}),
|
|
3031
|
+
h("span", { className: "dru-ia-row-main" }, h("span", { className: "dru-ia-row-title" }, r.label)));
|
|
3032
|
+
})),
|
|
3033
|
+
unrErr ? h("div", { className: "dru-ia-err", role: "alert" }, unrErr) : null),
|
|
3034
|
+
h("div", { className: "dru-ia-field dru-field" },
|
|
3035
|
+
h("label", { className: "dru-ia-label", htmlFor: "dru-uninstall-note" }, "还有什么想告诉我们的?(可选)"),
|
|
3036
|
+
h("textarea", {
|
|
3037
|
+
id: "dru-uninstall-note", className: "dru-ia-input", rows: 3, value: unrNote,
|
|
3038
|
+
placeholder: "比如卡在哪一步、希望我们怎么改", disabled: unrBusy,
|
|
3039
|
+
onChange: function (e) { setUnrNote(e.target.value); }
|
|
3040
|
+
})),
|
|
3041
|
+
// 后果说明(业主 A7:卸载前讲清楚会发生什么;也让"不再保留 bridge/配置"的文案有落点)
|
|
3042
|
+
h("div", { className: "dru-ia-hint" },
|
|
3043
|
+
"仍要卸载会:① 停止并移除 bridge 自启动服务;② 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销。")
|
|
3044
|
+
] }));
|
|
3045
|
+
}
|
|
3046
|
+
// Esc 关闭(弹层键盘契约;与面板里 iaDialog 的交互保持一致)
|
|
3047
|
+
useEffect(function () {
|
|
3048
|
+
if (!unrOpen) return undefined;
|
|
3049
|
+
var onKey = function (ev) {
|
|
3050
|
+
try {
|
|
3051
|
+
if (!ev) return;
|
|
3052
|
+
if (ev.key === "Escape" || ev.keyCode === 27) { if (!unrBusy) setUnrOpen(false); }
|
|
3053
|
+
} catch (e) { /* 忽略 */ }
|
|
3054
|
+
};
|
|
3055
|
+
try { document.addEventListener("keydown", onKey, true); } catch (e) { /* 忽略 */ }
|
|
3056
|
+
return function () { try { document.removeEventListener("keydown", onKey, true); } catch (e) { /* 忽略 */ } };
|
|
3057
|
+
}, [unrOpen, unrBusy]);
|
|
3058
|
+
|
|
3059
|
+
/** 挽留反馈的选项(业主:几个原因单选 + 可填建议)。 */
|
|
3060
|
+
var UNINSTALL_REASONS = [
|
|
3061
|
+
{ id: "unstable", label: "功能不好用 / 不稳定" },
|
|
3062
|
+
{ id: "complex", label: "太复杂,看不懂怎么用" },
|
|
3063
|
+
{ id: "alternative", label: "已经有别的替代工具" },
|
|
3064
|
+
{ id: "resource", label: "占用资源 / 影响电脑运行" },
|
|
3065
|
+
{ id: "temporary", label: "只是临时不用了,以后可能再装" },
|
|
3066
|
+
{ id: "other", label: "其它原因" }
|
|
3067
|
+
];
|
|
3068
|
+
/** A7:点「彻底卸载」→ 先弹挽留反馈(不直接卸载;确认语义由这个弹层承担)。 */
|
|
2974
3069
|
var doUninstall = function () {
|
|
2975
|
-
|
|
3070
|
+
setUnrReason("");
|
|
3071
|
+
setUnrNote("");
|
|
3072
|
+
setUnrErr(null);
|
|
3073
|
+
setUnrOpen(true);
|
|
3074
|
+
};
|
|
3075
|
+
/** 提交挽留反馈(失败也继续卸载:反馈是"尽量收集",不能反过来卡住用户)。 */
|
|
3076
|
+
var submitUninstallFeedback = function (thenUninstall) {
|
|
3077
|
+
if (unrBusy) return;
|
|
3078
|
+
var reason = String(unrReason || "");
|
|
3079
|
+
if (!reason) { setUnrErr("请选择一个卸载原因(必选)"); return; }
|
|
3080
|
+
var hit = null;
|
|
3081
|
+
for (var i = 0; i < UNINSTALL_REASONS.length; i++) { if (UNINSTALL_REASONS[i].id === reason) { hit = UNINSTALL_REASONS[i]; break; } }
|
|
3082
|
+
var content = "[卸载反馈] 原因:" + ((hit && hit.label) || reason)
|
|
3083
|
+
+ (unrNote.trim() ? ("\n建议:" + unrNote.trim()) : "");
|
|
3084
|
+
setUnrBusy(true);
|
|
3085
|
+
setUnrErr(null);
|
|
3086
|
+
post("/dsh-remote/feedback", { kind: "uninstall", category: "uninstall", content: content })
|
|
3087
|
+
.catch(function () { /* 反馈失败静默:不能因此拦住用户卸载 */ })
|
|
3088
|
+
.finally(function () {
|
|
3089
|
+
setUnrBusy(false);
|
|
3090
|
+
setUnrOpen(false);
|
|
3091
|
+
if (thenUninstall) runUninstall();
|
|
3092
|
+
});
|
|
3093
|
+
};
|
|
3094
|
+
var runUninstall = function () {
|
|
2976
3095
|
setUnBusy(true);
|
|
2977
3096
|
setSelfMsg(null);
|
|
2978
3097
|
// 卸载:服务端现在"先卸插件、先回响应、慢活交子进程",正常应在 1 秒内返回;
|
|
@@ -3036,6 +3155,26 @@ window.__ModuleLoader__.load({
|
|
|
3036
3155
|
? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
|
|
3037
3156
|
: chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
|
|
3038
3157
|
),
|
|
3158
|
+
// 安装 / 更新命令(可复制;复用账号卡那套 .dru-copy-* 样式与反馈)
|
|
3159
|
+
h("div", { className: "dru-copy-row", style: { marginTop: 8 } },
|
|
3160
|
+
h("div", { className: "dru-copy-label" }, "安装 / 更新命令"),
|
|
3161
|
+
h("div", { className: "dru-copy-box" },
|
|
3162
|
+
h("span", { className: "dru-code-val" }, INSTALL_CMD),
|
|
3163
|
+
h("button", {
|
|
3164
|
+
type: "button",
|
|
3165
|
+
className: "dru-copy-btn" + (instCopied ? " copied" : ""),
|
|
3166
|
+
"aria-label": "复制安装命令",
|
|
3167
|
+
onClick: function () {
|
|
3168
|
+
copyText(INSTALL_CMD).then(function (done) {
|
|
3169
|
+
if (!done) { setSelfMsg({ kind: "err", text: "复制失败:请手动选中上面的命令复制(浏览器可能限制了剪贴板权限)" }); return; }
|
|
3170
|
+
setInstCopied(true);
|
|
3171
|
+
try { setTimeout(function () { setInstCopied(false); }, 1600); } catch (e) { /* 忽略 */ }
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
}, instCopied ? "✅ 已复制" : "复制命令")
|
|
3175
|
+
),
|
|
3176
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } }, "已安装的更新:重跑这条命令即可。")
|
|
3177
|
+
),
|
|
3039
3178
|
h("div", { className: "dru-meta" },
|
|
3040
3179
|
runtimeReady === false
|
|
3041
3180
|
? "⚠ 后台服务运行环境缺失(点下方「一键更新」会自动补齐并启动,不用手动装)"
|
|
@@ -3103,7 +3242,7 @@ window.__ModuleLoader__.load({
|
|
|
3103
3242
|
style: { marginLeft: "auto" },
|
|
3104
3243
|
disabled: unBusy || updating || upBusy,
|
|
3105
3244
|
onClick: doUninstall
|
|
3106
|
-
}, unBusy ? "卸载中…" :
|
|
3245
|
+
}, unBusy ? "卸载中…" : "彻底卸载")
|
|
3107
3246
|
),
|
|
3108
3247
|
cxMsg ? h("div", { className: "dru-msg dru-msg-" + cxMsg.kind }, cxMsg.text) : null,
|
|
3109
3248
|
updated
|
|
@@ -3137,8 +3276,10 @@ window.__ModuleLoader__.load({
|
|
|
3137
3276
|
: null,
|
|
3138
3277
|
selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
|
|
3139
3278
|
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
3140
|
-
|
|
3141
|
-
"检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。"
|
|
3279
|
+
"", // A7:二次确认已改成"挽留反馈弹层"(见 renderUninstallFeedback),这里不再有 armed 文案
|
|
3280
|
+
"检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。"
|
|
3281
|
+
+ "彻底卸载前会先询问一句卸载原因(可跳过反馈直接卸载)。"),
|
|
3282
|
+
unrOpen ? renderUninstallFeedback() : null
|
|
3142
3283
|
);
|
|
3143
3284
|
}
|
|
3144
3285
|
|
|
@@ -3443,6 +3584,24 @@ window.__ModuleLoader__.load({
|
|
|
3443
3584
|
// 从「飞书/钉钉即将支持 → 告诉我,优先支持」跳到反馈视图时带过去的一句引导(独立状态:
|
|
3444
3585
|
// message 会被面板里各种动作清掉,跨视图的上下文不能押在它身上)。
|
|
3445
3586
|
var iaFbHintArr = useState(""); var iaFbHint = iaFbHintArr[0]; var setIaFbHint = iaFbHintArr[1];
|
|
3587
|
+
// 未登录守卫:为什么弹登录(A4/A5)+ 登录后自动回到哪个流程("手机访问"登录完继续生成访问码)。
|
|
3588
|
+
var iaLoginWhyArr = useState(""); var iaLoginWhy = iaLoginWhyArr[0]; var setIaLoginWhy = iaLoginWhyArr[1];
|
|
3589
|
+
var iaAfterLoginArr = useState(""); var iaAfterLogin = iaAfterLoginArr[0]; var setIaAfterLogin = iaAfterLoginArr[1];
|
|
3590
|
+
// ── 钉钉 / 飞书(2026-11-08):与 wecom 并列的两条通道 ──
|
|
3591
|
+
var imDingIdArr = useState(""); var imDingId = imDingIdArr[0]; var setImDingId = imDingIdArr[1];
|
|
3592
|
+
var imDingSecretArr = useState(""); var imDingSecret = imDingSecretArr[0]; var setImDingSecret = imDingSecretArr[1];
|
|
3593
|
+
var imDingRobotArr = useState(""); var imDingRobot = imDingRobotArr[0]; var setImDingRobot = imDingRobotArr[1];
|
|
3594
|
+
var imFeishuAppIdArr = useState(""); var imFeishuAppId = imFeishuAppIdArr[0]; var setImFeishuAppId = imFeishuAppIdArr[1];
|
|
3595
|
+
var imFeishuSecretArr = useState(""); var imFeishuSecret = imFeishuSecretArr[0]; var setImFeishuSecret = imFeishuSecretArr[1];
|
|
3596
|
+
var imFeishuRecvArr = useState(""); var imFeishuRecv = imFeishuRecvArr[0]; var setImFeishuRecv = imFeishuRecvArr[1];
|
|
3597
|
+
var imFeishuTypeArr = useState("open_id"); var imFeishuType = imFeishuTypeArr[0]; var setImFeishuType = imFeishuTypeArr[1];
|
|
3598
|
+
// 平台级消息/错误/忙碌(与 wecom 的 imBotsMsg 分开:切平台时不会串台)
|
|
3599
|
+
var imPlatMsgArr = useState(null); var imPlatMsg = imPlatMsgArr[0]; var setImPlatMsg = imPlatMsgArr[1]; // {kind,text}
|
|
3600
|
+
var imPlatErrArr = useState({}); var imPlatErr = imPlatErrArr[0]; var setImPlatErr = imPlatErrArr[1]; // {field: msg}
|
|
3601
|
+
var imPlatBusyArr = useState(""); var imPlatBusy = imPlatBusyArr[0]; var setImPlatBusy = imPlatBusyArr[1]; // ""|"save"|"test"|"deps"
|
|
3602
|
+
// 飞书 SDK 依赖(按需下载):installed/version/phase/lastError
|
|
3603
|
+
var imDepsArr = useState(null); var imDeps = imDepsArr[0]; var setImDeps = imDepsArr[1];
|
|
3604
|
+
var imFeishuWizardArr = useState(false); var imFeishuWizard = imFeishuWizardArr[0]; var setImFeishuWizard = imFeishuWizardArr[1];
|
|
3446
3605
|
|
|
3447
3606
|
/** 读通道配置(GET 只回脱敏视图:botId/hasSecret/url/targetChatId + 桥写的状态)。 */
|
|
3448
3607
|
function loadImBots() {
|
|
@@ -3624,8 +3783,12 @@ window.__ModuleLoader__.load({
|
|
|
3624
3783
|
.finally(function () { setImBotsBusy(""); });
|
|
3625
3784
|
}
|
|
3626
3785
|
|
|
3627
|
-
|
|
3628
|
-
|
|
3786
|
+
/**
|
|
3787
|
+
* 拉全量状态。quiet=true:**不占用忙碌态**(用于"登录后异步补状态"——
|
|
3788
|
+
* 业主实测登录 7~8 秒的体验里,按钮一直转圈是主因之一;这里让按钮只反映登录请求本身)。
|
|
3789
|
+
*/
|
|
3790
|
+
var refresh = useCallback(function (quiet) {
|
|
3791
|
+
if (!quiet) setBusy("status");
|
|
3629
3792
|
var t0 = netNow(); // 📶 延迟采样点①:打开面板时这一次 /status(既有请求,不新增)
|
|
3630
3793
|
api("/dsh-remote/status").then(function (body) {
|
|
3631
3794
|
noteRtt(netNow() - t0); // 先记样本再 setSt —— 这一次的延迟当轮就能显示出来
|
|
@@ -3633,9 +3796,14 @@ window.__ModuleLoader__.load({
|
|
|
3633
3796
|
setMode(body && body.config && body.config.mode === "local" ? "local" : "saas");
|
|
3634
3797
|
setMessage(null);
|
|
3635
3798
|
}).catch(function (e) { setMessage({ kind: "err", text: "读取状态失败: " + e.message }); })
|
|
3636
|
-
.finally(function () { setBusy(""); });
|
|
3799
|
+
.finally(function () { if (!quiet) setBusy(""); });
|
|
3637
3800
|
}, []);
|
|
3638
3801
|
|
|
3802
|
+
/** 后台静默补状态(不置忙碌、失败静默)——登录成功后用它把衍生信息补上。 */
|
|
3803
|
+
var refreshQuiet = function () {
|
|
3804
|
+
try { refresh(true); } catch (e) { /* 忽略 */ }
|
|
3805
|
+
};
|
|
3806
|
+
|
|
3639
3807
|
function loadCaptcha(kind) {
|
|
3640
3808
|
var setCap = kind === "login" ? setLcap : setRcap;
|
|
3641
3809
|
setCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
|
|
@@ -4260,8 +4428,25 @@ window.__ModuleLoader__.load({
|
|
|
4260
4428
|
* 于是"服务没重启成功"和"一切正常"在界面上长得一模一样。
|
|
4261
4429
|
*/
|
|
4262
4430
|
function applyAccountSaved(cfg, okText) {
|
|
4263
|
-
setSt(cfg);
|
|
4264
4431
|
var br = cfg && cfg.bridgeRestart;
|
|
4432
|
+
// B2/B3:登录响应已改成"立即返回"(statusPending=true,只回最小 config 回显)——
|
|
4433
|
+
// 合并进现有 st(不覆盖 service/connect 等已渲染数据),其余信息后台静默补。
|
|
4434
|
+
if (cfg && cfg.statusPending) {
|
|
4435
|
+
setSt(function (prev) {
|
|
4436
|
+
var base = prev && typeof prev === "object" ? prev : {};
|
|
4437
|
+
return Object.assign({}, base, { ok: true, config: Object.assign({}, (base && base.config) || {}, cfg.config || {}) });
|
|
4438
|
+
});
|
|
4439
|
+
if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
|
|
4440
|
+
setMsg("warn", okText + ";但后台服务未能自动重启:"
|
|
4441
|
+
+ ((br.detail || br.status) || "未知原因")
|
|
4442
|
+
+ " —— 新账号可能收不到这台设备,请点下方「重启服务」或重启一次 dsh web。");
|
|
4443
|
+
} else {
|
|
4444
|
+
setMsg("ok", okText + "(正在准备远程服务…)");
|
|
4445
|
+
}
|
|
4446
|
+
refreshQuiet();
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
setSt(cfg);
|
|
4265
4450
|
if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
|
|
4266
4451
|
setMsg("warn", okText + ";但后台服务未能自动重启:"
|
|
4267
4452
|
+ ((br.detail || br.status) || "未知原因")
|
|
@@ -4277,12 +4462,28 @@ window.__ModuleLoader__.load({
|
|
|
4277
4462
|
post("/dsh-remote/login", { phone: phone.trim(), password: pass, captcha_id: lcap.id, captcha_answer: lcapTxt.trim() })
|
|
4278
4463
|
.then(function (body) {
|
|
4279
4464
|
if (body.ok || body.status === 200) {
|
|
4465
|
+
// B3(2026-11-07):**登录请求成功即切已登录 UI**,不再等 /config(它要保存配对、
|
|
4466
|
+
// 重启 bridge、拉衍生信息)。按钮忙碌态只覆盖真正的登录请求。
|
|
4467
|
+
setBusy("");
|
|
4468
|
+
setSt(function (prev) {
|
|
4469
|
+
var base = prev && typeof prev === "object" ? prev : {};
|
|
4470
|
+
return Object.assign({}, base, {
|
|
4471
|
+
ok: true,
|
|
4472
|
+
config: Object.assign({}, (base && base.config) || {}, {
|
|
4473
|
+
phone: phone.trim(), hasPhone: true, hasPassword: true
|
|
4474
|
+
})
|
|
4475
|
+
});
|
|
4476
|
+
});
|
|
4477
|
+
setMsg("ok", "登录成功,正在准备远程服务…");
|
|
4280
4478
|
return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
|
|
4281
4479
|
// 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
|
|
4282
4480
|
if (prevPhone !== phone.trim()) fbClearThreads();
|
|
4283
4481
|
setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
|
|
4284
4482
|
setPass(""); setLcapTxt(""); setLcap(null);
|
|
4285
4483
|
applyAccountSaved(cfg, "✅ 登录成功,账号已保存");
|
|
4484
|
+
}).catch(function (e) {
|
|
4485
|
+
// 账号已登录成功,只是本机配对保存失败:如实提示,不影响已登录 UI
|
|
4486
|
+
setMsg("warn", "登录成功,但本机配对信息保存失败:" + (e && e.message ? e.message : e) + "(可重试或重启面板)");
|
|
4286
4487
|
});
|
|
4287
4488
|
}
|
|
4288
4489
|
var relayBody = body.body || {};
|
|
@@ -4333,10 +4534,24 @@ window.__ModuleLoader__.load({
|
|
|
4333
4534
|
post("/dsh-remote/register", regPayload)
|
|
4334
4535
|
.then(function (body) {
|
|
4335
4536
|
if (body.ok || body.status === 201 || (body.body && body.body.token)) {
|
|
4537
|
+
// B3:注册成功即切已登录 UI(与登录同一策略),/config 异步完成配对保存。
|
|
4538
|
+
setBusy("");
|
|
4539
|
+
setSt(function (prev) {
|
|
4540
|
+
var base = prev && typeof prev === "object" ? prev : {};
|
|
4541
|
+
return Object.assign({}, base, {
|
|
4542
|
+
ok: true,
|
|
4543
|
+
config: Object.assign({}, (base && base.config) || {}, {
|
|
4544
|
+
phone: rphone.trim(), hasPhone: true, hasPassword: true
|
|
4545
|
+
})
|
|
4546
|
+
});
|
|
4547
|
+
});
|
|
4548
|
+
setMsg("ok", "注册成功,正在准备远程服务…");
|
|
4336
4549
|
return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
|
|
4337
4550
|
setPwdOpen(false);
|
|
4338
4551
|
setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
|
|
4339
4552
|
applyAccountSaved(cfg, "✅ 注册成功,已自动登录");
|
|
4553
|
+
}).catch(function (e) {
|
|
4554
|
+
setMsg("warn", "注册成功,但本机配对信息保存失败:" + (e && e.message ? e.message : e));
|
|
4340
4555
|
});
|
|
4341
4556
|
}
|
|
4342
4557
|
var b = body.body || {};
|
|
@@ -4543,8 +4758,18 @@ window.__ModuleLoader__.load({
|
|
|
4543
4758
|
// 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
|
|
4544
4759
|
fbClearThreads();
|
|
4545
4760
|
setPwdOpen(false);
|
|
4546
|
-
|
|
4547
|
-
|
|
4761
|
+
// B2:登录/配对响应现在是精简的(statusPending)→ 合并最小 config 回显,其余静默补
|
|
4762
|
+
if (body && body.ok && body.statusPending) {
|
|
4763
|
+
setSt(function (prev) {
|
|
4764
|
+
var base = prev && typeof prev === "object" ? prev : {};
|
|
4765
|
+
return Object.assign({}, base, { ok: true, config: Object.assign({}, (base && base.config) || {}, body.config || {}) });
|
|
4766
|
+
});
|
|
4767
|
+
refreshQuiet();
|
|
4768
|
+
} else {
|
|
4769
|
+
setSt(body);
|
|
4770
|
+
}
|
|
4771
|
+
setSelfHost(""); setLocalKey("");
|
|
4772
|
+
setMsg(body && body.ok ? "ok" : "err", body && body.ok ? "✅ 已切换到自建服务,后台服务已重启" : ((body && (body.error || (body.body && body.body.error))) || "保存失败"));
|
|
4548
4773
|
})
|
|
4549
4774
|
.catch(function (e) { setMsg("err", "保存失败: " + e.message); })
|
|
4550
4775
|
.finally(function () { setBusy(""); });
|
|
@@ -4918,15 +5143,8 @@ window.__ModuleLoader__.load({
|
|
|
4918
5143
|
// 免费用户这里什么都不显示 —— 把「免费档」当标签挂在脸上,正是要减少的营销露出。
|
|
4919
5144
|
isMember ? h("div", { className: "plan" }, plan === "pro_max" ? "Pro Max" : "PRO") : null
|
|
4920
5145
|
),
|
|
4921
|
-
// 2026-
|
|
4922
|
-
//
|
|
4923
|
-
h("button", {
|
|
4924
|
-
type: "button",
|
|
4925
|
-
className: "dru-user-logout",
|
|
4926
|
-
disabled: busy !== "",
|
|
4927
|
-
title: "退出登录(电脑端后台服务与手机端连接不受影响)",
|
|
4928
|
-
onClick: function () { doLogout(false); }
|
|
4929
|
-
}, "退出登录")
|
|
5146
|
+
// A3(2026-11-07):退出登录统一放到**状态头右侧**(与「登录 / 注册」同一位置),
|
|
5147
|
+
// 账号弹层里不再重复一个同义按钮。
|
|
4930
5148
|
),
|
|
4931
5149
|
// 额度状态:不限量说明 / 剩余额度百分比 / 会员到期日。
|
|
4932
5150
|
// 首屏只说「还剩多少」,不提档位名、不写带宽数值(数值在下面「更多」里)。
|
|
@@ -4934,58 +5152,33 @@ window.__ModuleLoader__.load({
|
|
|
4934
5152
|
h("span", { className: "dru-dot " + (isMember ? "dru-dot-on" : "dru-dot-off") }),
|
|
4935
5153
|
h("span", null, planText)
|
|
4936
5154
|
),
|
|
4937
|
-
|
|
4938
|
-
|
|
5155
|
+
// A2(2026-11-07 业主:账号弹层不要折叠):弹层空间足够 → 所有功能**直接平铺**,
|
|
5156
|
+
// 不再有「更多」折叠体;一屏一个主操作(升级/续费仍是唯一 primary)。
|
|
5157
|
+
h("div", { className: "dru-actions dru-actions-wrap", style: { marginTop: 10 } },
|
|
4939
5158
|
h("button", {
|
|
4940
5159
|
type: "button", className: "dru-btn dru-btn-primary",
|
|
4941
5160
|
style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "",
|
|
4942
5161
|
onClick: openUpgradeAuth
|
|
4943
5162
|
},
|
|
4944
|
-
busy === "upgrade" ? "生成链接中…" : (!isMember ? "
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
h("span", { className: "dru-disclose-caret", "aria-hidden": "true" }, moreOpen ? "▲" : "▼")
|
|
4959
|
-
)
|
|
5163
|
+
busy === "upgrade" ? "生成链接中…" : (!isMember ? "升级 PRO" : source === "trial" ? "转正式 PRO" : "续费会员")),
|
|
5164
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "用户反馈"),
|
|
5165
|
+
invOff ? null : h("button", {
|
|
5166
|
+
type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "",
|
|
5167
|
+
title: invTipRule ? "带 " + invTipRule.n + " 位新用户可得 " + invTipRule.days + " 天 PRO 会员" : "带新用户换会员",
|
|
5168
|
+
onClick: function () { setView("invite"); loadInvite(); }
|
|
5169
|
+
}, "带新用户换会员"),
|
|
5170
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "修改密码"),
|
|
5171
|
+
community && community.qrcode
|
|
5172
|
+
? h("button", {
|
|
5173
|
+
type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "",
|
|
5174
|
+
onClick: function () { setCommOpen(true); }
|
|
5175
|
+
}, "加入交流群")
|
|
5176
|
+
: null
|
|
4960
5177
|
),
|
|
4961
5178
|
// 🎁 邀请引导气泡:紧跟按钮行(不遮挡按钮)。点 ✕ 只做"不再提示",不再改变入口位置。
|
|
4962
5179
|
renderInviteTip(),
|
|
4963
|
-
//
|
|
4964
|
-
|
|
4965
|
-
id: MKT_MORE_BODY_ID,
|
|
4966
|
-
className: "dru-more-body",
|
|
4967
|
-
hidden: !moreOpen
|
|
4968
|
-
},
|
|
4969
|
-
h("div", { className: "dru-more-actions" },
|
|
4970
|
-
invOff ? null : h("button", {
|
|
4971
|
-
type: "button",
|
|
4972
|
-
className: "dru-btn dru-btn-ghost",
|
|
4973
|
-
disabled: busy !== "",
|
|
4974
|
-
title: invTipRule ? "带 " + invTipRule.n + " 位新用户可得 " + invTipRule.days + " 天 PRO 会员" : "带新用户换会员",
|
|
4975
|
-
onClick: function () { setView("invite"); loadInvite(); }
|
|
4976
|
-
}, "🎁 带新用户换会员"),
|
|
4977
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
|
|
4978
|
-
community && community.qrcode
|
|
4979
|
-
? h("button", {
|
|
4980
|
-
type: "button",
|
|
4981
|
-
className: "dru-btn dru-btn-ghost",
|
|
4982
|
-
disabled: busy !== "",
|
|
4983
|
-
onClick: function () { setCommOpen(true); }
|
|
4984
|
-
}, "💬 加入交流群")
|
|
4985
|
-
: null
|
|
4986
|
-
),
|
|
4987
|
-
pwdOpen ? renderResetPwdForm(false) : null
|
|
4988
|
-
),
|
|
5180
|
+
// 修改密码表单直接展开在平铺按钮下方(不再藏在折叠体里)
|
|
5181
|
+
pwdOpen ? renderResetPwdForm(false) : null,
|
|
4989
5182
|
);
|
|
4990
5183
|
}
|
|
4991
5184
|
|
|
@@ -5599,7 +5792,9 @@ window.__ModuleLoader__.load({
|
|
|
5599
5792
|
h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
|
|
5600
5793
|
h("span", null, e.text));
|
|
5601
5794
|
}
|
|
5602
|
-
|
|
5795
|
+
// A1(2026-11-07 业主截图指出):正文里别再重复「远程访问」标题 —— 设置页顶部已有标题,
|
|
5796
|
+
// 弹层标题也已经写了「手机访问」;这里只保留内容卡片。
|
|
5797
|
+
return h("div", { className: "dru-card" }, [
|
|
5603
5798
|
// 【0.7.10】激活引导置顶:二维码链路的**收尾**(见 renderActivationGuide 的长注释)。
|
|
5604
5799
|
// 放在状态 chip 之前 = 「设备连上后第一屏看到的就是拿手机做什么」,而不是先读一堆技术状态。
|
|
5605
5800
|
renderActivationGuide(),
|
|
@@ -5795,8 +5990,9 @@ window.__ModuleLoader__.load({
|
|
|
5795
5990
|
// ═════════════════════════════════════════════════════════════════════
|
|
5796
5991
|
|
|
5797
5992
|
/** 弹层开关:同一时刻只开一个(真实产品里多弹层叠加只会让用户更迷路)。 */
|
|
5798
|
-
function openIAModal(name) {
|
|
5993
|
+
function openIAModal(name, why) {
|
|
5799
5994
|
setMessage(null);
|
|
5995
|
+
setIaLoginWhy(why ? String(why) : "");
|
|
5800
5996
|
setIaModal(name);
|
|
5801
5997
|
if (name === "channel") {
|
|
5802
5998
|
setIaChStep("list");
|
|
@@ -5811,6 +6007,21 @@ window.__ModuleLoader__.load({
|
|
|
5811
6007
|
setImBotsSessionTitle(String(w.sessionTitle || ""));
|
|
5812
6008
|
setImBotsWhyOpen(false);
|
|
5813
6009
|
setImBotsUrl(String(w.url || ""));
|
|
6010
|
+
var d = (b && b.dingtalk) || {};
|
|
6011
|
+
setImDingId(String(d.clientId || ""));
|
|
6012
|
+
setImDingSecret("");
|
|
6013
|
+
setImDingRobot(String(d.robotCode || ""));
|
|
6014
|
+
var f = (b && b.feishu) || {};
|
|
6015
|
+
setImFeishuAppId(String(f.appId || ""));
|
|
6016
|
+
setImFeishuSecret("");
|
|
6017
|
+
setImFeishuRecv(String(f.receiveId || ""));
|
|
6018
|
+
setImFeishuType(String(f.receiveIdType || "open_id"));
|
|
6019
|
+
setImFeishuWizard(false);
|
|
6020
|
+
setImPlatMsg(null);
|
|
6021
|
+
setImPlatErr({});
|
|
6022
|
+
var deps = b && b.status && b.status.deps && b.status.deps.feishu;
|
|
6023
|
+
if (deps) setImDeps(deps);
|
|
6024
|
+
loadImDeps();
|
|
5814
6025
|
});
|
|
5815
6026
|
}
|
|
5816
6027
|
// 数据加载交给下面的 effect(按"弹层是否打开"触发):
|
|
@@ -5819,16 +6030,29 @@ window.__ModuleLoader__.load({
|
|
|
5819
6030
|
}
|
|
5820
6031
|
|
|
5821
6032
|
// 打开弹层时按需拉数据(幂等;失败由既有重试/红字逻辑兜底)。
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
}, [iaModal, loggedIn, akey]);
|
|
5826
|
-
useEffect(function () {
|
|
5827
|
-
if (iaModal !== "devices" || !loggedIn || devSessions !== null) return;
|
|
5828
|
-
try { loadDevices(); } catch (e2) { /* 忽略 */ }
|
|
5829
|
-
}, [iaModal, loggedIn, devSessions]);
|
|
6033
|
+
// 注:这里**不再**额外拉 access-key / 设备列表 —— 面板已有「已登录 home 停留即取码」的
|
|
6034
|
+
// 既有 effect(含 25s 轮换),弹层打开时那份数据本就已经在页面上。重复拉会让"一次登录"
|
|
6035
|
+
// 打两次 /access-key(生产里浪费签发额度,也和既有回归用例的期望冲突)。
|
|
5830
6036
|
function closeIAModal() { setIaModal(null); }
|
|
5831
6037
|
|
|
6038
|
+
/** A4/A5:未登录时不要静默失败 —— 直接弹登录弹层,并说明"登录后才能用手机访问";
|
|
6039
|
+
* 登录成功后自动回到「手机访问」流程继续生成访问码(iaAfterLogin)。 */
|
|
6040
|
+
function openMobileAccess() {
|
|
6041
|
+
if (!loggedIn) {
|
|
6042
|
+
setIaAfterLogin("mobile");
|
|
6043
|
+
openIAModal("account", "登录后才能用手机访问");
|
|
6044
|
+
return;
|
|
6045
|
+
}
|
|
6046
|
+
openIAModal("mobile");
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6049
|
+
// 登录成功回跳:把"刚才想做的事"接上(目前只有手机访问这一个意图)。
|
|
6050
|
+
useEffect(function () {
|
|
6051
|
+
if (!loggedIn || iaAfterLogin !== "mobile") return;
|
|
6052
|
+
setIaAfterLogin("");
|
|
6053
|
+
try { openIAModal("mobile"); } catch (e) { /* 忽略 */ }
|
|
6054
|
+
}, [loggedIn, iaAfterLogin]);
|
|
6055
|
+
|
|
5832
6056
|
/** 连接状态 → pill 语义(圆点 + 文案;不只靠颜色)。 */
|
|
5833
6057
|
function iaConnState() {
|
|
5834
6058
|
if (st === null) return { cls: "", text: "连接中…" };
|
|
@@ -5857,8 +6081,22 @@ window.__ModuleLoader__.load({
|
|
|
5857
6081
|
h("div", { className: "dru-ia-pill " + cs.cls, role: "status", "aria-live": "polite" },
|
|
5858
6082
|
h("span", { className: "dru-ia-pill-dot", "aria-hidden": "true" }),
|
|
5859
6083
|
cs.text),
|
|
5860
|
-
h("
|
|
5861
|
-
|
|
6084
|
+
h("div", { className: "dru-ia-head-actions" },
|
|
6085
|
+
// A3:登录/退出登录**同一位置**(状态头右侧;账号弹层里不再放退出按钮)。
|
|
6086
|
+
// 未登录显示「登录 / 注册」入口,点击弹账号弹层。
|
|
6087
|
+
loggedIn
|
|
6088
|
+
? h("button", {
|
|
6089
|
+
type: "button", className: "dru-ia-btn", disabled: busy !== "",
|
|
6090
|
+
title: "退出登录(电脑端后台服务与手机端连接不受影响)",
|
|
6091
|
+
onClick: function () { doLogout(false); }
|
|
6092
|
+
}, "退出登录")
|
|
6093
|
+
: h("button", {
|
|
6094
|
+
type: "button", className: "dru-ia-btn",
|
|
6095
|
+
onClick: function () { openIAModal("account"); }
|
|
6096
|
+
}, "登录 / 注册"),
|
|
6097
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: openMobileAccess },
|
|
6098
|
+
druIcon("phone"), "打开手机端")
|
|
6099
|
+
)
|
|
5862
6100
|
);
|
|
5863
6101
|
}
|
|
5864
6102
|
|
|
@@ -5882,13 +6120,13 @@ window.__ModuleLoader__.load({
|
|
|
5882
6120
|
var remain = 0;
|
|
5883
6121
|
if (akey) { var expMs = toMs(akey.expires_at); if (expMs) remain = expMs - nowTick; }
|
|
5884
6122
|
var mobileMeta = !loggedIn
|
|
5885
|
-
? "
|
|
6123
|
+
? "登录后即可生成一次性访问码"
|
|
5886
6124
|
: (akey && akey.url && remain > 0 ? ("访问码有效 · " + fmtRemain(remain) + "后过期") : "尚未生成访问码");
|
|
5887
6125
|
|
|
5888
6126
|
return h("div", { className: "dru-ia-grid" },
|
|
5889
6127
|
iaCard("phone", "手机访问", mobileMeta,
|
|
5890
|
-
h("button", { type: "button", className: "dru-ia-btn is-primary", onClick:
|
|
5891
|
-
loggedIn ? (akey && akey.url ? "查看访问码" : "生成访问码") : "
|
|
6128
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: openMobileAccess },
|
|
6129
|
+
loggedIn ? (akey && akey.url ? "查看访问码" : "生成访问码") : "登录并生成访问码")),
|
|
5892
6130
|
iaCard("bell", "通知与消息通道", chMeta,
|
|
5893
6131
|
h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("channel"); } }, "设置")),
|
|
5894
6132
|
iaCard("devices", "已授权设备", devMeta,
|
|
@@ -5918,32 +6156,38 @@ window.__ModuleLoader__.load({
|
|
|
5918
6156
|
|
|
5919
6157
|
/** 后台服务卡(从旧 renderHome 搬出来;文案压缩,不再解释内部实现细节)。 */
|
|
5920
6158
|
function renderBridgeCard() {
|
|
6159
|
+
var autostartText = st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
|
|
6160
|
+
? (st.service.serviceManager === "detached"
|
|
6161
|
+
? (st.service.autostartTask
|
|
6162
|
+
? "自启动:已注册登录任务(Windows 任务计划程序 dsh-remote-bridge)"
|
|
6163
|
+
: "自启动:未注册(由面板在 dsh web 启动时自动拉起)")
|
|
6164
|
+
: "自启动服务已安装")
|
|
6165
|
+
: "自启动服务未安装(启动时自动创建)") : "";
|
|
6166
|
+
var deviceText = st && st.config && st.config.deviceId ? ("设备 ID:" + st.config.deviceId) : "设备 ID:生成中";
|
|
5921
6167
|
return h("div", { className: "dru-card" },
|
|
5922
6168
|
h("h3", null, "后台服务"),
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
6169
|
+
// A6:业主点名要这句提示 —— 显眼但不刺眼(浅色提示条 + 图标,不占满屏、不闪)
|
|
6170
|
+
h("div", { className: "dru-bridge-keep", role: "note" },
|
|
6171
|
+
druIcon("alert", 14),
|
|
6172
|
+
h("span", null, "保持在线,才能用手机端远程控制这台电脑,请不要关闭。")),
|
|
6173
|
+
// A6:横向紧凑布局(2 列网格):状态 / 启停按钮 / 自启动 / 设备 ID 不再一列排队
|
|
6174
|
+
h("div", { className: "dru-bridge-grid" },
|
|
6175
|
+
h("div", { className: "dru-bridge-cell" },
|
|
6176
|
+
h("div", { className: "dru-status-line" },
|
|
6177
|
+
h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
|
|
6178
|
+
h("span", null, st ? serviceStateText : "查询中…"),
|
|
6179
|
+
launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null)),
|
|
6180
|
+
h("div", { className: "dru-bridge-cell dru-bridge-cell-action" },
|
|
6181
|
+
!serviceRunning
|
|
6182
|
+
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
|
|
6183
|
+
: h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")),
|
|
6184
|
+
h("div", { className: "dru-bridge-cell dru-meta" }, autostartText),
|
|
6185
|
+
h("div", { className: "dru-bridge-cell dru-meta" }, deviceText)
|
|
5927
6186
|
),
|
|
5928
6187
|
connInfo && connPhase && connPhase !== "no_account"
|
|
5929
|
-
? h("div", { className: "dru-hint", style: { marginTop:
|
|
6188
|
+
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
5930
6189
|
"连接进度:" + (connInfo.locked ? "本机已被锁定(额度已满,升级或解绑后自动恢复)" : connText))
|
|
5931
6190
|
: null,
|
|
5932
|
-
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
5933
|
-
!serviceRunning
|
|
5934
|
-
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
|
|
5935
|
-
: h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")
|
|
5936
|
-
),
|
|
5937
|
-
// 自启动形态(平台差异要如实说:Windows 用任务计划程序、其它平台用 launchd/systemd 类服务)。
|
|
5938
|
-
h("div", { className: "dru-meta" }, st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
|
|
5939
|
-
? (st.service.serviceManager === "detached"
|
|
5940
|
-
? (st.service.autostartTask
|
|
5941
|
-
? "自启动:已注册登录任务(Windows 任务计划程序 dsh-remote-bridge)"
|
|
5942
|
-
: "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起后台服务)")
|
|
5943
|
-
: "自启动服务已安装")
|
|
5944
|
-
: "自启动服务未安装(启动时自动创建)") : ""),
|
|
5945
|
-
// 设备 ID 属于"客服排查时要报的一行",留在高级设置里(主界面不露)。
|
|
5946
|
-
h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
|
|
5947
6191
|
st && !serviceRuntimeReady
|
|
5948
6192
|
? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "运行环境正在自动补装,装完会自动启动,不需要你操作。")
|
|
5949
6193
|
: serviceCrashing
|
|
@@ -5953,7 +6197,7 @@ window.__ModuleLoader__.load({
|
|
|
5953
6197
|
? (function (be) {
|
|
5954
6198
|
var isLocked = be.code === "device_locked" || be.code === "device_limit_exceeded";
|
|
5955
6199
|
var beLimit = (be.limit === 0 || be.limit) ? be.limit : null;
|
|
5956
|
-
|
|
6200
|
+
var isBindCap = be.code === "device_bind_cap";
|
|
5957
6201
|
return h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
|
|
5958
6202
|
(isLocked ? "本机已被锁定:" : "设备注册失败:") + (be.message || "未说明原因"),
|
|
5959
6203
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
@@ -5973,7 +6217,7 @@ window.__ModuleLoader__.load({
|
|
|
5973
6217
|
renderBridgeCard(),
|
|
5974
6218
|
h(SelfManageCard, null),
|
|
5975
6219
|
h("div", { className: "dru-card" },
|
|
5976
|
-
h("h3", null, "关于 dsh-remote"),
|
|
6220
|
+
h("h3", null, "关于 dsh-remote-web"),
|
|
5977
6221
|
h("div", { className: "dru-hint" }, "用手机或另一台电脑随时接管这台电脑上的 DeepSeek Harness:装上即得专属加密地址,免公网 IP、免内网穿透,全程加密。"),
|
|
5978
6222
|
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
5979
6223
|
"匿名装机统计:只上报「装机 / 连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
|
|
@@ -6099,6 +6343,32 @@ window.__ModuleLoader__.load({
|
|
|
6099
6343
|
);
|
|
6100
6344
|
}
|
|
6101
6345
|
|
|
6346
|
+
/** 企微通道档位条(2026-11-07 桥侧会员判定):取不到 tier 就**不渲染**,绝不猜。 */
|
|
6347
|
+
function iaTierBar(ws) {
|
|
6348
|
+
try {
|
|
6349
|
+
var tier = String((ws && ws.tier) || "");
|
|
6350
|
+
if (tier !== "free" && tier !== "pro" && tier !== "pro_max") return null;
|
|
6351
|
+
if (tier === "free") {
|
|
6352
|
+
var remaining = (typeof ws.remainingMessages === "number" && isFinite(ws.remainingMessages))
|
|
6353
|
+
? ws.remainingMessages : null;
|
|
6354
|
+
var perMonth = Number(ws.messagesPerMonth) > 0 ? Number(ws.messagesPerMonth) : 0;
|
|
6355
|
+
var used = ws.usage && typeof ws.usage.messages === "number" ? ws.usage.messages : null;
|
|
6356
|
+
// 只有「桥明确给了余量」或「每月上限+本月已用」才显示数字;否则不瞎猜。
|
|
6357
|
+
if (remaining === null && perMonth && used !== null) remaining = Math.max(0, perMonth - used);
|
|
6358
|
+
var txt = "免费档";
|
|
6359
|
+
if (remaining !== null) txt += " · 本月剩余 " + remaining + " 条指令";
|
|
6360
|
+
if (perMonth) txt += "(每月 " + perMonth + " 条)";
|
|
6361
|
+
return h("div", { className: "dru-ia-tier", role: "status" },
|
|
6362
|
+
h("span", { className: "dru-ia-tier-name" }, txt),
|
|
6363
|
+
h("a", {
|
|
6364
|
+
className: "dru-ia-tier-link", href: "/app/promo", target: "_blank", rel: "noopener"
|
|
6365
|
+
}, "升级 Pro 不限量"));
|
|
6366
|
+
}
|
|
6367
|
+
return h("div", { className: "dru-ia-tier is-member", role: "status" },
|
|
6368
|
+
h("span", { className: "dru-ia-tier-name" }, "会员档 · 指令不限"));
|
|
6369
|
+
} catch (e) { return null; }
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6102
6372
|
/** 统一的表单字段结构(label + 控件 + 错误位;错误就近显示)。 */
|
|
6103
6373
|
function iaField(label, htmlFor, control, errText, hintText, extraHintId) {
|
|
6104
6374
|
// 同时带 dru-field 语义类名(任务里约定的字段结构名),样式仍由 .dru-ia-field 承担。
|
|
@@ -6172,8 +6442,14 @@ window.__ModuleLoader__.load({
|
|
|
6172
6442
|
sessionOptions.push(h("option", { value: String(it.id) },
|
|
6173
6443
|
iaTruncTitle(it.title) + " · " + iaShortId(it.id)));
|
|
6174
6444
|
}
|
|
6445
|
+
// 免费档:桥会忽略 sessionId(bindingGated=true)→ 下拉置灰 + 「会员功能」徽章 + 悬停说明。
|
|
6446
|
+
var sessionGated = !!ws.bindingGated;
|
|
6447
|
+
var sessionIgnored = sessionGated && !!String(imBotsSessionId || "").trim();
|
|
6175
6448
|
var sessionSelect = h("select", {
|
|
6176
|
-
id: "dru-wecom-session", className: "dru-ia-input", value: String(imBotsSessionId || ""),
|
|
6449
|
+
id: "dru-wecom-session", className: "dru-ia-input", value: sessionGated ? "" : String(imBotsSessionId || ""),
|
|
6450
|
+
disabled: sessionGated ? true : undefined,
|
|
6451
|
+
"aria-disabled": sessionGated ? "true" : undefined,
|
|
6452
|
+
title: sessionGated ? "升级后可固定转接会话" : undefined,
|
|
6177
6453
|
"aria-invalid": fieldErr.session ? "true" : "false",
|
|
6178
6454
|
"aria-describedby": fieldErr.session ? "dru-wecom-session-err" : undefined,
|
|
6179
6455
|
onChange: function (e) {
|
|
@@ -6206,6 +6482,8 @@ window.__ModuleLoader__.load({
|
|
|
6206
6482
|
}, imBotsBusy === "preview" ? "发送中…" : "发送预览卡片")
|
|
6207
6483
|
]
|
|
6208
6484
|
}, { children: [
|
|
6485
|
+
// 2026-11-07:档位条(免费档显示本月剩余指令 + 升级入口;会员档显示不限量;取不到不渲染)
|
|
6486
|
+
iaTierBar(ws),
|
|
6209
6487
|
// 当前实际转发目标:一行说清楚(状态不只靠颜色/图标)
|
|
6210
6488
|
h("div", { className: "dru-ia-state" }, druIcon("chat"), "绑定会话:" + iaBoundSessionText()),
|
|
6211
6489
|
// 没有接收方时如实提示(不拦着点:桥会记日志跳过,用户自己能看到结果)
|
|
@@ -6215,11 +6493,21 @@ window.__ModuleLoader__.load({
|
|
|
6215
6493
|
iaHHMM(ws.lastPreviewCardAt)
|
|
6216
6494
|
? h("div", { className: "dru-ia-hint" }, "上次预览:" + iaHHMM(ws.lastPreviewCardAt))
|
|
6217
6495
|
: null,
|
|
6218
|
-
iaField(
|
|
6496
|
+
iaField(
|
|
6497
|
+
sessionGated
|
|
6498
|
+
? h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
6499
|
+
"转接到哪个会话",
|
|
6500
|
+
h("span", { className: "dru-ia-badge", title: "升级后可固定转接会话" }, "会员功能"))
|
|
6501
|
+
: "转接到哪个会话",
|
|
6502
|
+
"dru-wecom-session", sessionSelect,
|
|
6219
6503
|
fieldErr.session || "",
|
|
6220
|
-
|
|
6221
|
-
?
|
|
6222
|
-
|
|
6504
|
+
sessionGated
|
|
6505
|
+
? (sessionIgnored
|
|
6506
|
+
? "当前跟随活跃会话(免费档不能固定转接:升级后可固定转接会话)。"
|
|
6507
|
+
: "免费档不能固定转接会话:当前跟随活跃会话,升级后可固定。")
|
|
6508
|
+
: (sessions.length
|
|
6509
|
+
? "给机器人发消息时,转发到这个会话;默认跟随当前活跃会话。"
|
|
6510
|
+
: "还没有会话,去创建一个任务后这里会出现。"),
|
|
6223
6511
|
"dru-wecom-session-err"),
|
|
6224
6512
|
iaField("Bot ID", "dru-wecom-bot",
|
|
6225
6513
|
h("input", {
|
|
@@ -6299,6 +6587,283 @@ window.__ModuleLoader__.load({
|
|
|
6299
6587
|
}, "去登录"))] }));
|
|
6300
6588
|
}
|
|
6301
6589
|
|
|
6590
|
+
// ══════════════════════════════════════════════════════════════════════════════════
|
|
6591
|
+
// 钉钉 / 飞书(2026-11-08):与 wecom 并列的两条通道;飞书 SDK 走「按需下载」。
|
|
6592
|
+
// ══════════════════════════════════════════════════════════════════════════════════
|
|
6593
|
+
|
|
6594
|
+
/** 飞书官方文档(配置页顶部固定展示;外链一律 target=_blank + rel=noopener)。 */
|
|
6595
|
+
var FEISHU_DOCS = {
|
|
6596
|
+
createApp: "https://open.feishu.cn/document/develop-an-app/create-an-app",
|
|
6597
|
+
longConn: "https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/event-subscription-configure-/long-connection"
|
|
6598
|
+
};
|
|
6599
|
+
/** 8 步向导(业主/文档要求;顺序坑单独强调,见 renderFeishuStep 的提示)。 */
|
|
6600
|
+
var FEISHU_STEPS = [
|
|
6601
|
+
"创建企业自建应用:飞书开放平台 → 开发者后台 → 创建企业自建应用",
|
|
6602
|
+
"复制 App ID / App Secret(应用详情 → 凭证与基础信息)",
|
|
6603
|
+
"开通机器人能力:应用功能 → 机器人 → 启用",
|
|
6604
|
+
"申请权限:收 p2p / 群@消息,发 im:message:send_as_bot",
|
|
6605
|
+
"事件配置:订阅方式选「长连接」,添加「接收消息 v2.0」",
|
|
6606
|
+
"回调配置:订阅方式选「长连接」,添加 card.action.trigger",
|
|
6607
|
+
"发布版本:版本管理与发布 → 创建版本 → 申请发布(管理员审核)",
|
|
6608
|
+
"回本插件填 App ID / Secret / 接收人并点「保存并连接」"
|
|
6609
|
+
];
|
|
6610
|
+
|
|
6611
|
+
/** GET /dsh-remote/im-deps:飞书 SDK 是否已装 + phase(下载进度轮询用)。 */
|
|
6612
|
+
function loadImDeps() {
|
|
6613
|
+
return api("/dsh-remote/im-deps").then(function (b) {
|
|
6614
|
+
var f = b && b.feishu;
|
|
6615
|
+
if (f) setImDeps(f);
|
|
6616
|
+
return f || null;
|
|
6617
|
+
}).catch(function () { return null; });
|
|
6618
|
+
}
|
|
6619
|
+
|
|
6620
|
+
/** 触发飞书 SDK 下载(后台 npm install;进度靠轮询 im-deps 的 phase)。 */
|
|
6621
|
+
function startFeishuDownload() {
|
|
6622
|
+
if (imPlatBusy) return;
|
|
6623
|
+
setImPlatBusy("deps");
|
|
6624
|
+
setImPlatMsg({ kind: "ok", text: "正在下载依赖(约 41MB,仅装一次)…" });
|
|
6625
|
+
post("/dsh-remote/im-deps/feishu", {}).then(function (b) {
|
|
6626
|
+
if (b && b.deps && b.deps.feishu) setImDeps(b.deps.feishu);
|
|
6627
|
+
loadImDeps();
|
|
6628
|
+
}).catch(function (e) {
|
|
6629
|
+
setImPlatMsg({ kind: "err", text: "下载失败:" + ((e && e.message) || e) + "(可重试)" });
|
|
6630
|
+
}).finally(function () { setImPlatBusy(""); });
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
/** 依赖下载中:每 ~900ms 轮询一次 phase;失败/完成后自动停。 */
|
|
6634
|
+
useEffect(function () {
|
|
6635
|
+
if (iaModal !== "channel" || iaChStep !== "feishu") return undefined;
|
|
6636
|
+
var phase = imDeps && imDeps.phase;
|
|
6637
|
+
if (phase !== "downloading" && phase !== "installing") return undefined;
|
|
6638
|
+
var t = null;
|
|
6639
|
+
try {
|
|
6640
|
+
t = setTimeout(function () { loadImDeps(); }, 900);
|
|
6641
|
+
} catch (e) { /* 忽略 */ }
|
|
6642
|
+
return function () { try { if (t) clearTimeout(t); } catch (e) { /* 忽略 */ } };
|
|
6643
|
+
}, [iaModal, iaChStep, imDeps]);
|
|
6644
|
+
|
|
6645
|
+
/** 钉钉表单提交体(空 secret = 不修改,与 wecom 一致)。 */
|
|
6646
|
+
function imDingPayload() {
|
|
6647
|
+
return {
|
|
6648
|
+
clientId: String(imDingId || "").trim(),
|
|
6649
|
+
clientSecret: String(imDingSecret || ""),
|
|
6650
|
+
robotCode: String(imDingRobot || "").trim()
|
|
6651
|
+
};
|
|
6652
|
+
}
|
|
6653
|
+
/** 飞书表单提交体。 */
|
|
6654
|
+
function imFeishuPayload() {
|
|
6655
|
+
return {
|
|
6656
|
+
appId: String(imFeishuAppId || "").trim(),
|
|
6657
|
+
appSecret: String(imFeishuSecret || ""),
|
|
6658
|
+
receiveId: String(imFeishuRecv || "").trim(),
|
|
6659
|
+
receiveIdType: String(imFeishuType || "open_id")
|
|
6660
|
+
};
|
|
6661
|
+
}
|
|
6662
|
+
function imPlatFieldErrOf(code) {
|
|
6663
|
+
var map = {
|
|
6664
|
+
missing_client_id: "clientId", missing_client_secret: "clientSecret", missing_robot_code: "robotCode",
|
|
6665
|
+
missing_app_id: "appId", missing_app_secret: "appSecret", missing_receive_id: "receiveId",
|
|
6666
|
+
invalid_receive_id_type: "receiveIdType"
|
|
6667
|
+
};
|
|
6668
|
+
return map[String(code || "")] || "";
|
|
6669
|
+
}
|
|
6670
|
+
var IM_PLAT_FIELD_NAMES = {
|
|
6671
|
+
clientId: "Client ID", clientSecret: "Client Secret", robotCode: "Robot Code",
|
|
6672
|
+
appId: "App ID", appSecret: "App Secret", receiveId: "接收人", receiveIdType: "接收对象类型"
|
|
6673
|
+
};
|
|
6674
|
+
|
|
6675
|
+
/**
|
|
6676
|
+
* 保存 / 测试(钉钉·飞书共用)。
|
|
6677
|
+
* @param {"dingtalk"|"feishu"} platform
|
|
6678
|
+
* @param {"save"|"test"} mode save=只保存;test=先保存再连接测试(「保存并连接」)
|
|
6679
|
+
*/
|
|
6680
|
+
function saveImPlatform(platform, mode) {
|
|
6681
|
+
if (imPlatBusy) return;
|
|
6682
|
+
var cfg = platform === "dingtalk" ? imDingPayload() : imFeishuPayload();
|
|
6683
|
+
setImPlatBusy(mode === "test" ? "test" : "save");
|
|
6684
|
+
setImPlatMsg(null);
|
|
6685
|
+
setImPlatErr({});
|
|
6686
|
+
// ⚠️ 必须是 PUT:/dsh-remote/im-bots 在节点半按 method 分发(GET/PUT),POST 会被 405。
|
|
6687
|
+
api("/dsh-remote/im-bots", {
|
|
6688
|
+
method: "PUT", headers: { "content-type": "application/json" },
|
|
6689
|
+
body: JSON.stringify({ platform: platform, config: cfg })
|
|
6690
|
+
}, 15000).then(function () {
|
|
6691
|
+
if (mode !== "test") {
|
|
6692
|
+
setImPlatMsg({ kind: "ok", text: "已保存(配置只在本机,Secret 不会回显)" });
|
|
6693
|
+
return loadImBots();
|
|
6694
|
+
}
|
|
6695
|
+
return post("/dsh-remote/im-bots/test", { platform: platform, config: cfg }, 15000).then(function (r) {
|
|
6696
|
+
if (r && r.connected) setImPlatMsg({ kind: "ok", text: "已连接,消息可以正常收发了" });
|
|
6697
|
+
else setImPlatMsg({ kind: "warn", text: "已保存,但连接还没成功" + (r && r.lastError ? ":" + r.lastError : "(稍后自动重试)") });
|
|
6698
|
+
return loadImBots();
|
|
6699
|
+
});
|
|
6700
|
+
}).catch(function (e) {
|
|
6701
|
+
var body = (e && e.body) || {};
|
|
6702
|
+
var code = String(body.error || "");
|
|
6703
|
+
var field = imPlatFieldErrOf(code);
|
|
6704
|
+
if (field) {
|
|
6705
|
+
var errs = {}; errs[field] = String(body.message || "请填写完整");
|
|
6706
|
+
setImPlatErr(errs);
|
|
6707
|
+
setImPlatMsg({ kind: "err", text: "请补全:" + IM_PLAT_FIELD_NAMES[field] });
|
|
6708
|
+
} else {
|
|
6709
|
+
setImPlatMsg({ kind: "err", text: "保存失败:" + (body.message || (e && e.message) || "未知错误") });
|
|
6710
|
+
}
|
|
6711
|
+
return null;
|
|
6712
|
+
}).finally(function () { setImPlatBusy(""); });
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6715
|
+
/** 平台状态行(连接中 / 已连接 / 失败;文案面向非技术用户)。 */
|
|
6716
|
+
function imPlatStateLine(st) {
|
|
6717
|
+
if (imPlatBusy === "test") return h("div", { className: "dru-ia-state", role: "status" }, h("span", { className: "dru-spin" }), "正在连接…(最多 10 秒)");
|
|
6718
|
+
if (imPlatMsg) return h("div", { className: "dru-ia-state " + (imPlatMsg.kind === "ok" ? "is-ok" : imPlatMsg.kind === "warn" ? "is-warn" : "is-err"), role: "status" }, imPlatMsg.text);
|
|
6719
|
+
if (st && st.connected) return h("div", { className: "dru-ia-state is-ok", role: "status" }, druIcon("check"), "已连接");
|
|
6720
|
+
if (st && st.lastError) return h("div", { className: "dru-ia-state is-err", role: "status" }, "上次连接失败:" + st.lastError);
|
|
6721
|
+
return null;
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
/** 钉钉配置页:Client ID / Client Secret / Robot Code(三个都要)。 */
|
|
6725
|
+
function renderDingtalkStep(close) {
|
|
6726
|
+
var d = (imBots && imBots.dingtalk) || {};
|
|
6727
|
+
var st = imBots && imBots.status && imBots.status.dingtalk;
|
|
6728
|
+
var err = imPlatErr || {};
|
|
6729
|
+
return iaDialog(Object.assign({}, {
|
|
6730
|
+
title: "钉钉机器人",
|
|
6731
|
+
sub: "Client ID / Secret 就是钉钉应用的 AppKey / AppSecret;Robot Code 是机器人编码",
|
|
6732
|
+
onClose: close,
|
|
6733
|
+
foot: [
|
|
6734
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", disabled: imPlatBusy !== "", onClick: function () { saveImPlatform("dingtalk", "test"); } },
|
|
6735
|
+
imPlatBusy === "test" ? "连接中…" : "保存并连接"),
|
|
6736
|
+
h("button", { type: "button", className: "dru-ia-btn", disabled: imPlatBusy !== "", onClick: function () { saveImPlatform("dingtalk", "save"); } },
|
|
6737
|
+
imPlatBusy === "save" ? "保存中…" : "仅保存"),
|
|
6738
|
+
h("button", { type: "button", className: "dru-ia-btn is-ghost", disabled: imPlatBusy !== "", onClick: function () {
|
|
6739
|
+
if (imPlatBusy) return;
|
|
6740
|
+
setImPlatBusy("test"); setImPlatMsg(null);
|
|
6741
|
+
post("/dsh-remote/im-bots/test", { platform: "dingtalk" }, 15000).then(function (r) {
|
|
6742
|
+
setImPlatMsg({ kind: r && r.connected ? "ok" : "warn", text: r && r.connected ? "已连接" : ("连接失败" + (r && r.lastError ? ":" + r.lastError : "(稍后自动重试)")) });
|
|
6743
|
+
}).catch(function (e) { setImPlatMsg({ kind: "err", text: "测试失败:" + ((e && e.message) || e) }); })
|
|
6744
|
+
.finally(function () { setImPlatBusy(""); });
|
|
6745
|
+
} }, "测试")
|
|
6746
|
+
]
|
|
6747
|
+
}, { children: [
|
|
6748
|
+
h("div", { className: "dru-ia-state" }, druIcon("send"), d.hasSecret ? "已保存凭据(Secret 不回显)" : "还没有保存凭据"),
|
|
6749
|
+
iaField("Client ID(= AppKey)", "dru-ding-client", h("input", {
|
|
6750
|
+
id: "dru-ding-client", className: "dru-ia-input", type: "text", value: imDingId, autoComplete: "off", spellCheck: false,
|
|
6751
|
+
placeholder: "钉钉应用 AppKey", "aria-invalid": err.clientId ? "true" : "false",
|
|
6752
|
+
onChange: function (e) { setImDingId(e.target.value); }
|
|
6753
|
+
}), err.clientId || ""),
|
|
6754
|
+
iaField("Client Secret(= AppSecret)", "dru-ding-secret", h("input", {
|
|
6755
|
+
id: "dru-ding-secret", className: "dru-ia-input", type: "password", value: imDingSecret, autoComplete: "new-password", spellCheck: false,
|
|
6756
|
+
placeholder: d.hasSecret ? "已保存(留空表示不修改)" : "钉钉应用 AppSecret", "aria-invalid": err.clientSecret ? "true" : "false",
|
|
6757
|
+
onChange: function (e) { setImDingSecret(e.target.value); }
|
|
6758
|
+
}), err.clientSecret || "", "只保存在本机;已保存的 Secret 永远不回显。"),
|
|
6759
|
+
iaField("Robot Code", "dru-ding-robot", h("input", {
|
|
6760
|
+
id: "dru-ding-robot", className: "dru-ia-input", type: "text", value: imDingRobot, autoComplete: "off", spellCheck: false,
|
|
6761
|
+
placeholder: "机器人编码(开放平台 → 机器人页复制)", "aria-invalid": err.robotCode ? "true" : "false",
|
|
6762
|
+
onChange: function (e) { setImDingRobot(e.target.value); }
|
|
6763
|
+
}), err.robotCode || "", "三个字段都要填:Client ID/Secret 建应用时给,Robot Code 在机器人页。"),
|
|
6764
|
+
imPlatStateLine(st)
|
|
6765
|
+
] }));
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
/** 飞书 SDK 依赖未安装时的那一屏(按需下载,不装也能用其它通道)。 */
|
|
6769
|
+
function renderFeishuDepsGate(close) {
|
|
6770
|
+
var deps = imDeps || {};
|
|
6771
|
+
var running = deps.phase === "downloading" || deps.phase === "installing";
|
|
6772
|
+
return iaDialog(Object.assign({}, {
|
|
6773
|
+
title: "飞书机器人",
|
|
6774
|
+
sub: "首次使用需要先下载依赖(约 41MB,仅装一次)",
|
|
6775
|
+
onClose: close,
|
|
6776
|
+
foot: [
|
|
6777
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", disabled: imPlatBusy !== "" || running, onClick: startFeishuDownload },
|
|
6778
|
+
running ? "下载中…" : (deps.phase === "failed" ? "重试下载" : "下载依赖")),
|
|
6779
|
+
h("button", { type: "button", className: "dru-ia-btn", onClick: function () { setIaChStep("list"); } }, "返回平台列表")
|
|
6780
|
+
]
|
|
6781
|
+
}, { children: [
|
|
6782
|
+
h("div", { className: "dru-ia-state" }, druIcon("bell"),
|
|
6783
|
+
running ? "正在下载并安装(约 41MB,仅装一次)…" : (deps.phase === "failed" ? "上次下载失败" : "还没有安装飞书依赖")),
|
|
6784
|
+
deps.lastError ? h("div", { className: "dru-ia-state is-err", role: "alert" }, deps.lastError) : null,
|
|
6785
|
+
h("div", { className: "dru-ia-hint" }, "下载的是飞书官方 Node SDK,仅装在这个插件自己的目录里,不影响其它插件和系统环境。"),
|
|
6786
|
+
h("div", { className: "dru-ia-hint" }, "不下载也能用其它通道:企业微信、钉钉都可正常配置;飞书随时回来装即可,不会影响已有通知。")
|
|
6787
|
+
] }));
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
/** 飞书配置页(依赖已装):官方文档 + 8 步向导 + 三个字段。 */
|
|
6791
|
+
function renderFeishuStep(close) {
|
|
6792
|
+
var deps = imDeps || {};
|
|
6793
|
+
if (deps.installed !== true) return renderFeishuDepsGate(close);
|
|
6794
|
+
var f = (imBots && imBots.feishu) || {};
|
|
6795
|
+
var st = imBots && imBots.status && imBots.status.feishu;
|
|
6796
|
+
var err = imPlatErr || {};
|
|
6797
|
+
return iaDialog(Object.assign({}, {
|
|
6798
|
+
title: "飞书机器人",
|
|
6799
|
+
sub: "依赖已就绪(v" + (deps.version || f.sdkVersion || "?") + ");填 App ID / Secret / 接收人即可",
|
|
6800
|
+
onClose: close,
|
|
6801
|
+
foot: [
|
|
6802
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", disabled: imPlatBusy !== "", onClick: function () { saveImPlatform("feishu", "test"); } },
|
|
6803
|
+
imPlatBusy === "test" ? "连接中…" : "保存并连接"),
|
|
6804
|
+
h("button", { type: "button", className: "dru-ia-btn", disabled: imPlatBusy !== "", onClick: function () { saveImPlatform("feishu", "save"); } },
|
|
6805
|
+
imPlatBusy === "save" ? "保存中…" : "仅保存"),
|
|
6806
|
+
h("button", { type: "button", className: "dru-ia-btn is-ghost", disabled: imPlatBusy !== "", onClick: function () {
|
|
6807
|
+
if (imPlatBusy) return;
|
|
6808
|
+
setImPlatBusy("test"); setImPlatMsg(null);
|
|
6809
|
+
post("/dsh-remote/im-bots/test", { platform: "feishu" }, 15000).then(function (r) {
|
|
6810
|
+
setImPlatMsg({ kind: r && r.connected ? "ok" : "warn", text: r && r.connected ? "已连接" : ("连接失败" + (r && r.lastError ? ":" + r.lastError : "(稍后自动重试)")) });
|
|
6811
|
+
}).catch(function (e) { setImPlatMsg({ kind: "err", text: "测试失败:" + ((e && e.message) || e) }); })
|
|
6812
|
+
.finally(function () { setImPlatBusy(""); });
|
|
6813
|
+
} }, "测试")
|
|
6814
|
+
]
|
|
6815
|
+
}, { children: [
|
|
6816
|
+
// 官方文档固定放顶部(两个外链)
|
|
6817
|
+
h("div", { className: "dru-ia-hint" },
|
|
6818
|
+
"官方文档:",
|
|
6819
|
+
h("a", { className: "dru-ia-tier-link", href: FEISHU_DOCS.createApp, target: "_blank", rel: "noopener" }, "飞书怎么创建应用"),
|
|
6820
|
+
" · ",
|
|
6821
|
+
h("a", { className: "dru-ia-tier-link", href: FEISHU_DOCS.longConn, target: "_blank", rel: "noopener" }, "长连接事件订阅")),
|
|
6822
|
+
// 8 步向导(折叠;默认收起,避免一屏文字堆砌)
|
|
6823
|
+
h("div", { className: "dru-ia-field" },
|
|
6824
|
+
h("button", {
|
|
6825
|
+
type: "button", className: "dru-ia-disclose", "aria-expanded": imFeishuWizard ? "true" : "false",
|
|
6826
|
+
onClick: function () { setImFeishuWizard(!imFeishuWizard); }
|
|
6827
|
+
}, "配置步骤(8 步)", druIcon(imFeishuWizard ? "left" : "right")),
|
|
6828
|
+
imFeishuWizard
|
|
6829
|
+
? h("div", null,
|
|
6830
|
+
h("ol", { className: "dru-ia-steps" }, FEISHU_STEPS.map(function (t, i) {
|
|
6831
|
+
return h("li", { key: String(i) }, t);
|
|
6832
|
+
})),
|
|
6833
|
+
// 顺序坑(必须写明):事件配置保存时「长连接必须已在线」
|
|
6834
|
+
h("div", { className: "dru-ia-state is-warn", role: "note" },
|
|
6835
|
+
"顺序很重要:保存事件/回调配置时飞书的「长连接必须已在线」——",
|
|
6836
|
+
"所以请**先在第 8 步填凭据让插件连上**,再回飞书后台保存事件配置;顺序反了会保存失败。"))
|
|
6837
|
+
: null),
|
|
6838
|
+
h("div", { className: "dru-ia-state" }, druIcon("send"), f.hasSecret ? "已保存凭据(Secret 不回显)" : "还没有保存凭据"),
|
|
6839
|
+
iaField("App ID", "dru-fs-appid", h("input", {
|
|
6840
|
+
id: "dru-fs-appid", className: "dru-ia-input", type: "text", value: imFeishuAppId, autoComplete: "off", spellCheck: false,
|
|
6841
|
+
placeholder: "cli_xxxxxxxx", "aria-invalid": err.appId ? "true" : "false",
|
|
6842
|
+
onChange: function (e) { setImFeishuAppId(e.target.value); }
|
|
6843
|
+
}), err.appId || ""),
|
|
6844
|
+
iaField("App Secret", "dru-fs-secret", h("input", {
|
|
6845
|
+
id: "dru-fs-secret", className: "dru-ia-input", type: "password", value: imFeishuSecret, autoComplete: "new-password", spellCheck: false,
|
|
6846
|
+
placeholder: f.hasSecret ? "已保存(留空表示不修改)" : "从应用详情 → 凭证与基础信息复制",
|
|
6847
|
+
"aria-invalid": err.appSecret ? "true" : "false",
|
|
6848
|
+
onChange: function (e) { setImFeishuSecret(e.target.value); }
|
|
6849
|
+
}), err.appSecret || "", "只保存在本机;已保存的 Secret 永远不回显。"),
|
|
6850
|
+
iaField("接收人(open_id 或 chat_id)", "dru-fs-recv", h("input", {
|
|
6851
|
+
id: "dru-fs-recv", className: "dru-ia-input", type: "text", value: imFeishuRecv, autoComplete: "off", spellCheck: false,
|
|
6852
|
+
placeholder: "ou_xxxxxxxx 或 oc_xxxxxxxx", "aria-invalid": err.receiveId ? "true" : "false",
|
|
6853
|
+
onChange: function (e) { setImFeishuRecv(e.target.value); }
|
|
6854
|
+
}), err.receiveId || ""),
|
|
6855
|
+
iaField("接收对象类型", "dru-fs-rtype",
|
|
6856
|
+
h("select", {
|
|
6857
|
+
id: "dru-fs-rtype", className: "dru-ia-input", value: imFeishuType,
|
|
6858
|
+
onChange: function (e) { setImFeishuType(e.target.value); }
|
|
6859
|
+
},
|
|
6860
|
+
h("option", { value: "open_id" }, "个人 open_id(ou_ 开头)"),
|
|
6861
|
+
h("option", { value: "chat_id" }, "群 chat_id(oc_ 开头)")),
|
|
6862
|
+
err.receiveIdType || "", "发给个人选 open_id;发到群选 chat_id。"),
|
|
6863
|
+
imPlatStateLine(st)
|
|
6864
|
+
] }));
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6302
6867
|
/** 还没支持的平台:只给**诚实**的空状态(说明 + 反馈入口),绝不放假表单/假入口。 */
|
|
6303
6868
|
function renderComingSoonStep(close, kind) {
|
|
6304
6869
|
var meta = {
|
|
@@ -6345,12 +6910,30 @@ window.__ModuleLoader__.load({
|
|
|
6345
6910
|
if (iaModal !== "channel") return null;
|
|
6346
6911
|
if (iaChStep === "wecom") return renderWecomStep(closeIAModal);
|
|
6347
6912
|
if (iaChStep === "wechat") return renderWechatStep(closeIAModal);
|
|
6348
|
-
if (iaChStep === "
|
|
6913
|
+
if (iaChStep === "dingtalk") return renderDingtalkStep(closeIAModal);
|
|
6914
|
+
if (iaChStep === "feishu") return renderFeishuStep(closeIAModal);
|
|
6915
|
+
if (iaChStep === "webhook") return renderComingSoonStep(closeIAModal, "webhook");
|
|
6349
6916
|
var w = (imBots && imBots.wecom) || {};
|
|
6350
6917
|
var ws = (imBots && imBots.status) || {};
|
|
6351
6918
|
var wecomSub = ws.connected
|
|
6352
6919
|
? "已连接 · 推送消息到企业微信"
|
|
6353
6920
|
: (w.enabled || w.hasSecret ? "已配置,等待连接" : "未配置 · 填 Bot ID 与 Secret 即可");
|
|
6921
|
+
var d = (imBots && imBots.dingtalk) || {};
|
|
6922
|
+
var f = (imBots && imBots.feishu) || {};
|
|
6923
|
+
var dst = (imBots && imBots.status && imBots.status.dingtalk) || {};
|
|
6924
|
+
var fst = (imBots && imBots.status && imBots.status.feishu) || {};
|
|
6925
|
+
var dingSub = dst.connected
|
|
6926
|
+
? "已连接 · 推送消息到钉钉"
|
|
6927
|
+
: (d.enabled || d.hasSecret ? "已配置,等待连接" : "未配置 · 填 Client ID / Secret / Robot Code");
|
|
6928
|
+
var feishuSub = !(imDeps && imDeps.installed)
|
|
6929
|
+
? "需要先下载依赖(约 41MB,仅装一次)"
|
|
6930
|
+
: (fst.connected ? "已连接 · 推送消息到飞书" : (f.enabled || f.hasSecret ? "已配置,等待连接" : "依赖已就绪 · 填 App ID / Secret / 接收人"));
|
|
6931
|
+
var resetPlatStep = function (step) {
|
|
6932
|
+
setIaChStep(step);
|
|
6933
|
+
setImPlatMsg(null);
|
|
6934
|
+
setImPlatErr({});
|
|
6935
|
+
if (step === "feishu") loadImDeps();
|
|
6936
|
+
};
|
|
6354
6937
|
return iaDialog(Object.assign({}, {
|
|
6355
6938
|
title: "通知与消息通道",
|
|
6356
6939
|
sub: "选一个平台,让电脑端在任务完成、出错或需要拍板时主动通知你",
|
|
@@ -6358,10 +6941,10 @@ window.__ModuleLoader__.load({
|
|
|
6358
6941
|
foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "关闭")
|
|
6359
6942
|
}, { children: [h("div", { className: "dru-ia-list" },
|
|
6360
6943
|
iaChannelRow("chat", "企业微信智能机器人", wecomSub, function () { setIaChStep("wecom"); setImBotsMsg(null); setImBotsFieldErr({}); }, "配置"),
|
|
6944
|
+
iaChannelRow("send", "钉钉机器人", dingSub, function () { resetPlatStep("dingtalk"); }, "配置"),
|
|
6945
|
+
iaChannelRow("send", "飞书机器人", feishuSub, function () { resetPlatStep("feishu"); }, imDeps && imDeps.installed ? "配置" : "下载依赖"),
|
|
6361
6946
|
iaChannelRow("chat", "微信机器人(扫码绑定)", "扫码绑定 · 在微信里收通知 / 回数字拍板", function () { setIaChStep("wechat"); }, "打开"),
|
|
6362
|
-
iaChannelRow("
|
|
6363
|
-
iaChannelRow("send", "钉钉机器人", "即将支持 · 需要企业管理员创建应用", function () { setIaChStep("dingtalk"); }, "查看"),
|
|
6364
|
-
iaChannelRow("code", "高级 · Webhook", "高级用法 · 自备公网端点", function () { setIaChStep("webhook"); }, "查看")
|
|
6947
|
+
iaChannelRow("code", "高级 · Webhook", "免费 · 高级用法:自备公网端点", function () { setIaChStep("webhook"); }, "查看")
|
|
6365
6948
|
)] }));
|
|
6366
6949
|
}
|
|
6367
6950
|
|
|
@@ -6403,7 +6986,11 @@ window.__ModuleLoader__.load({
|
|
|
6403
6986
|
sub: "登录后可同步设备、生成访问码;账号只用于你自己的设备之间同步",
|
|
6404
6987
|
onClose: closeIAModal,
|
|
6405
6988
|
foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
|
|
6406
|
-
}, { children: [
|
|
6989
|
+
}, { children: [
|
|
6990
|
+
// A4:从「打开手机端 / 生成访问码」被守卫弹过来时,说明一句"为什么"(不留静默跳转)
|
|
6991
|
+
iaLoginWhy ? h("div", { className: "dru-ia-state is-warn", role: "status" }, iaLoginWhy) : null,
|
|
6992
|
+
renderAccountBody()
|
|
6993
|
+
] }));
|
|
6407
6994
|
}
|
|
6408
6995
|
|
|
6409
6996
|
function renderIAModals() {
|
|
@@ -6694,8 +7281,9 @@ window.__ModuleLoader__.load({
|
|
|
6694
7281
|
h("div", { className: "dru-settings-head" },
|
|
6695
7282
|
h("span", { className: "dru-settings-icon" }, druIcon("phone", 20)),
|
|
6696
7283
|
h("div", null,
|
|
6697
|
-
|
|
6698
|
-
h("
|
|
7284
|
+
// 2026-11-07 业主:「我的插件名字叫 dsh-remote-web,这个是全名」→ 面板标题统一成品牌全名。
|
|
7285
|
+
h("h2", { className: "dru-settings-title" }, "dsh-remote-web"),
|
|
7286
|
+
h("div", { className: "dru-settings-sub" }, "远程访问:通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
|
|
6699
7287
|
h("div", { className: "dru-settings-sub" }, "装上即得专属加密地址:人在外面也能用手机全功能接管这台电脑上的 DeepSeek Harness(免内网穿透、免公网 IP、全程加密)。")
|
|
6700
7288
|
)
|
|
6701
7289
|
),
|