@wenbin_wb/dsh-bridge 2.12.0 → 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 +41 -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 +150 -6
- package/llms.txt +33 -0
- package/package.json +29 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@
|
|
|
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
|
+
|
|
27
|
+
## [v2.12.1] - 2026-10-03
|
|
28
|
+
|
|
29
|
+
> 本版为**桌面版(Electron)兼容修复**版。排查依据为一手来源:官方同仓库 `apps/desktop-host/src/index.ts`、`apps/desktop/README.zh.md` 及官方 `evaluatePluginCompatibility` 实测。核心功能(面板、IM、隧道、存储)在桌面端与 Web 版跑同一套宿主与前端,确认兼容;以下两处是"Web 正常、桌面走错路"的分支,已修。
|
|
30
|
+
|
|
31
|
+
### 🐞 修复
|
|
32
|
+
|
|
33
|
+
- **面板"重启 DSH"在桌面端拦截**:桌面宿主由 Electron 壳经 IPC 管理生死,插件若按 Web/CLI 假设派生 `restart-helper` 重拉进程,会产生壳外孤儿(抢占 19387 端口、触发壳的恢复对话框)。现 `restartDsh` 开头检测桌面环境(`process.versions.electron` 或主模块路径 `desktop-host`,fail-open),直接拒绝并指引走应用菜单重启,绝不派生任何子进程。
|
|
34
|
+
- **插件自升级 profile 参数化**:`upgradePlugin` 原来硬编码 `dsh plugin --profile web add`,在桌面端(profile 名为 `desktop`)会把包装进宿主不加载的 profile,造成"显示成功实际没升"。现按宿主形态推导(桌面 `desktop`、其余 `web`),传入值白名单只认 `desktop`/`web`(防 shell 注入)。
|
|
35
|
+
- **桌面端一键升级 DSH 文案**:`_probeDshUpgrade` 在桌面端仍正确返回不可升级(应用包内文件不能用 npm 动),但 reason 换成桌面 relevant 的指引(走应用菜单"检查更新")。
|
|
36
|
+
|
|
37
|
+
### 🧪 测试
|
|
38
|
+
|
|
39
|
+
- 新增 `test/desktop-host-compat.test.mjs`(11 项:electron 标记判定、`desktop-host` 路径分段判定、fail-open 脏输入、profile 推导、桌面重启零派生、非桌面重启不受影响、默认 web/显式 desktop/注入 profile 白名单,以及"测试文件名含子串不误判"的回归)。全量 **395 项测试 100% 通过**。
|
|
40
|
+
|
|
41
|
+
### 📝 升级说明
|
|
42
|
+
|
|
43
|
+
- 无破坏性变更;Web/CLI 用户行为与 v2.12.0 完全一致(桌面检测 fail-open,判不准时按原路走)。
|
|
44
|
+
- 桌面端实机验证仍未做(本机无桌面版安装包):P0/P1 修复基于官方源码的静态结论 + 单测,`process.versions.electron` 在 RunAsNode 下的存在性为 Electron 官方行为。建议在 macOS/Windows 桌面端实测"面板重启被拒"与"插件升级进 desktop profile"两条路径后再关闭该跟踪。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
7
48
|
## [v2.12.0] - 2026-10-03
|
|
8
49
|
|
|
9
50
|
> 本版为**安全加固 + 首次启用引导 + 发布包瘦身**版。依据外部安全审计(针对 v2.10.13)逐条核实后修复;按「不破坏现有用户」原则,**默认监听地址保持 `0.0.0.0` 不变**,改用引导提示降低「开箱敞开」风险。
|
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
|
@@ -317,6 +317,49 @@ export function detectSupervisor({ env = process.env, cgroup = '' } = {}) {
|
|
|
317
317
|
return { kind: 'self', reason: '未检出托管器(无 DSH_DAEMON/PM2_HOME,也不在 systemd 单元内)' };
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 判断当前宿主是否跑在 DSH 桌面版(Electron)内(纯函数,便于单测)。
|
|
322
|
+
*
|
|
323
|
+
* 背景:桌面端宿主是 Electron 以 RunAsNode 模式拉起的同一套 @deepseek-ai/dsh,
|
|
324
|
+
* 进程生死由 Electron 壳经 IPC 管理(shutdown/quit-inspection),没有 systemd、
|
|
325
|
+
* DSH_DAEMON/PM2 这类托管器。插件若按 Web/CLI 版的假设自行重启/升级宿主,
|
|
326
|
+
* 会拉起脱离壳管理的孤儿进程(抢占 19387 端口、触发壳的恢复对话框)或把包
|
|
327
|
+
* 装进错误的 profile(桌面端 profile 名为 `desktop`,不是 `web`)。
|
|
328
|
+
*
|
|
329
|
+
* 判据(按可靠性排序,任一命中即判为桌面端):
|
|
330
|
+
* 1. `process.versions.electron` 存在 —— Electron(含 RunAsNode 模式)的官方
|
|
331
|
+
* 行为保证该字段存在,跨平台最可靠;
|
|
332
|
+
* 2. 主模块路径含 `desktop-host` —— 桌面宿主入口固定为 desktop-host 包。
|
|
333
|
+
*
|
|
334
|
+
* fail-open:判不准时一律按 Web/CLI 版走,绝不能把正常用户的重启/升级拦掉。
|
|
335
|
+
*
|
|
336
|
+
* @param {object} [opts]
|
|
337
|
+
* @param {object} [opts.versions] process.versions(默认 process.versions)
|
|
338
|
+
* @param {string} [opts.argv1] process.argv[1](默认 process.argv[1])
|
|
339
|
+
* @returns {boolean}
|
|
340
|
+
*/
|
|
341
|
+
export function isDesktopHost({ versions = process.versions, argv1 = process.argv[1] } = {}) {
|
|
342
|
+
try {
|
|
343
|
+
if (versions && typeof versions.electron === 'string' && versions.electron.length > 0) return true;
|
|
344
|
+
} catch { /* 忽略,继续下一个判据 */ }
|
|
345
|
+
try {
|
|
346
|
+
// 必须按路径分段匹配(前后是分隔符或串 ends):避免用户目录名恰好包含
|
|
347
|
+
// desktop-host 子串(如本仓库的 test/desktop-host-compat.test.mjs)时误判。
|
|
348
|
+
// 生产路径形如 .../dsh-desktop-host/lib/index.js。
|
|
349
|
+
if (typeof argv1 === 'string' && /(?:^|[/\\])(?:dsh-)?desktop-host(?:[/\\]|$)/i.test(argv1)) return true;
|
|
350
|
+
} catch { /* 忽略,fail-open */ }
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 当前宿主加载的 profile 名(纯函数,便于单测)。
|
|
356
|
+
* 桌面端为 `desktop`,其余(Web/CLI)为 `web`。供 `dsh plugin --profile <name> add`
|
|
357
|
+
* 类命令使用,避免把包装进宿主不加载的 profile。
|
|
358
|
+
*/
|
|
359
|
+
export function currentProfileName(opts = {}) {
|
|
360
|
+
return isDesktopHost(opts) ? 'desktop' : 'web';
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
class ProxyServer {
|
|
321
364
|
constructor({ localPort, targetPort, authManager, logger, allowedOrigins }) {
|
|
322
365
|
this.localPort = localPort;
|
|
@@ -676,9 +719,11 @@ class ProxyServer {
|
|
|
676
719
|
* Bridge Service
|
|
677
720
|
*/
|
|
678
721
|
class BridgeService {
|
|
679
|
-
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger }) {
|
|
722
|
+
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger, mobileUiEnabled }) {
|
|
680
723
|
this.dshPort = dshPort;
|
|
681
724
|
this.proxyPort = proxyPort;
|
|
725
|
+
// 包级别页面改写层开关(issue #56 §4),默认 true;仅影响客户端页面改写,不影响隧道/IM
|
|
726
|
+
this.mobileUiEnabled = mobileUiEnabled ?? true;
|
|
682
727
|
this.home = home;
|
|
683
728
|
this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
|
|
684
729
|
this.customTunnelConfig = customTunnelConfig ?? null;
|
|
@@ -866,6 +911,11 @@ class BridgeService {
|
|
|
866
911
|
version: VERSION,
|
|
867
912
|
dshVersion: await this.getDshVersion(),
|
|
868
913
|
|
|
914
|
+
// 页面改写层:mobileUi=实际生效(宿主开关 && 用户偏好),另附分项供面板展示
|
|
915
|
+
mobileUi: (this.mobileUiEnabled ?? true) && (this.uiPageTweaks ?? true),
|
|
916
|
+
mobileUiHost: this.mobileUiEnabled ?? true,
|
|
917
|
+
uiPageTweaks: this.uiPageTweaks ?? true,
|
|
918
|
+
|
|
869
919
|
auth: this.authManager?.getStatus({ masked: !adminAuthValid }) ?? { enabled: false },
|
|
870
920
|
|
|
871
921
|
// 首次启用引导:为 true 时面板应提示用户开启访问门禁(默认 0.0.0.0 且认证默认关闭)
|
|
@@ -1183,7 +1233,11 @@ class BridgeService {
|
|
|
1183
1233
|
}
|
|
1184
1234
|
return {
|
|
1185
1235
|
upgradable: false,
|
|
1186
|
-
|
|
1236
|
+
// 桌面端 dsh 装在 App 包内:走桌面应用内更新,不要用 npm 动它。
|
|
1237
|
+
// 桌面检测是 fail-open 的,判不准时仍是这条通用文案,不影响 Web/CLI 用户。
|
|
1238
|
+
reason: isDesktopHost()
|
|
1239
|
+
? '桌面版 DSH 由应用内更新统一管理(一键升级仅支持 npm 全局安装的 CLI 版);请用桌面应用菜单的"检查更新"升级,勿用 npm 改动应用包内文件'
|
|
1240
|
+
: 'dsh 非标准 npm 全局安装(Electron/打包/源码/pnpm 等),无法自动升级;请按官方渠道手动更新',
|
|
1187
1241
|
};
|
|
1188
1242
|
} catch {
|
|
1189
1243
|
return { upgradable: false, reason: '探测 dsh 安装形态失败,请按官方渠道手动更新' };
|
|
@@ -1244,7 +1298,9 @@ class BridgeService {
|
|
|
1244
1298
|
}
|
|
1245
1299
|
|
|
1246
1300
|
// 一键直接升级插件(执行 dsh / npx / npm 自动升级,使用安全的参数数组彻底杜绝 shell 注入)
|
|
1247
|
-
|
|
1301
|
+
// opts.profile 供测试注入;默认按当前宿主推导(桌面端为 `desktop`,其余为 `web`),
|
|
1302
|
+
// 绝不能写死 --profile web——桌面端会把包装进宿主不加载的 profile,造成"显示成功实际没升"。
|
|
1303
|
+
async upgradePlugin({ version, spawnImpl = spawn, profile } = {}) {
|
|
1248
1304
|
const targetVersion = version ? String(version).trim() : 'latest';
|
|
1249
1305
|
// 严格 SemVer 白名单正则校验
|
|
1250
1306
|
if (!/^(latest|\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?)$/.test(targetVersion)) {
|
|
@@ -1283,9 +1339,15 @@ class BridgeService {
|
|
|
1283
1339
|
const siblingNpm = join(nodeDir, isWin ? 'npm.cmd' : 'npm');
|
|
1284
1340
|
const siblingNpx = join(nodeDir, isWin ? 'npx.cmd' : 'npx');
|
|
1285
1341
|
|
|
1342
|
+
// 目标 profile:显式传入优先,否则按宿主形态推导(桌面端 `desktop`,其余 `web`)。
|
|
1343
|
+
// 桌面端 profile 名固定,见官方 desktop-host(runProfile profile: 'desktop')。
|
|
1344
|
+
// 安全:payload 里的 profile 不可信,白名单只认 desktop/web,其余一律按推导值处理,
|
|
1345
|
+
// 避免未校验字符串进入 shell:true 的 spawn 参数(参数拼接有注入风险)。
|
|
1346
|
+
const targetProfile = (profile === 'desktop' || profile === 'web') ? profile : currentProfileName();
|
|
1347
|
+
|
|
1286
1348
|
const tasks = [
|
|
1287
|
-
{ cmd: 'dsh', args: ['plugin', '--profile',
|
|
1288
|
-
{ cmd: existsSync(siblingNpx) ? siblingNpx : 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile',
|
|
1349
|
+
{ cmd: 'dsh', args: ['plugin', '--profile', targetProfile, 'add', pkgSpec] },
|
|
1350
|
+
{ cmd: existsSync(siblingNpx) ? siblingNpx : 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', targetProfile, 'add', pkgSpec] },
|
|
1289
1351
|
{ cmd: existsSync(siblingNpm) ? siblingNpm : 'npm', args: ['install', pkgSpec] },
|
|
1290
1352
|
];
|
|
1291
1353
|
|
|
@@ -1296,7 +1358,7 @@ class BridgeService {
|
|
|
1296
1358
|
const res = await new Promise((resolve, reject) => {
|
|
1297
1359
|
let cp;
|
|
1298
1360
|
try {
|
|
1299
|
-
cp =
|
|
1361
|
+
cp = spawnImpl(task.cmd, task.args, {
|
|
1300
1362
|
windowsHide: true,
|
|
1301
1363
|
shell: true,
|
|
1302
1364
|
env: augmentedEnv,
|
|
@@ -1517,6 +1579,17 @@ class BridgeService {
|
|
|
1517
1579
|
// 并把全过程写入 ~/.dsh/dsh-bridge/restart.log(失败不再无声)。
|
|
1518
1580
|
// opts 仅供单测注入(spawnImpl / readCgroup / env / scheduleExit)。
|
|
1519
1581
|
async restartDsh(opts = {}) {
|
|
1582
|
+
// 桌面端拦截(P0):桌面宿主由 Electron 壳经 IPC 管理生死,插件绝不能自行
|
|
1583
|
+
// 派生 helper 重拉进程——那会产生壳外孤儿(抢占 19387 端口、触发壳的恢复
|
|
1584
|
+
// 对话框)。直接拒绝,并指引用户走应用菜单重启。
|
|
1585
|
+
// isDesktopHost 是 fail-open 的:判不准时按 Web/CLI 版走,不拦正常用户。
|
|
1586
|
+
if (isDesktopHost({ versions: opts.processVersions, argv1: opts.argv1 })) {
|
|
1587
|
+
return {
|
|
1588
|
+
ok: false,
|
|
1589
|
+
error: '当前运行在 DSH 桌面版内,宿主由应用统一管理,插件不能自行重启。请用桌面应用菜单的重启/退出重进(有关闭确认保护正在跑的任务),不要用面板重启。',
|
|
1590
|
+
desktopManaged: true,
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1520
1593
|
const env = opts.env ?? process.env;
|
|
1521
1594
|
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
1522
1595
|
const readCgroup = opts.readCgroup
|
|
@@ -2069,6 +2142,42 @@ class BridgeService {
|
|
|
2069
2142
|
/**
|
|
2070
2143
|
* 插件入口
|
|
2071
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
|
+
|
|
2072
2181
|
function apply(ctx, config = {}) {
|
|
2073
2182
|
const logger = ctx.logger(name);
|
|
2074
2183
|
const dshPort = ctx.webServer?.port ?? config.targetPort ?? 3080;
|
|
@@ -2090,6 +2199,19 @@ function apply(ctx, config = {}) {
|
|
|
2090
2199
|
const configFile = join(dshHome, 'dsh-bridge', 'config.json');
|
|
2091
2200
|
const emergencyResetFile = join(dshHome, 'dsh-bridge', 'reset-auth');
|
|
2092
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
|
+
|
|
2093
2215
|
// 配置持久化互斥队列:读-改-写事务整体入队,杜绝多平台并发持久化时
|
|
2094
2216
|
// "读到同一份旧配置 → 各自合并 → 后写覆盖先写"的丢失更新问题
|
|
2095
2217
|
let configQueue = Promise.resolve();
|
|
@@ -2215,6 +2337,7 @@ function apply(ctx, config = {}) {
|
|
|
2215
2337
|
}).catch(() => {});
|
|
2216
2338
|
|
|
2217
2339
|
const service = new BridgeService({
|
|
2340
|
+
mobileUiEnabled,
|
|
2218
2341
|
dshPort,
|
|
2219
2342
|
proxyPort,
|
|
2220
2343
|
home: config.home,
|
|
@@ -2225,6 +2348,7 @@ function apply(ctx, config = {}) {
|
|
|
2225
2348
|
onPersist: (patch) => updateConfig((stored) => Object.assign(stored, patch)),
|
|
2226
2349
|
logger,
|
|
2227
2350
|
});
|
|
2351
|
+
service.uiPageTweaks = uiPageTweaks;
|
|
2228
2352
|
|
|
2229
2353
|
// 启动时读取已保存的局域网网卡配置与公网隧道配置并按需自动拉起
|
|
2230
2354
|
loadConfig().then(async (stored) => {
|
|
@@ -2382,6 +2506,26 @@ function apply(ctx, config = {}) {
|
|
|
2382
2506
|
logger,
|
|
2383
2507
|
// 运行中任务探测所需的引用:探测失败一律降级为「无任务」,不阻断重启
|
|
2384
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
|
+
},
|
|
2385
2529
|
saveCustomTunnelConfig: async (serverUrl, accessToken, sseStreaming) => {
|
|
2386
2530
|
const stored = await updateConfig((current) => {
|
|
2387
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": {
|