@wenbin_wb/dsh-bridge 2.10.15 → 2.11.1
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 +40 -0
- package/client/client.js +259 -19
- package/client/index.js +97 -15
- package/client/mobile-styles.js +17 -3
- package/client/mobile-yield.js +192 -0
- package/client/settings-nav-icon.js +90 -0
- package/docs/release-process.md +6 -1
- package/lib/feishu/lark-bundled.mjs +76 -230
- package/lib/index.js +37 -12
- package/lib/restart-helper.mjs +17 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.11.1] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
> 本版为修复版:解决 DSH 0.1.7 升级后暴露的移动端「看不到最新消息」、设置弹窗层级误抬,以及 linux 下托管误判导致的 systemd 掉线问题。
|
|
10
|
+
|
|
11
|
+
### 🐞 修复
|
|
12
|
+
|
|
13
|
+
- **手机上聊天区「看不到最新消息」(PR #48,感谢 imroc)**:移动端主框架规则原用 `div[class*="_frame"]` 跨包通配选择器,除了命中有意的布局外壳(`[data-slot="root"]` 直接子元素),还会误伤 DSH 0.1.7 新增的聊天记录容器(`dsh-client-ui-chat` 的 `.EvIC1a_frame`)——给它强加 `height:100dvh + overflow:hidden` 后被钳到视口高度、溢出内容被裁掉,而外层滚动区已到底,表现为「看到旧消息、看不到最新消息」。现选择器收窄为 `[data-slot="root"] > div[class*="_frame"]`,仅命中布局外壳;对 0.1.5 / 0.1.6 宿主无行为差异。
|
|
14
|
+
- **linux 下托管误判与 systemd 掉线**:真实 `/proc/self/cgroup` 带末尾换行且 cgroup v1 为多行,原解析按单行整体切分,导致 `endsWith('.service')` 判定失败、托管器被误判为 `self`,`Restart=on-failure` 下 dsh 掉线不自动拉起。现逐行解析并 trim;同时重启前为 `systemctl` 补齐默认 PATH 与 `XDG_RUNTIME_DIR`(`--user` 场景)、加 5s 执行超时兜底、自动创建日志目录。
|
|
15
|
+
- **设置弹窗自身被层级规则误抬(适配 DSH 0.1.7)**:0.1.7 把设置弹窗(`VOzbGW_overlay`)从 `#root` 内部移为 `<body>` 直接子级,满足「body 子级 + 含 `role=dialog` 子元素」的抬升特征,被一并抬到第三方模态同层,退化成按 DOM 顺序决胜,「安装确认框被盖住」可能复发。现抬升规则排除设置弹窗自身,仍只抬其它顶层模态。
|
|
16
|
+
|
|
17
|
+
### 🧪 测试
|
|
18
|
+
|
|
19
|
+
- 新增 `test/restart-dsh.test.mjs` cgroup 多行/换行回归断言;`test/mobile-fixed-panel-offset.test.mjs` 新增 frame 规则限定回归守卫;`test/mobile-settings-narrow-layout.test.mjs` 新增抬升规则排除断言。全量 338 项测试 100% 通过。
|
|
20
|
+
|
|
21
|
+
> ⚠️ 生效说明:客户端样式刷新页面即生效;重启守护逻辑改动需重启 dsh 后生效。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [v2.11.0] - 2026-09-28
|
|
26
|
+
|
|
27
|
+
> 本版为客户端架构升级与环境解耦:支持桌面端宿主(如 Tauri)将设置面板作为独立组件嵌入并动态管理设置页入口;同时为 Android 移动壳及第三方自适应插件引入环境自适应让位与多级特性开关,消除界面冲突与遮挡。
|
|
28
|
+
|
|
29
|
+
### ✨ 新功能
|
|
30
|
+
|
|
31
|
+
- **客户端宿主接入面(对齐 #51)**:在客户端入口暴露 Cordis 反射服务 `ctx.provide('dsh-bridge', { version: 1, render, setSettingsVisible, settingsVisible, configureFeatures, getFeatures })`。桌面端宿主(如 DSH Desktop / Tauri)可调用 `render()` 将 `BridgePanel` 现构造嵌入自定义侧栏或扩展容器,支持 `preferredTab` 指定默认停靠页并保持动态响应;收编后可调用 `setSettingsVisible(false)` 动态撤下全局设置页中的重复条目。老宿主环境下平稳降级。
|
|
32
|
+
- **设置页左侧导航栏图标定制**:新增 `client/settings-nav-icon.js`,通过 CSS mask 将设置中心原生回退的通用小齿轮(gear)替换为「远程访问 / 手机无线连接」专属矢量图标,颜色自动跟随 `currentColor` 保持原生悬停与激活态一致。
|
|
33
|
+
- **移动端多级特性开关与排障支持(对齐 #52)**:新增 `client/mobile-yield.js`,支持通过 URL 查询参数(如 `?dsh_bridge_tweaks=0` 全局禁用、`?dsh_mobile_header=0` 仅禁用顶栏)、宿主全局配置(`window.__DSH_BRIDGE_CONFIG__`)、运行时反射服务或本地存储细粒度控制移动端 UI 特性。
|
|
34
|
+
|
|
35
|
+
### 🐞 修复
|
|
36
|
+
|
|
37
|
+
- **Android 移动壳/已有自适应插件下的界面冲突与顶栏遮挡(#52)**:在 Android 原生 WebView 壳(如 `com.dsharnessmobile.shell`)或已加载移动自适应插件(如 `@dsh-android/dsh-client-ui-responsive`)的环境下,插件主动避让自建 body 级顶栏与全局覆盖样式,杜绝双重顶栏重叠、设置页错位与触控遮挡失效。
|
|
38
|
+
|
|
39
|
+
### 🧪 测试
|
|
40
|
+
|
|
41
|
+
- 新增 `test/mobile-yield.test.mjs`(11 条)、`test/client-provide.test.mjs`(6 条)与 `test/settings-nav-icon.test.mjs`(3 条),全量 337 项测试 100% 通过。
|
|
42
|
+
|
|
43
|
+
> ⚠️ 生效说明:纯前端改动,刷新页面即可生效,无需重启 dsh。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
7
47
|
## [v2.10.15] - 2026-09-25
|
|
8
48
|
|
|
9
49
|
> 本版为设置页关闭按钮的可用性微调:手机上「✕」更容易点中。
|
package/client/client.js
CHANGED
|
@@ -78,7 +78,12 @@ var MOBILE_STYLES_CSS = `
|
|
|
78
78
|
\u5F39\u7A97\u4E4B\u4E0A\u5E76\u76D6\u4F4F\u8BBE\u7F6E\u9875\u3002\u8FD9\u662F\u300C\u6A21\u6001\u5C31\u8BE5\u76D6\u5728\u8BBE\u7F6E\u5F39\u7A97\u4E4B\u4E0A\u300D\u7684\u65E2\u5B9A\u53D6\u820D\uFF1A\u5B81\u53EF\u8BA9\u771F\u6A21\u6001
|
|
79
79
|
\u53EF\u89C1\uFF0C\u4E5F\u4E0D\u63A5\u53D7"\u5F39\u4E86\u5374\u70B9\u4E0D\u5230"\u3002\u72EC\u7ACB\u9A8C\u6536\u5DF2\u5B9E\u6D4B\u8BE5\u6784\u9020\u4F1A\u547D\u4E2D\uFF1B\u82E5\u5C06\u6765\u771F\u8E29\u5230\uFF0C
|
|
80
80
|
\u5E94\u6539\u4E3A\u6309\u300C\u8BE5\u6D6E\u5C42\u662F\u5426\u53EF\u5173\u95ED\u300D\u8FDB\u4E00\u6B65\u6536\u7A84\uFF0C\u800C\u4E0D\u662F\u56DE\u9000\u8FD9\u6761\u89C4\u5219\u3002 */
|
|
81
|
-
|
|
81
|
+
/* \u26A0\uFE0F \u5FC5\u987B\u6392\u9664\u8BBE\u7F6E\u5F39\u7A97\u81EA\u8EAB\uFF1ADSH 0.1.7 \u628A VOzbGW_overlay \u53D8\u6210\u4E86 <body> \u7684\u76F4\u63A5\u5B50\u7EA7
|
|
82
|
+
\uFF080.1.5 \u91CC\u5B83\u5D4C\u5728 #root \u5185\u90E8\uFF09\uFF0C\u5B83\u540C\u6837\u6EE1\u8DB3\u300Cbody \u5B50\u7EA7 + \u542B role=dialog \u5B50\u5143\u7D20\u300D\uFF0C
|
|
83
|
+
\u4F1A\u88AB\u672C\u6761\u4E00\u5E76\u62AC\u5230 10050 \u2014\u2014 \u4E8E\u662F\u4E0E\u7B2C\u4E09\u65B9\u6A21\u6001\u6839\u540C\u5C42\u3001\u9000\u5316\u6210\u6309 DOM \u987A\u5E8F\u51B3\u80DC\uFF0C
|
|
84
|
+
\u521A\u4FEE\u597D\u7684\u300C\u5B89\u88C5\u786E\u8BA4\u6846\u88AB\u76D6\u4F4F\u300D\u5C31\u53EF\u80FD\u590D\u53D1\u3002:not(...) \u628A\u5B83\u6392\u9664\uFF0C\u8BA9\u8BBE\u7F6E\u5F39\u7A97\u7EE7\u7EED\u7531
|
|
85
|
+
\u5B83\u81EA\u5DF1\u7684 10002 \u89C4\u5219\u5B9A\u4F4D\uFF0C\u672C\u6761\u53EA\u62AC**\u5176\u5B83**\u9876\u5C42\u6A21\u6001\u3002 */
|
|
86
|
+
body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[class*="mask"], > div[role="dialog"]):not([class*="VOzbGW_overlay"]) {
|
|
82
87
|
z-index: 10050 !important;
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -104,8 +109,17 @@ var MOBILE_STYLES_CSS = `
|
|
|
104
109
|
\uFF08dsh-client-ui-sidebar-right/lib/client.js\uFF09\uFF0C\u6545\u79FB\u52A8\u7AEF\u6837\u5F0F\u53D6 <=767px\uFF0C
|
|
105
110
|
768px \u8D77\u5F7B\u5E95\u4EA4\u8FD8\u684C\u9762\u5E03\u5C40\uFF0C\u907F\u514D"\u6865\u6E32\u67D3\u9876\u680F\u3001\u5BBF\u4E3B\u5374\u672A\u5168\u5C4F"\u7684\u9519\u4F4D\u3002 */
|
|
106
111
|
@media (max-width: 767px) {
|
|
107
|
-
/* 1. \u4E3B\u6846\u67B6\u4E3A Header \u817E\u51FA\u9876\u90E8\u7A7A\u95F4
|
|
108
|
-
|
|
112
|
+
/* 1. \u4E3B\u6846\u67B6\u4E3A Header \u817E\u51FA\u9876\u90E8\u7A7A\u95F4\u3002
|
|
113
|
+
|
|
114
|
+
\u9009\u62E9\u5668\u5FC5\u987B\u9650\u5B9A\u5728\u5E03\u5C40\u5916\u58F3\uFF08dsh-client-ui-layout \u7684 AppFrame\uFF0C\u5373
|
|
115
|
+
[data-slot="root"] \u7684\u76F4\u63A5\u5B50\u5143\u7D20\uFF09\u3002\u5BBF\u4E3B\u7684 CSS-module \u54C8\u5E0C\u53EA\u4FDD\u8BC1\u540C\u6587\u4EF6
|
|
116
|
+
\u5185\u552F\u4E00\uFF0C*="_frame" \u662F\u8DE8\u5305\u901A\u914D\uFF1A\u804A\u5929\u8BB0\u5F55\u5BB9\u5668\uFF08dsh-client-ui-chat \u7684
|
|
117
|
+
.EvIC1a_frame\uFF0C\u540C\u6837\u4EE5 _frame \u7ED3\u5C3E\uFF09\u4E5F\u4F1A\u88AB\u547D\u4E2D\u3002\u4E00\u65E6\u547D\u4E2D\uFF0C\u5B83\u88AB\u5F3A\u52A0
|
|
118
|
+
height:100dvh + overflow:hidden\uFF0C\u5728\u624B\u673A\u4E0A\u88AB\u94B3\u5230\u89C6\u53E3\u9AD8\u5EA6\u5E76\u88C1\u6389\u6EA2\u51FA\u5185\u5BB9\uFF1A
|
|
119
|
+
\u5916\u5C42 [data-conversation-scroll] \u5DF2\u6EDA\u5230\u5E95\u3001\u5185\u90E8\u5374\u8FD8\u6709\u6570\u5343 px \u8BFB\u4E0D\u5230\uFF0C
|
|
120
|
+
\u8868\u73B0\u4E3A\u300C\u770B\u4E0D\u5230\u6700\u65B0\u6D88\u606F\u300D\u3002\u5B98\u65B9\u7ED9\u8BE5\u5BB9\u5668\u7684\u89C4\u5219\u662F flex:none;height:auto
|
|
121
|
+
\uFF08\u8BA9\u8BB0\u5F55\u968F\u5185\u5BB9\u81EA\u7136\u589E\u9AD8\u3001\u7531\u5916\u5C42\u7EDF\u4E00\u6EDA\u52A8\uFF09\uFF0C\u4E0E\u8FD9\u91CC\u51B2\u7A81\uFF0C\u6545\u5FC5\u987B\u6392\u9664\u3002 */
|
|
122
|
+
[data-slot="root"] > div[class*="_frame"] {
|
|
109
123
|
display: flex !important;
|
|
110
124
|
flex-direction: column !important;
|
|
111
125
|
width: 100vw !important;
|
|
@@ -1374,6 +1388,166 @@ function shouldYieldToOfficialPicker(facts) {
|
|
|
1374
1388
|
return Boolean(facts?.local) && Boolean(facts?.officialPicker);
|
|
1375
1389
|
}
|
|
1376
1390
|
|
|
1391
|
+
// client/mobile-yield.js
|
|
1392
|
+
var FEATURES = {
|
|
1393
|
+
MOBILE_HEADER: "mobileHeader",
|
|
1394
|
+
MOBILE_STYLES: "mobileStyles",
|
|
1395
|
+
COMPOSER_COLLAPSE: "composerCollapse"
|
|
1396
|
+
};
|
|
1397
|
+
var runtimeFeatureOverrides = {};
|
|
1398
|
+
function setRuntimeFeatureOverrides(overrides) {
|
|
1399
|
+
if (overrides && typeof overrides === "object") {
|
|
1400
|
+
runtimeFeatureOverrides = { ...runtimeFeatureOverrides, ...overrides };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function getRuntimeFeatureOverrides() {
|
|
1404
|
+
return { ...runtimeFeatureOverrides };
|
|
1405
|
+
}
|
|
1406
|
+
function detectMobileShellOrResponsivePlugin() {
|
|
1407
|
+
if (typeof window === "undefined") return false;
|
|
1408
|
+
if (window.__DSH_MOBILE_SHELL__ || window.__TAURI__ || window.__DSH_DESKTOP__) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
if (window.__DSH_CLIENT_UI_RESPONSIVE__) {
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof document !== "undefined") {
|
|
1415
|
+
if (document.querySelector("[data-dsh-responsive-mobile]") || document.querySelector("[data-dsh-mobile-shell]")) {
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
function isFeatureEnabled(featureName, defaultDetector) {
|
|
1422
|
+
if (typeof window === "undefined") {
|
|
1423
|
+
return typeof defaultDetector === "function" ? defaultDetector() : true;
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
if (window.location && window.location.search) {
|
|
1427
|
+
const params = new URLSearchParams(window.location.search);
|
|
1428
|
+
const tweaksVal = params.get("dsh_bridge_tweaks") ?? params.get("dsh_mobile_tweaks");
|
|
1429
|
+
if (tweaksVal === "0" || tweaksVal === "false" || tweaksVal === "off") {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
if (tweaksVal === "1" || tweaksVal === "true" || tweaksVal === "on") {
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
const kebabName = featureName.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
|
1436
|
+
const specificVal = params.get(`dsh_${kebabName}`) ?? params.get(`dsh_${featureName}`);
|
|
1437
|
+
if (specificVal === "0" || specificVal === "false" || specificVal === "off") {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
if (specificVal === "1" || specificVal === "true" || specificVal === "on") {
|
|
1441
|
+
return true;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
const hostConfig = window.__DSH_BRIDGE_CONFIG__;
|
|
1448
|
+
if (hostConfig && typeof hostConfig === "object") {
|
|
1449
|
+
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
if (typeof hostConfig[featureName] === "boolean") {
|
|
1453
|
+
return hostConfig[featureName];
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
if (typeof runtimeFeatureOverrides[featureName] === "boolean") {
|
|
1459
|
+
return runtimeFeatureOverrides[featureName];
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
if (window.localStorage) {
|
|
1463
|
+
const stored = window.localStorage.getItem(`dsh_bridge:feature:${featureName}`);
|
|
1464
|
+
if (stored === "0" || stored === "false") return false;
|
|
1465
|
+
if (stored === "1" || stored === "true") return true;
|
|
1466
|
+
}
|
|
1467
|
+
} catch {
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof defaultDetector === "function") {
|
|
1470
|
+
return defaultDetector();
|
|
1471
|
+
}
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
function shouldYieldMobileHeader() {
|
|
1475
|
+
return !isFeatureEnabled(FEATURES.MOBILE_HEADER, () => {
|
|
1476
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function shouldYieldMobileStyles() {
|
|
1480
|
+
return !isFeatureEnabled(FEATURES.MOBILE_STYLES, () => {
|
|
1481
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
function shouldYieldComposerCollapse() {
|
|
1485
|
+
return !isFeatureEnabled(FEATURES.COMPOSER_COLLAPSE, () => {
|
|
1486
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// client/settings-nav-icon.js
|
|
1491
|
+
var SETTINGS_NAV_MARKER = "data-dsh-bridge-settings-nav";
|
|
1492
|
+
var REMOTE_NAV_SVG = `<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect width='10' height='16' x='3' y='4' rx='2'/><path d='M7 16h2'/><path d='M17 8a4 4 0 0 1 0 8'/><path d='M20 5a8 8 0 0 1 0 14'/></svg>`;
|
|
1493
|
+
function getNavIconCss(customSvg = REMOTE_NAV_SVG) {
|
|
1494
|
+
const maskUrl = `data:image/svg+xml,${encodeURIComponent(customSvg)}`;
|
|
1495
|
+
return `
|
|
1496
|
+
[${SETTINGS_NAV_MARKER}] > svg:first-child {
|
|
1497
|
+
display: none !important;
|
|
1498
|
+
}
|
|
1499
|
+
[${SETTINGS_NAV_MARKER}]::before {
|
|
1500
|
+
content: '';
|
|
1501
|
+
flex: none;
|
|
1502
|
+
width: 16px;
|
|
1503
|
+
height: 16px;
|
|
1504
|
+
background-color: currentColor;
|
|
1505
|
+
-webkit-mask: url("${maskUrl}") center / contain no-repeat;
|
|
1506
|
+
mask: url("${maskUrl}") center / contain no-repeat;
|
|
1507
|
+
}
|
|
1508
|
+
`;
|
|
1509
|
+
}
|
|
1510
|
+
function registerSettingsNavIcon(resolveLabel = () => "\u8FDC\u7A0B\u8BBF\u95EE") {
|
|
1511
|
+
if (typeof document === "undefined") return () => {
|
|
1512
|
+
};
|
|
1513
|
+
const styleId = "dsh-bridge-settings-nav-icon-style";
|
|
1514
|
+
if (!document.getElementById(styleId)) {
|
|
1515
|
+
const style = document.createElement("style");
|
|
1516
|
+
style.id = styleId;
|
|
1517
|
+
style.dataset.plugin = "@wenbin_wb/dsh-bridge";
|
|
1518
|
+
style.dataset.pluginCss = "@wenbin_wb/dsh-bridge/settings-nav-icon";
|
|
1519
|
+
style.textContent = getNavIconCss();
|
|
1520
|
+
document.head.appendChild(style);
|
|
1521
|
+
}
|
|
1522
|
+
let disposed = false;
|
|
1523
|
+
const sync = () => {
|
|
1524
|
+
if (disposed) return;
|
|
1525
|
+
const currentLabel = typeof resolveLabel === "function" ? resolveLabel().trim() : "\u8FDC\u7A0B\u8BBF\u95EE";
|
|
1526
|
+
const buttons = document.querySelectorAll('[role="dialog"] nav button, div[class*="VOzbGW_nav"] button');
|
|
1527
|
+
for (const button of buttons) {
|
|
1528
|
+
const text = button.textContent?.trim() || "";
|
|
1529
|
+
if (currentLabel.length > 0 && (text === currentLabel || text.includes(currentLabel))) {
|
|
1530
|
+
button.setAttribute(SETTINGS_NAV_MARKER, "");
|
|
1531
|
+
} else if (button.hasAttribute(SETTINGS_NAV_MARKER)) {
|
|
1532
|
+
button.removeAttribute(SETTINGS_NAV_MARKER);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
sync();
|
|
1537
|
+
let observer = null;
|
|
1538
|
+
if (typeof MutationObserver !== "undefined") {
|
|
1539
|
+
observer = new MutationObserver(sync);
|
|
1540
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
1541
|
+
}
|
|
1542
|
+
return () => {
|
|
1543
|
+
disposed = true;
|
|
1544
|
+
if (observer) observer.disconnect();
|
|
1545
|
+
document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`).forEach((el) => el.removeAttribute(SETTINGS_NAV_MARKER));
|
|
1546
|
+
const styleEl = document.getElementById(styleId);
|
|
1547
|
+
if (styleEl) styleEl.remove();
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1377
1551
|
// lib/bridge-rpc-constants.js
|
|
1378
1552
|
var BRIDGE_RPC_CHANNEL = "/dsh-bridge";
|
|
1379
1553
|
var BRIDGE_ENDPOINTS = {
|
|
@@ -4860,10 +5034,13 @@ function TabBar({ active, onChange, dots }) {
|
|
|
4860
5034
|
})
|
|
4861
5035
|
);
|
|
4862
5036
|
}
|
|
4863
|
-
function BridgePanel({ rpcCall }) {
|
|
5037
|
+
function BridgePanel({ rpcCall, preferredTab }) {
|
|
4864
5038
|
const [status, setStatus] = React.useState(null);
|
|
4865
5039
|
const [err, setErr] = React.useState(null);
|
|
4866
|
-
const [activeTab, setActiveTab] = React.useState("lan");
|
|
5040
|
+
const [activeTab, setActiveTab] = React.useState(preferredTab || "lan");
|
|
5041
|
+
React.useEffect(() => {
|
|
5042
|
+
if (preferredTab) setActiveTab(preferredTab);
|
|
5043
|
+
}, [preferredTab]);
|
|
4867
5044
|
const [platforms, setPlatforms] = React.useState(null);
|
|
4868
5045
|
const [selectedPlatform, setSelectedPlatform] = React.useState("wechat");
|
|
4869
5046
|
const [copiedUrl, setCopiedUrl] = React.useState("");
|
|
@@ -5645,6 +5822,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
5645
5822
|
}
|
|
5646
5823
|
function injectMobileStyles() {
|
|
5647
5824
|
if (typeof document === "undefined") return;
|
|
5825
|
+
if (shouldYieldMobileStyles()) return;
|
|
5648
5826
|
if (document.getElementById("dsh-bridge-mobile-styles")) return;
|
|
5649
5827
|
const style = document.createElement("style");
|
|
5650
5828
|
style.id = "dsh-bridge-mobile-styles";
|
|
@@ -5703,6 +5881,7 @@ function setupSettingsDrilldown() {
|
|
|
5703
5881
|
function setupMobileExperience(rpcCall, ctx) {
|
|
5704
5882
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
5705
5883
|
injectMobileStyles();
|
|
5884
|
+
if (shouldYieldMobileHeader()) return;
|
|
5706
5885
|
let header = document.querySelector(".dsh-mobile-app-header");
|
|
5707
5886
|
let titleEl = document.querySelector(".dsh-mobile-header-title");
|
|
5708
5887
|
if (!header) {
|
|
@@ -6659,8 +6838,9 @@ function setupIosKeyboardAdapter() {
|
|
|
6659
6838
|
});
|
|
6660
6839
|
}
|
|
6661
6840
|
function setupComposerCollapse() {
|
|
6662
|
-
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
6841
|
+
if (typeof window === "undefined" || typeof document === "undefined" || typeof MutationObserver === "undefined") return;
|
|
6663
6842
|
if (window.innerWidth > MOBILE_MAX_WIDTH) return;
|
|
6843
|
+
if (shouldYieldComposerCollapse()) return;
|
|
6664
6844
|
const LS_KEY = "dsh-composer-fold";
|
|
6665
6845
|
let bar = null;
|
|
6666
6846
|
let busy = false;
|
|
@@ -6793,6 +6973,11 @@ function setupComposerCollapse() {
|
|
|
6793
6973
|
function apply(ctx) {
|
|
6794
6974
|
window.__dshClientCtx = ctx;
|
|
6795
6975
|
setupSettingsDrilldown();
|
|
6976
|
+
if (typeof ctx.effect === "function") {
|
|
6977
|
+
ctx.effect(() => registerSettingsNavIcon(() => "\u8FDC\u7A0B\u8BBF\u95EE"), "dsh-bridge: settings nav icon");
|
|
6978
|
+
} else {
|
|
6979
|
+
registerSettingsNavIcon(() => "\u8FDC\u7A0B\u8BBF\u95EE");
|
|
6980
|
+
}
|
|
6796
6981
|
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
6797
6982
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) => showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
6798
6983
|
setupIosKeyboardAdapter();
|
|
@@ -6824,19 +7009,74 @@ function apply(ctx) {
|
|
|
6824
7009
|
);
|
|
6825
7010
|
})
|
|
6826
7011
|
);
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
7012
|
+
let settingsVisible = true;
|
|
7013
|
+
let sectionRegistration = null;
|
|
7014
|
+
const ensureSectionRegistered = () => {
|
|
7015
|
+
if (!settingsVisible || sectionRegistration) return;
|
|
7016
|
+
try {
|
|
7017
|
+
sectionRegistration = ctx.slots.register(
|
|
7018
|
+
{
|
|
7019
|
+
name: "settings.section",
|
|
7020
|
+
id: "dsh-bridge",
|
|
7021
|
+
order: 10,
|
|
7022
|
+
label: () => "\u8FDC\u7A0B\u8BBF\u95EE",
|
|
7023
|
+
inject: () => ({ rpcCall })
|
|
7024
|
+
},
|
|
7025
|
+
BridgePanel
|
|
7026
|
+
);
|
|
7027
|
+
} catch {
|
|
7028
|
+
}
|
|
7029
|
+
};
|
|
7030
|
+
const ensureSectionUnregistered = () => {
|
|
7031
|
+
if (!sectionRegistration) return;
|
|
7032
|
+
try {
|
|
7033
|
+
if (typeof sectionRegistration === "function") {
|
|
7034
|
+
sectionRegistration();
|
|
7035
|
+
} else if (typeof sectionRegistration.dispose === "function") {
|
|
7036
|
+
sectionRegistration.dispose();
|
|
7037
|
+
}
|
|
7038
|
+
} catch {
|
|
7039
|
+
}
|
|
7040
|
+
sectionRegistration = null;
|
|
7041
|
+
};
|
|
7042
|
+
const sectionGate = {
|
|
7043
|
+
visible: () => settingsVisible,
|
|
7044
|
+
setVisible: (visible) => {
|
|
7045
|
+
const next = !!visible;
|
|
7046
|
+
if (settingsVisible === next) return;
|
|
7047
|
+
settingsVisible = next;
|
|
7048
|
+
if (next) {
|
|
7049
|
+
ensureSectionRegistered();
|
|
7050
|
+
} else {
|
|
7051
|
+
ensureSectionUnregistered();
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
};
|
|
7055
|
+
ctx.slots.inject("settings.section", () => {
|
|
7056
|
+
if (settingsVisible) {
|
|
7057
|
+
ensureSectionRegistered();
|
|
7058
|
+
}
|
|
7059
|
+
});
|
|
7060
|
+
const bridgeControl = {
|
|
7061
|
+
version: 1,
|
|
7062
|
+
/** 把插件自己的面板交给宿主渲染(返回 React element) */
|
|
7063
|
+
render: (props = {}) => {
|
|
7064
|
+
return React.createElement(BridgePanel, {
|
|
7065
|
+
rpcCall,
|
|
7066
|
+
preferredTab: props.preferredTab,
|
|
7067
|
+
...props
|
|
7068
|
+
});
|
|
7069
|
+
},
|
|
7070
|
+
/** 宿主收编面板后,撤下设置页那条重复入口 */
|
|
7071
|
+
setSettingsVisible: (visible) => sectionGate.setVisible(visible),
|
|
7072
|
+
settingsVisible: () => sectionGate.visible(),
|
|
7073
|
+
/** 运行时动态特性开关配置 (Issue #52) */
|
|
7074
|
+
configureFeatures: (flags) => setRuntimeFeatureOverrides(flags),
|
|
7075
|
+
getFeatures: () => getRuntimeFeatureOverrides()
|
|
7076
|
+
};
|
|
7077
|
+
if (typeof ctx.provide === "function") {
|
|
7078
|
+
ctx.provide("dsh-bridge", bridgeControl);
|
|
7079
|
+
}
|
|
6840
7080
|
}
|
|
6841
7081
|
|
|
6842
7082
|
return module.exports;
|
package/client/index.js
CHANGED
|
@@ -5,6 +5,14 @@ import {
|
|
|
5
5
|
fetchLoopbackTokenOnce,
|
|
6
6
|
} from './unlock-manager.js'
|
|
7
7
|
import { hasOfficialDirectoryPicker, shouldYieldToOfficialPicker } from './picker-yield.js'
|
|
8
|
+
import {
|
|
9
|
+
shouldYieldMobileHeader,
|
|
10
|
+
shouldYieldMobileStyles,
|
|
11
|
+
shouldYieldComposerCollapse,
|
|
12
|
+
setRuntimeFeatureOverrides,
|
|
13
|
+
getRuntimeFeatureOverrides,
|
|
14
|
+
} from './mobile-yield.js'
|
|
15
|
+
import { registerSettingsNavIcon } from './settings-nav-icon.js'
|
|
8
16
|
// dsh-bridge 客户端插件:设置页「远程访问」面板
|
|
9
17
|
|
|
10
18
|
// 兼容非 HTTPS 环境(如手机局域网 HTTP 访问):为非安全上下文补齐 crypto.randomUUID
|
|
@@ -3081,10 +3089,13 @@ function TabBar({ active, onChange, dots }) {
|
|
|
3081
3089
|
|
|
3082
3090
|
// ---- 主面板 ----
|
|
3083
3091
|
|
|
3084
|
-
function BridgePanel({ rpcCall }) {
|
|
3092
|
+
function BridgePanel({ rpcCall, preferredTab }) {
|
|
3085
3093
|
const [status, setStatus] = React.useState(null);
|
|
3086
3094
|
const [err, setErr] = React.useState(null);
|
|
3087
|
-
const [activeTab, setActiveTab] = React.useState('lan');
|
|
3095
|
+
const [activeTab, setActiveTab] = React.useState(preferredTab || 'lan');
|
|
3096
|
+
React.useEffect(() => {
|
|
3097
|
+
if (preferredTab) setActiveTab(preferredTab);
|
|
3098
|
+
}, [preferredTab]);
|
|
3088
3099
|
// 平台列表和连接状态
|
|
3089
3100
|
const [platforms, setPlatforms] = React.useState(null);
|
|
3090
3101
|
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
@@ -3819,6 +3830,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3819
3830
|
|
|
3820
3831
|
function injectMobileStyles() {
|
|
3821
3832
|
if (typeof document === 'undefined') return;
|
|
3833
|
+
if (shouldYieldMobileStyles()) return;
|
|
3822
3834
|
if (document.getElementById('dsh-bridge-mobile-styles')) return;
|
|
3823
3835
|
|
|
3824
3836
|
const style = document.createElement('style');
|
|
@@ -3915,6 +3927,7 @@ function setupSettingsDrilldown() {
|
|
|
3915
3927
|
function setupMobileExperience(rpcCall, ctx) {
|
|
3916
3928
|
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
3917
3929
|
injectMobileStyles();
|
|
3930
|
+
if (shouldYieldMobileHeader()) return;
|
|
3918
3931
|
|
|
3919
3932
|
// 1. 创建顶部 DeepSeek App 风格导航条 (Header: 左侧双横线,中间当前会话标题,右侧(+))
|
|
3920
3933
|
let header = document.querySelector('.dsh-mobile-app-header');
|
|
@@ -5067,8 +5080,9 @@ function setupIosKeyboardAdapter() {
|
|
|
5067
5080
|
// 中间消息可视区很小。折叠后 composer 隐藏、消息 viewArea 自动伸展全高,阅读区显著增大。
|
|
5068
5081
|
// 折叠态记忆到 localStorage(dsh-composer-fold),跨会话保持用户偏好。
|
|
5069
5082
|
function setupComposerCollapse() {
|
|
5070
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
5083
|
+
if (typeof window === 'undefined' || typeof document === 'undefined' || typeof MutationObserver === 'undefined') return;
|
|
5071
5084
|
if (window.innerWidth > MOBILE_MAX_WIDTH) return; // 仅移动端
|
|
5085
|
+
if (shouldYieldComposerCollapse()) return;
|
|
5072
5086
|
const LS_KEY = 'dsh-composer-fold';
|
|
5073
5087
|
|
|
5074
5088
|
let bar = null; // 折叠态底部"点击输入"细条
|
|
@@ -5197,6 +5211,12 @@ function apply(ctx) {
|
|
|
5197
5211
|
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5198
5212
|
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5199
5213
|
setupSettingsDrilldown();
|
|
5214
|
+
// 设置页左侧导航栏图标定制:将原生通用齿轮替换为远程访问专属图标
|
|
5215
|
+
if (typeof ctx.effect === 'function') {
|
|
5216
|
+
ctx.effect(() => registerSettingsNavIcon(() => '远程访问'), 'dsh-bridge: settings nav icon');
|
|
5217
|
+
} else {
|
|
5218
|
+
registerSettingsNavIcon(() => '远程访问');
|
|
5219
|
+
}
|
|
5200
5220
|
const rpcCall = (endpoint, payload, signal) =>
|
|
5201
5221
|
ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
5202
5222
|
|
|
@@ -5245,18 +5265,80 @@ function apply(ctx) {
|
|
|
5245
5265
|
}),
|
|
5246
5266
|
);
|
|
5247
5267
|
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5268
|
+
// 设置页条目动态门控与可见性管理 (Issue #51)
|
|
5269
|
+
let settingsVisible = true;
|
|
5270
|
+
let sectionRegistration = null;
|
|
5271
|
+
|
|
5272
|
+
const ensureSectionRegistered = () => {
|
|
5273
|
+
if (!settingsVisible || sectionRegistration) return;
|
|
5274
|
+
try {
|
|
5275
|
+
sectionRegistration = ctx.slots.register(
|
|
5276
|
+
{
|
|
5277
|
+
name: 'settings.section',
|
|
5278
|
+
id: 'dsh-bridge',
|
|
5279
|
+
order: 10,
|
|
5280
|
+
label: () => '远程访问',
|
|
5281
|
+
inject: () => ({ rpcCall }),
|
|
5282
|
+
},
|
|
5283
|
+
BridgePanel,
|
|
5284
|
+
);
|
|
5285
|
+
} catch {}
|
|
5286
|
+
};
|
|
5287
|
+
|
|
5288
|
+
const ensureSectionUnregistered = () => {
|
|
5289
|
+
if (!sectionRegistration) return;
|
|
5290
|
+
try {
|
|
5291
|
+
if (typeof sectionRegistration === 'function') {
|
|
5292
|
+
sectionRegistration();
|
|
5293
|
+
} else if (typeof sectionRegistration.dispose === 'function') {
|
|
5294
|
+
sectionRegistration.dispose();
|
|
5295
|
+
}
|
|
5296
|
+
} catch {}
|
|
5297
|
+
sectionRegistration = null;
|
|
5298
|
+
};
|
|
5299
|
+
|
|
5300
|
+
const sectionGate = {
|
|
5301
|
+
visible: () => settingsVisible,
|
|
5302
|
+
setVisible: (visible) => {
|
|
5303
|
+
const next = !!visible;
|
|
5304
|
+
if (settingsVisible === next) return;
|
|
5305
|
+
settingsVisible = next;
|
|
5306
|
+
if (next) {
|
|
5307
|
+
ensureSectionRegistered();
|
|
5308
|
+
} else {
|
|
5309
|
+
ensureSectionUnregistered();
|
|
5310
|
+
}
|
|
5311
|
+
},
|
|
5312
|
+
};
|
|
5313
|
+
|
|
5314
|
+
ctx.slots.inject('settings.section', () => {
|
|
5315
|
+
if (settingsVisible) {
|
|
5316
|
+
ensureSectionRegistered();
|
|
5317
|
+
}
|
|
5318
|
+
});
|
|
5319
|
+
|
|
5320
|
+
// 暴露客户端反射控制面 (Issue #51)
|
|
5321
|
+
const bridgeControl = {
|
|
5322
|
+
version: 1,
|
|
5323
|
+
/** 把插件自己的面板交给宿主渲染(返回 React element) */
|
|
5324
|
+
render: (props = {}) => {
|
|
5325
|
+
return React.createElement(BridgePanel, {
|
|
5326
|
+
rpcCall,
|
|
5327
|
+
preferredTab: props.preferredTab,
|
|
5328
|
+
...props,
|
|
5329
|
+
});
|
|
5330
|
+
},
|
|
5331
|
+
/** 宿主收编面板后,撤下设置页那条重复入口 */
|
|
5332
|
+
setSettingsVisible: (visible) => sectionGate.setVisible(visible),
|
|
5333
|
+
settingsVisible: () => sectionGate.visible(),
|
|
5334
|
+
/** 运行时动态特性开关配置 (Issue #52) */
|
|
5335
|
+
configureFeatures: (flags) => setRuntimeFeatureOverrides(flags),
|
|
5336
|
+
getFeatures: () => getRuntimeFeatureOverrides(),
|
|
5337
|
+
};
|
|
5338
|
+
|
|
5339
|
+
if (typeof ctx.provide === 'function') {
|
|
5340
|
+
ctx.provide('dsh-bridge', bridgeControl);
|
|
5341
|
+
}
|
|
5260
5342
|
}
|
|
5261
5343
|
|
|
5262
5344
|
export { name, inject, apply };
|
package/client/mobile-styles.js
CHANGED
|
@@ -47,7 +47,12 @@ export const MOBILE_STYLES_CSS = `
|
|
|
47
47
|
弹窗之上并盖住设置页。这是「模态就该盖在设置弹窗之上」的既定取舍:宁可让真模态
|
|
48
48
|
可见,也不接受"弹了却点不到"。独立验收已实测该构造会命中;若将来真踩到,
|
|
49
49
|
应改为按「该浮层是否可关闭」进一步收窄,而不是回退这条规则。 */
|
|
50
|
-
|
|
50
|
+
/* ⚠️ 必须排除设置弹窗自身:DSH 0.1.7 把 VOzbGW_overlay 变成了 <body> 的直接子级
|
|
51
|
+
(0.1.5 里它嵌在 #root 内部),它同样满足「body 子级 + 含 role=dialog 子元素」,
|
|
52
|
+
会被本条一并抬到 10050 —— 于是与第三方模态根同层、退化成按 DOM 顺序决胜,
|
|
53
|
+
刚修好的「安装确认框被盖住」就可能复发。:not(...) 把它排除,让设置弹窗继续由
|
|
54
|
+
它自己的 10002 规则定位,本条只抬**其它**顶层模态。 */
|
|
55
|
+
body:has(div[class*="VOzbGW_overlay"]) > div:has(> div[class*="mask"], > div[role="dialog"]):not([class*="VOzbGW_overlay"]) {
|
|
51
56
|
z-index: 10050 !important;
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -73,8 +78,17 @@ export const MOBILE_STYLES_CSS = `
|
|
|
73
78
|
(dsh-client-ui-sidebar-right/lib/client.js),故移动端样式取 <=767px,
|
|
74
79
|
768px 起彻底交还桌面布局,避免"桥渲染顶栏、宿主却未全屏"的错位。 */
|
|
75
80
|
@media (max-width: 767px) {
|
|
76
|
-
/* 1. 主框架为 Header
|
|
77
|
-
|
|
81
|
+
/* 1. 主框架为 Header 腾出顶部空间。
|
|
82
|
+
|
|
83
|
+
选择器必须限定在布局外壳(dsh-client-ui-layout 的 AppFrame,即
|
|
84
|
+
[data-slot="root"] 的直接子元素)。宿主的 CSS-module 哈希只保证同文件
|
|
85
|
+
内唯一,*="_frame" 是跨包通配:聊天记录容器(dsh-client-ui-chat 的
|
|
86
|
+
.EvIC1a_frame,同样以 _frame 结尾)也会被命中。一旦命中,它被强加
|
|
87
|
+
height:100dvh + overflow:hidden,在手机上被钳到视口高度并裁掉溢出内容:
|
|
88
|
+
外层 [data-conversation-scroll] 已滚到底、内部却还有数千 px 读不到,
|
|
89
|
+
表现为「看不到最新消息」。官方给该容器的规则是 flex:none;height:auto
|
|
90
|
+
(让记录随内容自然增高、由外层统一滚动),与这里冲突,故必须排除。 */
|
|
91
|
+
[data-slot="root"] > div[class*="_frame"] {
|
|
78
92
|
display: flex !important;
|
|
79
93
|
flex-direction: column !important;
|
|
80
94
|
width: 100vw !important;
|