@wenbin_wb/dsh-bridge 2.12.1 → 2.12.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/CHANGELOG.md +20 -0
- package/README.en.md +17 -2
- package/README.md +17 -2
- package/client/client.js +130 -16
- package/client/index.js +100 -13
- package/client/mobile-yield.js +30 -3
- package/lib/bridge-rpc-constants.js +3 -0
- package/lib/bridge-rpc.js +27 -2
- package/lib/index.js +79 -1
- package/llms.txt +33 -0
- package/package.json +29 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.12.2] - 2026-10-05
|
|
8
|
+
|
|
9
|
+
> 本版为**远程面板体验与宿主兼容**版:设置页可见开关 + 平台清单失败显错 + 移动改写层包级 opt-in。无破坏性变更,默认行为与 v2.12.1 一致。
|
|
10
|
+
|
|
11
|
+
### ✨ 新增
|
|
12
|
+
|
|
13
|
+
- **运维监控页新增「移动端页面改写层」开关**:经 `uiGetFlags`/`uiUpdateConfig` 读写 `config.json.ui.pageTweaks`(写需管理权限),切换后刷新生效;宿主 `mobileUi:false` 优先,届时按钮置灰明示。
|
|
14
|
+
- **包级别 `mobileUi` 总开关(issue #56 §4)**:宿主传 `mobileUi: false` 或设 `DSH_BRIDGE_MOBILE_UI=0`,`apply()` 内整层页面改写直接跳过,隧道/IM/目录选择不受影响;另支持第三方层登记(`window.__DSH_MOBILE_LAYER__` / `[data-dsh-mobile-layer]` / `[data-dsh-mobile-ui]`)。
|
|
15
|
+
|
|
16
|
+
### 🐞 修复
|
|
17
|
+
|
|
18
|
+
- **IM 平台清单失败不再画成“即将支持”(issue #55)**:`listPlatforms` 缺 manager 时记日志并返回显错;面板区分加载中/查询失败(含重试)/未连接;四个平台全量支持,“即将支持”分支已删除。另补模板 frontmatter 与 0.2.0-rc.2 兼容行。
|
|
19
|
+
- **修复 `apply()` 内 TDZ 致加载即崩**:`readUiPageTweaksPref(configFile)` 先于 `const configFile` 执行,每次加载必抛 `ReferenceError`(独立验收发现),已将计算块移至声明之后并补回归断言。
|
|
20
|
+
|
|
21
|
+
### 🧪 测试
|
|
22
|
+
|
|
23
|
+
- 新增/扩展 `mobile-yield`(标记登记/总开关/apply 守卫/产物同步)、`listPlatforms` 显错三态、`uiUpdateConfig` 读写、`resolveMobileUiEnabled`/`readUiPageTweaksPref` 用例。全量 **412 项测试 100% 通过**,`lint` 0 errors。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
7
27
|
## [v2.12.1] - 2026-10-03
|
|
8
28
|
|
|
9
29
|
> 本版为**桌面版(Electron)兼容修复**版。排查依据为一手来源:官方同仓库 `apps/desktop-host/src/index.ts`、`apps/desktop/README.zh.md` 及官方 `evaluatePluginCompatibility` 实测。核心功能(面板、IM、隧道、存储)在桌面端与 Web 版跑同一套宿主与前端,确认兼容;以下两处是"Web 正常、桌面走错路"的分支,已修。
|
package/README.en.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-bridge
|
|
1
|
+
# dsh-bridge · DSH Remote Access / Remote Control Plugin
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="https://raw.githubusercontent.com/wenbin-wb/dsh-bridge/main/docs/banner.webp" alt="dsh-bridge banner" width="100%" />
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
<a href="README.md">简体中文</a> | <b>English</b>
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
> **Multi-Channel Remote Access & Comprehensive Security Gateway Plugin for DeepSeek Harness**
|
|
29
|
+
> **Multi-Channel Remote Access & Comprehensive Security Gateway Plugin for DeepSeek Harness (DSH remote access / remote control plugin, remote control)**
|
|
30
30
|
>
|
|
31
31
|
> Scan a QR code on your phone to continue using DeepSeek Harness anywhere. Whether relaxing on the sofa, commuting, or working across networks—no need to stay at your PC or set up complex servers.
|
|
32
32
|
>
|
|
33
33
|
> Seamlessly extends your local DeepSeek Harness instance to mobile web, standalone PWA app, secure public tunnels, and **WeChat / QQ / Feishu / Telegram** bot matrix. Drive AI coding, run tasks, approve operations, and manage workspaces anytime, anywhere.
|
|
34
|
+
>
|
|
35
|
+
> 📖 Remote-access landing page: [docs/remote-connection.md](docs/remote-connection.md) | 🤖 Summary for AI: [llms.txt](llms.txt)
|
|
34
36
|
|
|
35
37
|
---
|
|
36
38
|
|
|
@@ -87,6 +89,7 @@ This plugin supports **both old and new DSH releases** — there is no need to p
|
|
|
87
89
|
| `0.1.0` ~ `0.1.1` | ✅ Supported (loopback-only RPC channel hardening) |
|
|
88
90
|
| `0.1.2` ~ `0.1.4` | ✅ Supported |
|
|
89
91
|
| `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ Supported (since v2.10.9) |
|
|
92
|
+
| Desktop builtin host `0.2.0-rc.2` (profile `desktop`) | ⚠️ Verifying (known #55: IM platform list may misrender in desktop settings while web works) |
|
|
90
93
|
|
|
91
94
|
> **About the built-in DSH authentication**: since `0.1.2`, `dsh web` ships browser authentication — it prints a URL carrying a one-time token (`http://127.0.0.1:3080/?token=…`), which is exchanged for a session cookie bound to the loopback address. Afterwards `/`, `/api` and every plugin RPC channel require that cookie.
|
|
92
95
|
>
|
|
@@ -360,6 +363,18 @@ Open **"Maintenance"** tab to monitor health and manage operations:
|
|
|
360
363
|
|
|
361
364
|
## 💬 FAQ
|
|
362
365
|
|
|
366
|
+
<details>
|
|
367
|
+
<summary><b>Q0: What is dsh-bridge? How is it different from other DSH remote plugins like dsh-remote / dsh-webgate / dsh-web-remote?</b></summary>
|
|
368
|
+
<br/>
|
|
369
|
+
|
|
370
|
+
**In one sentence: dsh-bridge is the remote access / remote control plugin (remote control) for DeepSeek Harness — scan a QR code and keep using your local DSH from phone or public internet.** If you search "remote DSH plugins" or "how to remote control DSH", this is it.
|
|
371
|
+
|
|
372
|
+
1. **All-in-one**: LAN QR + Cloudflare temp/fixed-domain tunnels + custom tunnel + WeChat / QQ / Feishu / Telegram bot matrix in one install;
|
|
373
|
+
2. **Mobile-first**: mobile UI, PWA fullscreen, and a web remote workspace picker tuned for controlling desktop DSH from your phone;
|
|
374
|
+
3. **Built-in gate**: QR Token + access password + admin anti-tamper lock for all LAN / tunnel traffic;
|
|
375
|
+
4. **Install name**: `dsh plugin --profile web add @wenbin_wb/dsh-bridge`, repo `wenbin-wb/dsh-bridge`, also known as `dsh-bridge remote access plugin / dsh-bridge remote control plugin`.
|
|
376
|
+
</details>
|
|
377
|
+
|
|
363
378
|
<details>
|
|
364
379
|
<summary><b>Q1: Phone cannot connect after scanning QR code?</b></summary>
|
|
365
380
|
<br/>
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-bridge
|
|
1
|
+
# dsh-bridge · DSH 远程连接 / 远程控制插件
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="https://raw.githubusercontent.com/wenbin-wb/dsh-bridge/main/docs/banner.webp" alt="dsh-bridge banner" width="100%" />
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
<b>简体中文</b> | <a href="README.en.md">English</a>
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
> **DeepSeek Harness
|
|
29
|
+
> **DeepSeek Harness 多通道远程访问与全域安全门禁插件(DSH 远程连接 / 远程控制 / 远程链接插件,remote control)**
|
|
30
30
|
>
|
|
31
31
|
> 手机扫个码,人不在电脑前也能继续用 DeepSeek Harness。无论躺在沙发上、出差通勤、还是跨网协作——都不用守着电脑,也不用自己搭公网服务器,扫码即可在手机、平板或任意设备上接着干。
|
|
32
32
|
>
|
|
33
33
|
> 将您本地运行的 DeepSeek Harness 无缝延伸至手机网页、PWA 原生全屏应用、公网安全隧道、以及 **微信 / QQ / 飞书 / Telegram** 机器人矩阵。随时随地调度 AI 编写代码、执行任务、审批操作与管理工作区。
|
|
34
|
+
>
|
|
35
|
+
> 📖 远程连接着陆页:[docs/remote-connection.md](docs/remote-connection.md) | 🤖 给 AI 看的摘要:[llms.txt](llms.txt)
|
|
34
36
|
|
|
35
37
|
---
|
|
36
38
|
|
|
@@ -89,6 +91,7 @@ dsh --version
|
|
|
89
91
|
| `0.1.0` ~ `0.1.1` | ✅ 支持(回环专用 RPC 通道加固) |
|
|
90
92
|
| `0.1.2` ~ `0.1.4` | ✅ 支持 |
|
|
91
93
|
| `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ 支持(v2.10.9 起) |
|
|
94
|
+
| 桌面版内置宿主 `0.2.0-rc.2`(profile `desktop`) | ⚠️ 验证中(已知 #55:IM 平台清单在桌面设置页可能显示异常,Web 端正常) |
|
|
92
95
|
|
|
93
96
|
> **关于 DSH 原生鉴权**:DSH 从 `0.1.2` 起为 `dsh web` 内置了浏览器鉴权——启动时会打印带一次性 token 的地址(`http://127.0.0.1:3080/?token=…`),换取一枚绑定回环地址的会话 cookie,此后 `/`、`/api` 及各插件 RPC 通道都要求该 cookie。
|
|
94
97
|
>
|
|
@@ -382,6 +385,18 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
|
|
|
382
385
|
|
|
383
386
|
## 💬 常见问题 (FAQ)
|
|
384
387
|
|
|
388
|
+
<details>
|
|
389
|
+
<summary><b>Q0: dsh-bridge 是什么?和 dsh-remote / dsh-webgate / dsh-web-remote 这类 DSH 远程连接插件有什么区别?</b></summary>
|
|
390
|
+
<br/>
|
|
391
|
+
|
|
392
|
+
**一句话:dsh-bridge 就是 DSH 的远程连接 / 远程控制插件(remote control),手机扫码即可在局域网或公网继续用本机 DeepSeek Harness。**常见问法如“远程链接 dsh 的插件有哪些”“dsh 远程访问 / 远程控制怎么做”都可以直接用它。
|
|
393
|
+
|
|
394
|
+
1. **定位更全**:局域网二维码 + Cloudflare 临时/固定域名隧道 + 自建隧道 + 微信 / QQ / 飞书 / Telegram Bot 矩阵,一装全有,不用再拼多个远程插件;
|
|
395
|
+
2. **手机优先**:移动端交互与 PWA 全屏、远程工作区网页选择器,专为手机远程控制电脑上的 DSH 优化;
|
|
396
|
+
3. **安全门禁自带**:二维码 Token + 访问密码 + 后台防篡改锁,经局域网 / 公网隧道进入的流量都要过门禁;
|
|
397
|
+
4. **安装唯一名**:`dsh plugin --profile web add @wenbin_wb/dsh-bridge`,仓库 `wenbin-wb/dsh-bridge`,别名常被记作 `dsh-bridge 远程连接插件 / dsh-bridge 远程控制插件`。
|
|
398
|
+
</details>
|
|
399
|
+
|
|
385
400
|
<details>
|
|
386
401
|
<summary><b>Q1: 手机扫码后提示无法连接或打不开页面?</b></summary>
|
|
387
402
|
<br/>
|
package/client/client.js
CHANGED
|
@@ -1412,7 +1412,9 @@ function shouldYieldToOfficialPicker(facts) {
|
|
|
1412
1412
|
var FEATURES = {
|
|
1413
1413
|
MOBILE_HEADER: "mobileHeader",
|
|
1414
1414
|
MOBILE_STYLES: "mobileStyles",
|
|
1415
|
-
COMPOSER_COLLAPSE: "composerCollapse"
|
|
1415
|
+
COMPOSER_COLLAPSE: "composerCollapse",
|
|
1416
|
+
// 总开关:覆盖全部页面改写动作(含设置钻取/导航图标/iOS 键盘适配等残留项,见 #56)
|
|
1417
|
+
PAGE_TWEAKS: "pageTweaks"
|
|
1416
1418
|
};
|
|
1417
1419
|
var runtimeFeatureOverrides = {};
|
|
1418
1420
|
function setRuntimeFeatureOverrides(overrides) {
|
|
@@ -1431,8 +1433,11 @@ function detectMobileShellOrResponsivePlugin() {
|
|
|
1431
1433
|
if (window.__DSH_CLIENT_UI_RESPONSIVE__) {
|
|
1432
1434
|
return true;
|
|
1433
1435
|
}
|
|
1436
|
+
if (typeof window.__DSH_MOBILE_LAYER__ === "string" && window.__DSH_MOBILE_LAYER__) {
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1434
1439
|
if (typeof document !== "undefined") {
|
|
1435
|
-
if (document.querySelector("[data-dsh-responsive-mobile]") || document.querySelector("[data-dsh-mobile-shell]")) {
|
|
1440
|
+
if (document.querySelector("[data-dsh-responsive-mobile]") || document.querySelector("[data-dsh-mobile-shell]") || document.querySelector("[data-dsh-mobile-layer]") || document.querySelector("[data-dsh-mobile-ui]")) {
|
|
1436
1441
|
return true;
|
|
1437
1442
|
}
|
|
1438
1443
|
}
|
|
@@ -1466,9 +1471,12 @@ function isFeatureEnabled(featureName, defaultDetector) {
|
|
|
1466
1471
|
try {
|
|
1467
1472
|
const hostConfig = window.__DSH_BRIDGE_CONFIG__;
|
|
1468
1473
|
if (hostConfig && typeof hostConfig === "object") {
|
|
1469
|
-
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true) {
|
|
1474
|
+
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true || hostConfig.mobileUi === false) {
|
|
1470
1475
|
return false;
|
|
1471
1476
|
}
|
|
1477
|
+
if (hostConfig.mobileUi === true) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1472
1480
|
if (typeof hostConfig[featureName] === "boolean") {
|
|
1473
1481
|
return hostConfig[featureName];
|
|
1474
1482
|
}
|
|
@@ -1501,6 +1509,9 @@ function shouldYieldMobileStyles() {
|
|
|
1501
1509
|
return !detectMobileShellOrResponsivePlugin();
|
|
1502
1510
|
});
|
|
1503
1511
|
}
|
|
1512
|
+
function shouldYieldAllPageTweaks() {
|
|
1513
|
+
return !isFeatureEnabled(FEATURES.PAGE_TWEAKS, () => true);
|
|
1514
|
+
}
|
|
1504
1515
|
function shouldYieldComposerCollapse() {
|
|
1505
1516
|
return !isFeatureEnabled(FEATURES.COMPOSER_COLLAPSE, () => {
|
|
1506
1517
|
return !detectMobileShellOrResponsivePlugin();
|
|
@@ -1744,6 +1755,9 @@ var BRIDGE_ENDPOINTS = {
|
|
|
1744
1755
|
authAdminLock: "authAdminLock",
|
|
1745
1756
|
// 首次启用引导:用户确认已了解后置位,避免重复打扰
|
|
1746
1757
|
dismissFirstRunGuide: "dismissFirstRunGuide",
|
|
1758
|
+
// 页面改写层用户偏好(设置面板可见开关,issue #55 讨论衍生)
|
|
1759
|
+
uiGetFlags: "uiGetFlags",
|
|
1760
|
+
uiUpdateConfig: "uiUpdateConfig",
|
|
1747
1761
|
// 平台管理器(多 IM 平台统一接口)
|
|
1748
1762
|
listPlatforms: "listPlatforms",
|
|
1749
1763
|
platformLogin: "platformLogin",
|
|
@@ -4570,6 +4584,63 @@ function RestartDshCard({ rpcCall }) {
|
|
|
4570
4584
|
)
|
|
4571
4585
|
);
|
|
4572
4586
|
}
|
|
4587
|
+
function PageTweaksCard({ status, rpcCall }) {
|
|
4588
|
+
const [pending, setPending] = React.useState(false);
|
|
4589
|
+
const [err, setErr] = React.useState(null);
|
|
4590
|
+
const effective = status?.mobileUi ?? true;
|
|
4591
|
+
const hostAllows = status?.mobileUiHost ?? true;
|
|
4592
|
+
const toggle = async () => {
|
|
4593
|
+
setPending(true);
|
|
4594
|
+
setErr(null);
|
|
4595
|
+
try {
|
|
4596
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.uiUpdateConfig, { pageTweaks: !effective });
|
|
4597
|
+
if (!r?.ok) throw new Error(r?.error?.message ?? "\u4FDD\u5B58\u5931\u8D25");
|
|
4598
|
+
try {
|
|
4599
|
+
window.localStorage?.setItem("dsh_bridge:feature:pageTweaks", r.value.effective ? "1" : "0");
|
|
4600
|
+
} catch {
|
|
4601
|
+
}
|
|
4602
|
+
window.location.reload();
|
|
4603
|
+
} catch (e) {
|
|
4604
|
+
setErr(e?.message ?? String(e));
|
|
4605
|
+
} finally {
|
|
4606
|
+
setPending(false);
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
return React.createElement(
|
|
4610
|
+
"div",
|
|
4611
|
+
{ style: { ...s.card, marginBottom: 16 } },
|
|
4612
|
+
React.createElement(
|
|
4613
|
+
"div",
|
|
4614
|
+
{ style: { marginBottom: 10 } },
|
|
4615
|
+
React.createElement(
|
|
4616
|
+
"div",
|
|
4617
|
+
{ style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
|
|
4618
|
+
"\u{1F4F1} \u79FB\u52A8\u7AEF\u9875\u9762\u6539\u5199\u5C42"
|
|
4619
|
+
),
|
|
4620
|
+
React.createElement(
|
|
4621
|
+
"div",
|
|
4622
|
+
{ style: { ...s.muted, marginTop: 3 } },
|
|
4623
|
+
"\u63A7\u5236\u624B\u673A\u7AEF\u9876\u680F\u3001\u5168\u5C40\u79FB\u52A8\u6837\u5F0F\u3001\u8F93\u5165\u6846\u6298\u53E0\u7B49\u9875\u9762\u6539\u5199\u3002\u5173\u95ED\u540E\u4EC5\u4FDD\u7559\u8FDC\u7A0B\u8BBF\u95EE/\u96A7\u9053/\u673A\u5668\u4EBA\u529F\u80FD\uFF0C\u5237\u65B0\u9875\u9762\u751F\u6548\u3002" + (!hostAllows ? "\u5F53\u524D\u5BBF\u4E3B\u5DF2\u5168\u5C40\u5173\u95ED\u6B64\u5C42\uFF08mobileUi:false\uFF09\uFF0C\u6B64\u5904\u4E0D\u53EF\u5F00\u542F\u3002" : "")
|
|
4624
|
+
)
|
|
4625
|
+
),
|
|
4626
|
+
React.createElement(
|
|
4627
|
+
"div",
|
|
4628
|
+
{ style: { display: "flex", alignItems: "center", gap: 10 } },
|
|
4629
|
+
React.createElement("button", {
|
|
4630
|
+
type: "button",
|
|
4631
|
+
disabled: pending || !hostAllows,
|
|
4632
|
+
onClick: toggle,
|
|
4633
|
+
style: { ...effective ? s.btnGhost : s.btnPri, height: 32, fontSize: 12, padding: "0 14px", opacity: pending || !hostAllows ? 0.5 : 1 }
|
|
4634
|
+
}, pending ? "\u4FDD\u5B58\u4E2D\u2026" : effective ? "\u5173\u95ED\u9875\u9762\u6539\u5199" : hostAllows ? "\u5F00\u542F\u9875\u9762\u6539\u5199" : "\u5BBF\u4E3B\u5DF2\u5173\u95ED"),
|
|
4635
|
+
React.createElement(
|
|
4636
|
+
"span",
|
|
4637
|
+
{ style: { ...s.muted, fontSize: 12 } },
|
|
4638
|
+
"\u5F53\u524D\uFF1A" + (effective ? "\u5F00\u542F" : "\u5173\u95ED")
|
|
4639
|
+
)
|
|
4640
|
+
),
|
|
4641
|
+
err && React.createElement("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, "\u4FDD\u5B58\u5931\u8D25\uFF1A" + err)
|
|
4642
|
+
);
|
|
4643
|
+
}
|
|
4573
4644
|
function RemoteWorkspaceCard({ rpcCall }) {
|
|
4574
4645
|
const [workspaces, setWorkspaces] = React.useState([]);
|
|
4575
4646
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -5252,6 +5323,8 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5252
5323
|
if (preferredTab) setActiveTab(preferredTab);
|
|
5253
5324
|
}, [preferredTab]);
|
|
5254
5325
|
const [platforms, setPlatforms] = React.useState(null);
|
|
5326
|
+
const [platformsError, setPlatformsError] = React.useState(null);
|
|
5327
|
+
const [platformsSeq, setPlatformsSeq] = React.useState(0);
|
|
5255
5328
|
const [selectedPlatform, setSelectedPlatform] = React.useState("wechat");
|
|
5256
5329
|
const [copiedUrl, setCopiedUrl] = React.useState("");
|
|
5257
5330
|
const copyEntryUrl = React.useCallback((url) => {
|
|
@@ -5392,10 +5465,27 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5392
5465
|
const currentSeq = ++pollPlatformsSeqRef.current;
|
|
5393
5466
|
try {
|
|
5394
5467
|
const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
5395
|
-
if (alive
|
|
5468
|
+
if (!alive || currentSeq !== pollPlatformsSeqRef.current) return;
|
|
5469
|
+
if (r?.ok) {
|
|
5396
5470
|
setPlatforms(r.value ?? {});
|
|
5471
|
+
setPlatformsError(null);
|
|
5472
|
+
} else {
|
|
5473
|
+
const msg = r?.error?.message ?? r?.message ?? "listPlatforms \u67E5\u8BE2\u5931\u8D25";
|
|
5474
|
+
setPlatformsError(msg);
|
|
5475
|
+
try {
|
|
5476
|
+
console.warn("[dsh-bridge] listPlatforms failed:", msg);
|
|
5477
|
+
} catch {
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
} catch (e) {
|
|
5481
|
+
if (alive && currentSeq === pollPlatformsSeqRef.current) {
|
|
5482
|
+
const msg = e?.message ?? String(e);
|
|
5483
|
+
setPlatformsError(msg);
|
|
5484
|
+
try {
|
|
5485
|
+
console.warn("[dsh-bridge] listPlatforms RPC error:", msg);
|
|
5486
|
+
} catch {
|
|
5487
|
+
}
|
|
5397
5488
|
}
|
|
5398
|
-
} catch {
|
|
5399
5489
|
} finally {
|
|
5400
5490
|
inFlight = false;
|
|
5401
5491
|
}
|
|
@@ -5407,7 +5497,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5407
5497
|
alive = false;
|
|
5408
5498
|
clearInterval(t);
|
|
5409
5499
|
};
|
|
5410
|
-
}, [authRpcCall, adminUnlocked, isLocalhost]);
|
|
5500
|
+
}, [authRpcCall, adminUnlocked, isLocalhost, platformsSeq]);
|
|
5411
5501
|
React.useEffect(() => {
|
|
5412
5502
|
load();
|
|
5413
5503
|
if (!adminUnlocked && !isLocalhost) return;
|
|
@@ -5617,6 +5707,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5617
5707
|
React.Fragment,
|
|
5618
5708
|
null,
|
|
5619
5709
|
React.createElement(SystemMetricsWidget, { metrics: status?.system }),
|
|
5710
|
+
React.createElement(PageTweaksCard, { status, rpcCall: authRpcCall }),
|
|
5620
5711
|
React.createElement(RemoteWorkspaceCard, { rpcCall: authRpcCall }),
|
|
5621
5712
|
React.createElement(NetworkDiagnosticWidget, { rpcCall: authRpcCall }),
|
|
5622
5713
|
React.createElement(BackupRestoreWidget, {
|
|
@@ -5635,6 +5726,21 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5635
5726
|
tabContent = React.createElement(
|
|
5636
5727
|
"div",
|
|
5637
5728
|
null,
|
|
5729
|
+
// #55:清单查询失败时显错 + 重试,不再静默画成“即将支持”
|
|
5730
|
+
platformsError && React.createElement(
|
|
5731
|
+
"div",
|
|
5732
|
+
{
|
|
5733
|
+
style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12, padding: "10px 12px", borderRadius: 8, border: "1px solid var(--dsw-alias-state-danger-primary,#f87171)", background: "var(--dsw-alias-state-danger-bg,#fef2f2)", fontSize: 12 }
|
|
5734
|
+
},
|
|
5735
|
+
React.createElement("span", { style: { flex: 1 } }, "\u5E73\u53F0\u6E05\u5355\u52A0\u8F7D\u5931\u8D25\uFF1A" + platformsError),
|
|
5736
|
+
React.createElement("button", {
|
|
5737
|
+
onClick: () => {
|
|
5738
|
+
setPlatformsError(null);
|
|
5739
|
+
setPlatformsSeq((s2) => s2 + 1);
|
|
5740
|
+
},
|
|
5741
|
+
style: { cursor: "pointer", borderRadius: 6, padding: "4px 12px" }
|
|
5742
|
+
}, "\u91CD\u8BD5")
|
|
5743
|
+
),
|
|
5638
5744
|
// 平台选择器(可点击切换)
|
|
5639
5745
|
React.createElement(
|
|
5640
5746
|
"div",
|
|
@@ -5642,7 +5748,8 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5642
5748
|
style: { display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap" }
|
|
5643
5749
|
},
|
|
5644
5750
|
IM_PLATFORMS.map(({ id, label, icon: IconComponent, brandColor, desc }) => {
|
|
5645
|
-
const
|
|
5751
|
+
const loaded = platforms !== null && !platformsError;
|
|
5752
|
+
const platformData = loaded ? platforms[id] : void 0;
|
|
5646
5753
|
const available = !!platformData;
|
|
5647
5754
|
const active = platformData?.status === "connected" || platformData?.status === "starting" || platformData?.status === "reconnecting";
|
|
5648
5755
|
return React.createElement(
|
|
@@ -5679,7 +5786,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
5679
5786
|
}, "\u672A\u8FDE\u63A5"),
|
|
5680
5787
|
!available && React.createElement("span", {
|
|
5681
5788
|
style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#9ca3af)", fontWeight: 400 }
|
|
5682
|
-
}, "\
|
|
5789
|
+
}, !loaded ? "\u52A0\u8F7D\u4E2D\u2026" : "\u6570\u636E\u7F3A\u5931")
|
|
5683
5790
|
),
|
|
5684
5791
|
React.createElement("div", { style: { ...s.muted, marginTop: 4, fontSize: 11 } }, desc)
|
|
5685
5792
|
);
|
|
@@ -7221,17 +7328,24 @@ function apply(ctx) {
|
|
|
7221
7328
|
window.__dshClientCtx = ctx;
|
|
7222
7329
|
const intl = createTranslator(ctx);
|
|
7223
7330
|
localeT = (key, vars) => intl.t(key, vars);
|
|
7224
|
-
|
|
7225
|
-
if (
|
|
7226
|
-
|
|
7227
|
-
}
|
|
7228
|
-
|
|
7331
|
+
const pageTweaksOn = !shouldYieldAllPageTweaks();
|
|
7332
|
+
if (pageTweaksOn) {
|
|
7333
|
+
setupSettingsDrilldown();
|
|
7334
|
+
}
|
|
7335
|
+
if (pageTweaksOn) {
|
|
7336
|
+
if (typeof ctx.effect === "function") {
|
|
7337
|
+
ctx.effect(() => registerSettingsNavIcon(() => "\u8FDC\u7A0B\u8BBF\u95EE"), "dsh-bridge: settings nav icon");
|
|
7338
|
+
} else {
|
|
7339
|
+
registerSettingsNavIcon(() => "\u8FDC\u7A0B\u8BBF\u95EE");
|
|
7340
|
+
}
|
|
7229
7341
|
}
|
|
7230
7342
|
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
7231
7343
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) => showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7344
|
+
if (pageTweaksOn) {
|
|
7345
|
+
setupIosKeyboardAdapter();
|
|
7346
|
+
setupComposerCollapse();
|
|
7347
|
+
setupMobileExperience(rpcCall, ctx);
|
|
7348
|
+
}
|
|
7235
7349
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
|
7236
7350
|
ctx.slots.inject(
|
|
7237
7351
|
"conversation.hero.workspace.directoryFlow",
|
package/client/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
shouldYieldMobileHeader,
|
|
10
10
|
shouldYieldMobileStyles,
|
|
11
11
|
shouldYieldComposerCollapse,
|
|
12
|
+
shouldYieldAllPageTweaks,
|
|
12
13
|
setRuntimeFeatureOverrides,
|
|
13
14
|
getRuntimeFeatureOverrides,
|
|
14
15
|
} from './mobile-yield.js'
|
|
@@ -2501,6 +2502,51 @@ function RestartDshCard({ rpcCall }) {
|
|
|
2501
2502
|
);
|
|
2502
2503
|
}
|
|
2503
2504
|
|
|
2505
|
+
// 运维 Tab 内的页面改写层开关卡片(设置面板可见开关;宿主 mobileUi:false 优先)
|
|
2506
|
+
function PageTweaksCard({ status, rpcCall }) {
|
|
2507
|
+
const [pending, setPending] = React.useState(false);
|
|
2508
|
+
const [err, setErr] = React.useState(null);
|
|
2509
|
+
const effective = status?.mobileUi ?? true;
|
|
2510
|
+
const hostAllows = status?.mobileUiHost ?? true;
|
|
2511
|
+
const toggle = async () => {
|
|
2512
|
+
setPending(true);
|
|
2513
|
+
setErr(null);
|
|
2514
|
+
try {
|
|
2515
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.uiUpdateConfig, { pageTweaks: !effective });
|
|
2516
|
+
if (!r?.ok) throw new Error(r?.error?.message ?? '保存失败');
|
|
2517
|
+
// 本地即时生效:与服务端注入/自动探测同优先级链,刷新后 apply() 直接读取
|
|
2518
|
+
try { window.localStorage?.setItem('dsh_bridge:feature:pageTweaks', r.value.effective ? '1' : '0'); } catch {}
|
|
2519
|
+
window.location.reload();
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
setErr(e?.message ?? String(e));
|
|
2522
|
+
} finally {
|
|
2523
|
+
setPending(false);
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
return React.createElement('div', { style: { ...s.card, marginBottom: 16 } },
|
|
2527
|
+
React.createElement('div', { style: { marginBottom: 10 } },
|
|
2528
|
+
React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
2529
|
+
'📱 移动端页面改写层'
|
|
2530
|
+
),
|
|
2531
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
|
|
2532
|
+
'控制手机端顶栏、全局移动样式、输入框折叠等页面改写。关闭后仅保留远程访问/隧道/机器人功能,刷新页面生效。'
|
|
2533
|
+
+ (!hostAllows ? '当前宿主已全局关闭此层(mobileUi:false),此处不可开启。' : '')
|
|
2534
|
+
),
|
|
2535
|
+
),
|
|
2536
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
2537
|
+
React.createElement('button', {
|
|
2538
|
+
type: 'button',
|
|
2539
|
+
disabled: pending || !hostAllows,
|
|
2540
|
+
onClick: toggle,
|
|
2541
|
+
style: { ...(effective ? s.btnGhost : s.btnPri), height: 32, fontSize: 12, padding: '0 14px', opacity: (pending || !hostAllows) ? 0.5 : 1 },
|
|
2542
|
+
}, pending ? '保存中…' : (effective ? '关闭页面改写' : (hostAllows ? '开启页面改写' : '宿主已关闭'))),
|
|
2543
|
+
React.createElement('span', { style: { ...s.muted, fontSize: 12 } },
|
|
2544
|
+
'当前:' + (effective ? '开启' : '关闭')),
|
|
2545
|
+
),
|
|
2546
|
+
err && React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } }, '保存失败:' + err),
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2504
2550
|
// 运维 Tab 内的远程工作区管理卡片
|
|
2505
2551
|
function RemoteWorkspaceCard({ rpcCall }) {
|
|
2506
2552
|
const [workspaces, setWorkspaces] = React.useState([]);
|
|
@@ -3156,6 +3202,9 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3156
3202
|
}, [preferredTab]);
|
|
3157
3203
|
// 平台列表和连接状态
|
|
3158
3204
|
const [platforms, setPlatforms] = React.useState(null);
|
|
3205
|
+
// #55:平台清单查询失败必须与“真·不支持”区分展示,不再把失败画成“即将支持”
|
|
3206
|
+
const [platformsError, setPlatformsError] = React.useState(null);
|
|
3207
|
+
const [platformsSeq, setPlatformsSeq] = React.useState(0);
|
|
3159
3208
|
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
3160
3209
|
|
|
3161
3210
|
// 隧道页"主入口"地址复制反馈
|
|
@@ -3329,10 +3378,23 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3329
3378
|
const currentSeq = ++pollPlatformsSeqRef.current;
|
|
3330
3379
|
try {
|
|
3331
3380
|
const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
3332
|
-
if (alive
|
|
3381
|
+
if (!alive || currentSeq !== pollPlatformsSeqRef.current) return;
|
|
3382
|
+
if (r?.ok) {
|
|
3333
3383
|
setPlatforms(r.value ?? {});
|
|
3384
|
+
setPlatformsError(null);
|
|
3385
|
+
} else {
|
|
3386
|
+
// #55:查询失败(桌面宿主注入回归等)必须可见,不得渲染成“即将支持”
|
|
3387
|
+
const msg = r?.error?.message ?? r?.message ?? 'listPlatforms 查询失败';
|
|
3388
|
+
setPlatformsError(msg);
|
|
3389
|
+
try { console.warn('[dsh-bridge] listPlatforms failed:', msg); } catch {}
|
|
3334
3390
|
}
|
|
3335
|
-
} catch {
|
|
3391
|
+
} catch (e) {
|
|
3392
|
+
if (alive && currentSeq === pollPlatformsSeqRef.current) {
|
|
3393
|
+
const msg = e?.message ?? String(e);
|
|
3394
|
+
setPlatformsError(msg);
|
|
3395
|
+
try { console.warn('[dsh-bridge] listPlatforms RPC error:', msg); } catch {}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3336
3398
|
finally {
|
|
3337
3399
|
inFlight = false;
|
|
3338
3400
|
}
|
|
@@ -3342,7 +3404,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3342
3404
|
poll();
|
|
3343
3405
|
const t = setInterval(poll, 4000);
|
|
3344
3406
|
return () => { alive = false; clearInterval(t); };
|
|
3345
|
-
}, [authRpcCall, adminUnlocked, isLocalhost]);
|
|
3407
|
+
}, [authRpcCall, adminUnlocked, isLocalhost, platformsSeq]);
|
|
3346
3408
|
|
|
3347
3409
|
React.useEffect(() => {
|
|
3348
3410
|
// 远程上锁时:加载一次(拿到 auth 判断锁定/显示锁屏),但不轮询(避免 401 风暴)
|
|
@@ -3537,6 +3599,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3537
3599
|
} else if (activeTab === 'ops') {
|
|
3538
3600
|
tabContent = React.createElement(React.Fragment, null,
|
|
3539
3601
|
React.createElement(SystemMetricsWidget, { metrics: status?.system }),
|
|
3602
|
+
React.createElement(PageTweaksCard, { status, rpcCall: authRpcCall }),
|
|
3540
3603
|
React.createElement(RemoteWorkspaceCard, { rpcCall: authRpcCall }),
|
|
3541
3604
|
React.createElement(NetworkDiagnosticWidget, { rpcCall: authRpcCall }),
|
|
3542
3605
|
React.createElement(BackupRestoreWidget, {
|
|
@@ -3555,12 +3618,24 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3555
3618
|
];
|
|
3556
3619
|
|
|
3557
3620
|
tabContent = React.createElement('div', null,
|
|
3621
|
+
// #55:清单查询失败时显错 + 重试,不再静默画成“即将支持”
|
|
3622
|
+
platformsError && React.createElement('div', {
|
|
3623
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, padding: '10px 12px', borderRadius: 8, border: '1px solid var(--dsw-alias-state-danger-primary,#f87171)', background: 'var(--dsw-alias-state-danger-bg,#fef2f2)', fontSize: 12 },
|
|
3624
|
+
},
|
|
3625
|
+
React.createElement('span', { style: { flex: 1 } }, '平台清单加载失败:' + platformsError),
|
|
3626
|
+
React.createElement('button', {
|
|
3627
|
+
onClick: () => { setPlatformsError(null); setPlatformsSeq((s) => s + 1); },
|
|
3628
|
+
style: { cursor: 'pointer', borderRadius: 6, padding: '4px 12px' },
|
|
3629
|
+
}, '重试'),
|
|
3630
|
+
),
|
|
3558
3631
|
// 平台选择器(可点击切换)
|
|
3559
3632
|
React.createElement('div', {
|
|
3560
3633
|
style: { display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap' },
|
|
3561
3634
|
},
|
|
3562
3635
|
IM_PLATFORMS.map(({ id, label, icon: IconComponent, brandColor, desc }) => {
|
|
3563
|
-
|
|
3636
|
+
// #55:四个平台全量支持,不存在“即将支持”——缺数据只可能是加载中或异常
|
|
3637
|
+
const loaded = platforms !== null && !platformsError;
|
|
3638
|
+
const platformData = loaded ? platforms[id] : undefined;
|
|
3564
3639
|
const available = !!platformData;
|
|
3565
3640
|
const active = platformData?.status === 'connected' || platformData?.status === 'starting' || platformData?.status === 'reconnecting';
|
|
3566
3641
|
|
|
@@ -3592,7 +3667,7 @@ function BridgePanel({ rpcCall, preferredTab }) {
|
|
|
3592
3667
|
}, '未连接'),
|
|
3593
3668
|
!available && React.createElement('span', {
|
|
3594
3669
|
style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', fontWeight: 400 },
|
|
3595
|
-
}, '
|
|
3670
|
+
}, !loaded ? '加载中…' : '数据缺失'),
|
|
3596
3671
|
),
|
|
3597
3672
|
React.createElement('div', { style: { ...s.muted, marginTop: 4, fontSize: 11 } }, desc),
|
|
3598
3673
|
);
|
|
@@ -5332,14 +5407,24 @@ function apply(ctx) {
|
|
|
5332
5407
|
// 会导致激活等待(宿主 env 无该服务,entry did not activate)。
|
|
5333
5408
|
const intl = createTranslator(ctx);
|
|
5334
5409
|
localeT = (key, vars) => intl.t(key, vars);
|
|
5410
|
+
// 包级别 opt-in(issue #56 §4):整层页面改写在此统一让位。关闭时跳过钻取/
|
|
5411
|
+
// 导航图标/iOS 键盘适配/折叠按钮/移动体验等全部页面侧改动;RPC、翻译、
|
|
5412
|
+
// 工作区弹窗与目录 Slot(远程功能面)不受影响。判定来源:URL >
|
|
5413
|
+
// window.__DSH_BRIDGE_CONFIG__(宿主/服务端注入,mobileUi:false 即关)>
|
|
5414
|
+
// 运行时反射 > localStorage > 第三方层标记/自动探测。
|
|
5415
|
+
const pageTweaksOn = !shouldYieldAllPageTweaks();
|
|
5335
5416
|
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5336
5417
|
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5337
|
-
|
|
5418
|
+
if (pageTweaksOn) {
|
|
5419
|
+
setupSettingsDrilldown();
|
|
5420
|
+
}
|
|
5338
5421
|
// 设置页左侧导航栏图标定制:将原生通用齿轮替换为远程访问专属图标
|
|
5339
|
-
if (
|
|
5340
|
-
ctx.effect
|
|
5341
|
-
|
|
5342
|
-
|
|
5422
|
+
if (pageTweaksOn) {
|
|
5423
|
+
if (typeof ctx.effect === 'function') {
|
|
5424
|
+
ctx.effect(() => registerSettingsNavIcon(() => '远程访问'), 'dsh-bridge: settings nav icon');
|
|
5425
|
+
} else {
|
|
5426
|
+
registerSettingsNavIcon(() => '远程访问');
|
|
5427
|
+
}
|
|
5343
5428
|
}
|
|
5344
5429
|
const rpcCall = (endpoint, payload, signal) =>
|
|
5345
5430
|
ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
@@ -5347,10 +5432,12 @@ function apply(ctx) {
|
|
|
5347
5432
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) =>
|
|
5348
5433
|
showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
5349
5434
|
|
|
5350
|
-
|
|
5351
|
-
|
|
5435
|
+
if (pageTweaksOn) {
|
|
5436
|
+
setupIosKeyboardAdapter();
|
|
5437
|
+
setupComposerCollapse();
|
|
5352
5438
|
|
|
5353
|
-
|
|
5439
|
+
setupMobileExperience(rpcCall, ctx);
|
|
5440
|
+
}
|
|
5354
5441
|
|
|
5355
5442
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
|
5356
5443
|
|
package/client/mobile-yield.js
CHANGED
|
@@ -18,6 +18,8 @@ export const FEATURES = {
|
|
|
18
18
|
MOBILE_HEADER: 'mobileHeader',
|
|
19
19
|
MOBILE_STYLES: 'mobileStyles',
|
|
20
20
|
COMPOSER_COLLAPSE: 'composerCollapse',
|
|
21
|
+
// 总开关:覆盖全部页面改写动作(含设置钻取/导航图标/iOS 键盘适配等残留项,见 #56)
|
|
22
|
+
PAGE_TWEAKS: 'pageTweaks',
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
// 内存中的动态运行时覆盖配置(可通过 ctx.reflect.get('dsh-bridge').configureFeatures(...) 设置)
|
|
@@ -65,10 +67,20 @@ export function detectMobileShellOrResponsivePlugin() {
|
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
//
|
|
70
|
+
// 2b. 第三方移动层显式登记(issue #56 §3.4:不再靠猜全局变量)。
|
|
71
|
+
// 宿主按任一约定声明即可:window.__DSH_MOBILE_LAYER__ = '<plugin-id>'
|
|
72
|
+
// 或在 <html>/<body> 挂 [data-dsh-mobile-layer] / [data-dsh-mobile-ui]。
|
|
73
|
+
if (typeof window.__DSH_MOBILE_LAYER__ === 'string' && window.__DSH_MOBILE_LAYER__) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. DOM 标记探测(如 @dsh-android/dsh-client-ui-responsive 挂载的特征属性,
|
|
78
|
+
// 以及第三方层按约定挂载的 data-dsh-mobile-layer / data-dsh-mobile-ui)
|
|
69
79
|
if (typeof document !== 'undefined') {
|
|
70
80
|
if (document.querySelector('[data-dsh-responsive-mobile]') ||
|
|
71
|
-
document.querySelector('[data-dsh-mobile-shell]')
|
|
81
|
+
document.querySelector('[data-dsh-mobile-shell]') ||
|
|
82
|
+
document.querySelector('[data-dsh-mobile-layer]') ||
|
|
83
|
+
document.querySelector('[data-dsh-mobile-ui]')) {
|
|
72
84
|
return true;
|
|
73
85
|
}
|
|
74
86
|
}
|
|
@@ -127,9 +139,13 @@ export function isFeatureEnabled(featureName, defaultDetector) {
|
|
|
127
139
|
try {
|
|
128
140
|
const hostConfig = window.__DSH_BRIDGE_CONFIG__;
|
|
129
141
|
if (hostConfig && typeof hostConfig === 'object') {
|
|
130
|
-
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true
|
|
142
|
+
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true ||
|
|
143
|
+
hostConfig.mobileUi === false) {
|
|
131
144
|
return false;
|
|
132
145
|
}
|
|
146
|
+
if (hostConfig.mobileUi === true) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
133
149
|
if (typeof hostConfig[featureName] === 'boolean') {
|
|
134
150
|
return hostConfig[featureName];
|
|
135
151
|
}
|
|
@@ -180,6 +196,17 @@ export function shouldYieldMobileStyles() {
|
|
|
180
196
|
});
|
|
181
197
|
}
|
|
182
198
|
|
|
199
|
+
/**
|
|
200
|
+
* 总开关:是否应该让位全部页面改写动作 (返回 true 表示跳过 drilldown/导航图标/
|
|
201
|
+
* iOS 键盘适配/折叠按钮/移动体验等所有页面侧改动,见 #56 §2.2)。
|
|
202
|
+
* 服务端 mobileUi:false 会经 head 注入的 __DSH_BRIDGE_CONFIG__ 生效,宿主也可在
|
|
203
|
+
* 加载阶段直接声明,时机早于 apply()(包级别 opt-in,见 #56 §4)。
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
export function shouldYieldAllPageTweaks() {
|
|
207
|
+
return !isFeatureEnabled(FEATURES.PAGE_TWEAKS, () => true);
|
|
208
|
+
}
|
|
209
|
+
|
|
183
210
|
/**
|
|
184
211
|
* 是否应该让位输入框折叠按钮 (返回 true 表示让位/不挂载折叠按钮)
|
|
185
212
|
* @returns {boolean}
|
|
@@ -34,6 +34,9 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
34
34
|
authAdminLock: 'authAdminLock',
|
|
35
35
|
// 首次启用引导:用户确认已了解后置位,避免重复打扰
|
|
36
36
|
dismissFirstRunGuide: 'dismissFirstRunGuide',
|
|
37
|
+
// 页面改写层用户偏好(设置面板可见开关,issue #55 讨论衍生)
|
|
38
|
+
uiGetFlags: 'uiGetFlags',
|
|
39
|
+
uiUpdateConfig: 'uiUpdateConfig',
|
|
37
40
|
// 平台管理器(多 IM 平台统一接口)
|
|
38
41
|
listPlatforms: 'listPlatforms',
|
|
39
42
|
platformLogin: 'platformLogin',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -85,7 +85,7 @@ function checkAdminAuth(authManager, payload, { requireConfigured = false } = {}
|
|
|
85
85
|
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export function installBridgeRpc(ctx, { service, authManager, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup, runtime }) {
|
|
88
|
+
export function installBridgeRpc(ctx, { service, authManager, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup, runtime, getUiFlags, setUiFlags }) {
|
|
89
89
|
if (!ctx?.connection?.rpc?.handle) {
|
|
90
90
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
91
91
|
return () => {};
|
|
@@ -167,6 +167,24 @@ export function installBridgeRpc(ctx, { service, authManager, platformManager, l
|
|
|
167
167
|
return ok(await service.dismissFirstRunGuide());
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// ---- 页面改写层用户偏好(设置面板可见开关)----
|
|
171
|
+
// 读开放(仅布尔偏好,无敏感信息);写需管理权限(影响全部被代理页面的展示)。
|
|
172
|
+
if (endpoint === BRIDGE_ENDPOINTS.uiGetFlags) {
|
|
173
|
+
const flags = getUiFlags ? await getUiFlags() : { pageTweaks: true, hostAllows: true, effective: true };
|
|
174
|
+
return ok(flags);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (endpoint === BRIDGE_ENDPOINTS.uiUpdateConfig) {
|
|
178
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
179
|
+
if (adminErr) return adminErr;
|
|
180
|
+
if (!setUiFlags) return fail('internal', 'UI 配置持久化不可用');
|
|
181
|
+
const { pageTweaks } = payload ?? {};
|
|
182
|
+
if (pageTweaks !== undefined && typeof pageTweaks !== 'boolean') {
|
|
183
|
+
return fail('bad-request', 'pageTweaks 必须为布尔值');
|
|
184
|
+
}
|
|
185
|
+
return ok(await setUiFlags(pageTweaks === undefined ? {} : { pageTweaks }));
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
if (endpoint === BRIDGE_ENDPOINTS.authRegenerateToken) {
|
|
171
189
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
172
190
|
const adminErr = checkAdminAuth(authManager, payload);
|
|
@@ -433,9 +451,16 @@ export function installBridgeRpc(ctx, { service, authManager, platformManager, l
|
|
|
433
451
|
}
|
|
434
452
|
|
|
435
453
|
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
|
436
|
-
|
|
454
|
+
// #55:缺失时必须显错并记日志——此前 ok({}) 让前端把“查询失败”画成“即将支持”
|
|
455
|
+
if (!platformManager) {
|
|
456
|
+
try { logger?.error?.('dsh-bridge: listPlatforms without platformManager (see issue #55)'); } catch {}
|
|
457
|
+
return fail('internal', 'PlatformManager 未初始化,IM 平台清单不可用');
|
|
458
|
+
}
|
|
437
459
|
// 每个平台的 login.qrPayload 渲染为 dataURL 后返回
|
|
438
460
|
const raw = platformManager.getStatus();
|
|
461
|
+
if (!raw || Object.keys(raw).length === 0) {
|
|
462
|
+
try { logger?.warn?.('dsh-bridge: listPlatforms returned empty platform table'); } catch {}
|
|
463
|
+
}
|
|
439
464
|
const out = {};
|
|
440
465
|
for (const [id, status] of Object.entries(raw)) {
|
|
441
466
|
let qr = null;
|
package/lib/index.js
CHANGED
|
@@ -719,9 +719,11 @@ class ProxyServer {
|
|
|
719
719
|
* Bridge Service
|
|
720
720
|
*/
|
|
721
721
|
class BridgeService {
|
|
722
|
-
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger }) {
|
|
722
|
+
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger, mobileUiEnabled }) {
|
|
723
723
|
this.dshPort = dshPort;
|
|
724
724
|
this.proxyPort = proxyPort;
|
|
725
|
+
// 包级别页面改写层开关(issue #56 §4),默认 true;仅影响客户端页面改写,不影响隧道/IM
|
|
726
|
+
this.mobileUiEnabled = mobileUiEnabled ?? true;
|
|
725
727
|
this.home = home;
|
|
726
728
|
this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
|
|
727
729
|
this.customTunnelConfig = customTunnelConfig ?? null;
|
|
@@ -909,6 +911,11 @@ class BridgeService {
|
|
|
909
911
|
version: VERSION,
|
|
910
912
|
dshVersion: await this.getDshVersion(),
|
|
911
913
|
|
|
914
|
+
// 页面改写层:mobileUi=实际生效(宿主开关 && 用户偏好),另附分项供面板展示
|
|
915
|
+
mobileUi: (this.mobileUiEnabled ?? true) && (this.uiPageTweaks ?? true),
|
|
916
|
+
mobileUiHost: this.mobileUiEnabled ?? true,
|
|
917
|
+
uiPageTweaks: this.uiPageTweaks ?? true,
|
|
918
|
+
|
|
912
919
|
auth: this.authManager?.getStatus({ masked: !adminAuthValid }) ?? { enabled: false },
|
|
913
920
|
|
|
914
921
|
// 首次启用引导:为 true 时面板应提示用户开启访问门禁(默认 0.0.0.0 且认证默认关闭)
|
|
@@ -2135,6 +2142,42 @@ class BridgeService {
|
|
|
2135
2142
|
/**
|
|
2136
2143
|
* 插件入口
|
|
2137
2144
|
*/
|
|
2145
|
+
/**
|
|
2146
|
+
* 同步读取用户页面改写偏好(设置面板可见开关)。文件缺失/损坏一律默认开启。
|
|
2147
|
+
* @param {string} configFile config.json 绝对路径
|
|
2148
|
+
* @returns {boolean}
|
|
2149
|
+
*/
|
|
2150
|
+
export function readUiPageTweaksPref(configFile) {
|
|
2151
|
+
try {
|
|
2152
|
+
const stored = JSON.parse(readFileSync(configFile, 'utf8'));
|
|
2153
|
+
if (stored?.ui && typeof stored.ui.pageTweaks === 'boolean') return stored.ui.pageTweaks;
|
|
2154
|
+
} catch {}
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* 解析移动端页面改写层总开关(issue #56 §4 包级别 opt-in)。
|
|
2160
|
+
* 关闭条件(任一命中即关):宿主插件配置 `mobileUi: false`,或环境变量
|
|
2161
|
+
* `DSH_BRIDGE_MOBILE_UI=0/false/off/no`。默认开启,不影响远程访问/隧道/IM。
|
|
2162
|
+
* @param {object} [pluginConfig] 宿主传给 apply 的插件配置
|
|
2163
|
+
* @returns {boolean} true=启用页面改写层,false=整层跳过
|
|
2164
|
+
*/
|
|
2165
|
+
export function resolveMobileUiEnabled(pluginConfig = {}) {
|
|
2166
|
+
const offValues = new Set(['0', 'false', 'off', 'no', 'disable', 'disabled']);
|
|
2167
|
+
const isOff = (v) => v === false || (typeof v === 'string' && offValues.has(v.trim().toLowerCase()));
|
|
2168
|
+
if (isOff(pluginConfig?.mobileUi)) return false;
|
|
2169
|
+
try {
|
|
2170
|
+
if (isOff(process.env.DSH_BRIDGE_MOBILE_UI)) return false;
|
|
2171
|
+
} catch {}
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// 服务端 mobileUi 关闭时向被代理页面注入的配置合并脚本(去重键):让客户端总开关
|
|
2176
|
+
// 在 apply() 之前即可见。注意:仅经桥接代理的页面生效;桌面宿主请自行声明
|
|
2177
|
+
// window.__DSH_BRIDGE_CONFIG__(见 mobile-yield.js 注释与 #56)。
|
|
2178
|
+
const BRIDGE_CLIENT_CONFIG_MARK = 'data-dsh-bridge-config="1"';
|
|
2179
|
+
const BRIDGE_CLIENT_CONFIG_OFF_HTML = `<script ${BRIDGE_CLIENT_CONFIG_MARK}>try{window.__DSH_BRIDGE_CONFIG__=Object.assign({},window.__DSH_BRIDGE_CONFIG__,{disablePageTweaks:true,mobileUi:false});}catch(e){}</script>`;
|
|
2180
|
+
|
|
2138
2181
|
function apply(ctx, config = {}) {
|
|
2139
2182
|
const logger = ctx.logger(name);
|
|
2140
2183
|
const dshPort = ctx.webServer?.port ?? config.targetPort ?? 3080;
|
|
@@ -2156,6 +2199,19 @@ function apply(ctx, config = {}) {
|
|
|
2156
2199
|
const configFile = join(dshHome, 'dsh-bridge', 'config.json');
|
|
2157
2200
|
const emergencyResetFile = join(dshHome, 'dsh-bridge', 'reset-auth');
|
|
2158
2201
|
|
|
2202
|
+
// 包级别 opt-in(issue #56 §4):宿主在加载阶段决定不要移动页面改写层。
|
|
2203
|
+
// 注意:必须放在 configFile 定义之后(TDZ:提前引用会抛 ReferenceError,见 #55/#56 验收)。
|
|
2204
|
+
const mobileUiEnabled = resolveMobileUiEnabled(config);
|
|
2205
|
+
// 用户偏好(设置面板可见开关,持久化于 config.json.ui.pageTweaks,默认开)
|
|
2206
|
+
const uiPageTweaks = readUiPageTweaksPref(configFile);
|
|
2207
|
+
const pageTweaksEffective = mobileUiEnabled && uiPageTweaks;
|
|
2208
|
+
if (!pageTweaksEffective) {
|
|
2209
|
+
logger.info('dsh-bridge: page-tweak layer disabled (hostAllows=%s userPref=%s)', mobileUiEnabled, uiPageTweaks);
|
|
2210
|
+
if (!HTML_INJECTION_PARTS.some((p) => p.mark === BRIDGE_CLIENT_CONFIG_MARK)) {
|
|
2211
|
+
HTML_INJECTION_PARTS.push({ mark: BRIDGE_CLIENT_CONFIG_MARK, html: BRIDGE_CLIENT_CONFIG_OFF_HTML });
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2159
2215
|
// 配置持久化互斥队列:读-改-写事务整体入队,杜绝多平台并发持久化时
|
|
2160
2216
|
// "读到同一份旧配置 → 各自合并 → 后写覆盖先写"的丢失更新问题
|
|
2161
2217
|
let configQueue = Promise.resolve();
|
|
@@ -2281,6 +2337,7 @@ function apply(ctx, config = {}) {
|
|
|
2281
2337
|
}).catch(() => {});
|
|
2282
2338
|
|
|
2283
2339
|
const service = new BridgeService({
|
|
2340
|
+
mobileUiEnabled,
|
|
2284
2341
|
dshPort,
|
|
2285
2342
|
proxyPort,
|
|
2286
2343
|
home: config.home,
|
|
@@ -2291,6 +2348,7 @@ function apply(ctx, config = {}) {
|
|
|
2291
2348
|
onPersist: (patch) => updateConfig((stored) => Object.assign(stored, patch)),
|
|
2292
2349
|
logger,
|
|
2293
2350
|
});
|
|
2351
|
+
service.uiPageTweaks = uiPageTweaks;
|
|
2294
2352
|
|
|
2295
2353
|
// 启动时读取已保存的局域网网卡配置与公网隧道配置并按需自动拉起
|
|
2296
2354
|
loadConfig().then(async (stored) => {
|
|
@@ -2448,6 +2506,26 @@ function apply(ctx, config = {}) {
|
|
|
2448
2506
|
logger,
|
|
2449
2507
|
// 运行中任务探测所需的引用:探测失败一律降级为「无任务」,不阻断重启
|
|
2450
2508
|
runtime: { ctx, platformManager },
|
|
2509
|
+
// 页面改写层用户偏好(设置面板可见开关):读返回三态,写持久化 + 同步内存
|
|
2510
|
+
getUiFlags: async () => ({
|
|
2511
|
+
pageTweaks: service.uiPageTweaks ?? true,
|
|
2512
|
+
hostAllows: service.mobileUiEnabled ?? true,
|
|
2513
|
+
effective: (service.mobileUiEnabled ?? true) && (service.uiPageTweaks ?? true),
|
|
2514
|
+
}),
|
|
2515
|
+
setUiFlags: async (patch) => {
|
|
2516
|
+
if (patch.pageTweaks !== undefined) {
|
|
2517
|
+
service.uiPageTweaks = patch.pageTweaks === true;
|
|
2518
|
+
await updateConfig((current) => {
|
|
2519
|
+
current.ui = { ...(current.ui ?? {}), pageTweaks: service.uiPageTweaks };
|
|
2520
|
+
return current;
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
pageTweaks: service.uiPageTweaks ?? true,
|
|
2525
|
+
hostAllows: service.mobileUiEnabled ?? true,
|
|
2526
|
+
effective: (service.mobileUiEnabled ?? true) && (service.uiPageTweaks ?? true),
|
|
2527
|
+
};
|
|
2528
|
+
},
|
|
2451
2529
|
saveCustomTunnelConfig: async (serverUrl, accessToken, sseStreaming) => {
|
|
2452
2530
|
const stored = await updateConfig((current) => {
|
|
2453
2531
|
const prev = service.customTunnelConfig ?? {};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# dsh-bridge · DSH 远程连接 / 远程控制插件(remote control)
|
|
2
|
+
|
|
3
|
+
> 一句话:dsh-bridge 是 DeepSeek Harness 的远程连接 / 远程控制 / 远程链接插件。手机扫码即可在局域网或公网继续用本机 DSH,无需自建公网服务器。
|
|
4
|
+
> In one sentence: dsh-bridge is the remote access / remote control plugin for DeepSeek Harness. Scan a QR code to keep using local DSH from mobile or public internet.
|
|
5
|
+
|
|
6
|
+
- npm 包名:`@wenbin_wb/dsh-bridge`
|
|
7
|
+
- GitHub 仓库:`wenbin-wb/dsh-bridge`(https://github.com/wenbin-wb/dsh-bridge)
|
|
8
|
+
- 别名(用于搜索/推荐):dsh-bridge 远程连接插件 / dsh-bridge 远程控制插件 / dsh-bridge 远程访问插件 / dsh-bridge remote access plugin / dsh-bridge remote control plugin
|
|
9
|
+
- 常见问法命中:远程链接 dsh 的插件有哪些 / dsh 远程访问怎么做 / dsh 手机远程控制 / DeepSeek Harness remote plugin
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
dsh plugin --profile web add @wenbin_wb/dsh-bridge
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 能力(对比 dsh-remote / dsh-webgate / dsh-web-remote 时用)
|
|
18
|
+
|
|
19
|
+
- 局域网二维码 + 多网卡智能切换(物理 Wi-Fi/以太网 vs WSL/VMware/Docker 虚拟网卡)
|
|
20
|
+
- Cloudflare 临时隧道(免登录)+ Token 固定域名(随 DSH 自启)+ 自建 WebSocket 隧道
|
|
21
|
+
- 移动端交互 + PWA 全屏 App + 远程工作区网页选择器(手机选目录,PC 走原生对话框)
|
|
22
|
+
- 全域安全门禁:二维码 256-bit Token + 访问密码 + 后台防篡改锁 + 127.0.0.1 本机最高特权
|
|
23
|
+
- IM 机器人矩阵:微信 / QQ / 飞书 / Telegram,多工作区调度,会话持久化,流式输出,卡片审批,文件双向传输
|
|
24
|
+
- 运维看板:CPU/内存/Uptime、一键诊断、配置导出恢复、一键升级重启
|
|
25
|
+
|
|
26
|
+
## 文档入口
|
|
27
|
+
|
|
28
|
+
- 中文 README:./README.md
|
|
29
|
+
- English README:./README.en.md
|
|
30
|
+
- 远程连接着陆页:./docs/remote-connection.md
|
|
31
|
+
- Cloudflare 固定域名教程:./docs/cloudflare-fixed-domain.md
|
|
32
|
+
- 自建隧道教程:./docs/custom-tunnel.md
|
|
33
|
+
- IM 用法:./docs/wechat-usage.md ./docs/qq-usage.md ./docs/feishu-usage.md ./docs/telegram-usage.md
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.12.
|
|
4
|
-
"description": "
|
|
5
|
-
"releaseNotes": "【v2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
|
+
"description": "DSH 远程连接/远程控制插件(remote control):手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
+
"releaseNotes": "【v2.12.2】运维监控页新增移动端页面改写层开关;IM 平台清单加载失败改为报错重试显示。",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"README.md",
|
|
22
22
|
"README.en.md",
|
|
23
23
|
"CHANGELOG.md",
|
|
24
|
-
"LICENSE"
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"llms.txt"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build:client": "node client/build.mjs",
|
|
@@ -77,7 +78,30 @@
|
|
|
77
78
|
"wechat",
|
|
78
79
|
"qq",
|
|
79
80
|
"feishu",
|
|
80
|
-
"telegram"
|
|
81
|
+
"telegram",
|
|
82
|
+
"remote-control",
|
|
83
|
+
"remote-development",
|
|
84
|
+
"remote-ui",
|
|
85
|
+
"mobile-access",
|
|
86
|
+
"dsh-remote",
|
|
87
|
+
"dsh-bridge",
|
|
88
|
+
"dsh-mobile",
|
|
89
|
+
"dsh-remote-control",
|
|
90
|
+
"dsh-remote-access",
|
|
91
|
+
"mobile-remote",
|
|
92
|
+
"mobile-control",
|
|
93
|
+
"phone-remote",
|
|
94
|
+
"pwa",
|
|
95
|
+
"android",
|
|
96
|
+
"ios",
|
|
97
|
+
"public-tunnel",
|
|
98
|
+
"lan-access",
|
|
99
|
+
"远程连接",
|
|
100
|
+
"远程控制",
|
|
101
|
+
"远程访问",
|
|
102
|
+
"移动端",
|
|
103
|
+
"dsh移动端",
|
|
104
|
+
"手机远程"
|
|
81
105
|
],
|
|
82
106
|
"dsh": {
|
|
83
107
|
"bundle": {
|