@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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// dsh-bridge 移动端体验与界面改动层的「让位与开关」机制 (Issue #52)
|
|
2
|
+
//
|
|
3
|
+
// 背景:
|
|
4
|
+
// dsh-bridge 为了在移动端网页访问(纯手机浏览器/局域网扫码)时提供类 App 的移动交互,
|
|
5
|
+
// 自建了 body 级导航条(.dsh-mobile-app-header)、输入框折叠按钮以及一套含 !important
|
|
6
|
+
// 的移动端全局覆盖样式。
|
|
7
|
+
//
|
|
8
|
+
// 但在以下环境,这一层自绘会引发严重冲突:
|
|
9
|
+
// 1. Android 原生 WebView 壳(如 com.dsharnessmobile.shell),壳层自身已有原生导航栏与手势;
|
|
10
|
+
// 2. 页面已安装专门的响应式插件(如 @dsh-android/dsh-client-ui-responsive),争抢同层 DOM;
|
|
11
|
+
// 3. 桌面端宿主(如 Tauri 桌面端 / DSH Desktop),无需任何移动端 DOM 侵入。
|
|
12
|
+
//
|
|
13
|
+
// 本模块提供「多级开关 + 自动特征探测让位」:
|
|
14
|
+
// 优先级:URL 参数 > 宿主全局注入配置 (__DSH_BRIDGE_CONFIG__) > 运行时动态配置 > 本地存储 > 自动探测兜底
|
|
15
|
+
|
|
16
|
+
/** 特性名称常量 */
|
|
17
|
+
export const FEATURES = {
|
|
18
|
+
MOBILE_HEADER: 'mobileHeader',
|
|
19
|
+
MOBILE_STYLES: 'mobileStyles',
|
|
20
|
+
COMPOSER_COLLAPSE: 'composerCollapse',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 内存中的动态运行时覆盖配置(可通过 ctx.reflect.get('dsh-bridge').configureFeatures(...) 设置)
|
|
24
|
+
let runtimeFeatureOverrides = {};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 设置运行时特性覆盖(供 Cordis 反射服务调用)
|
|
28
|
+
* @param {Record<string, boolean>} overrides
|
|
29
|
+
*/
|
|
30
|
+
export function setRuntimeFeatureOverrides(overrides) {
|
|
31
|
+
if (overrides && typeof overrides === 'object') {
|
|
32
|
+
runtimeFeatureOverrides = { ...runtimeFeatureOverrides, ...overrides };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 获取当前运行时特性状态
|
|
38
|
+
* @returns {Record<string, boolean>}
|
|
39
|
+
*/
|
|
40
|
+
export function getRuntimeFeatureOverrides() {
|
|
41
|
+
return { ...runtimeFeatureOverrides };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 重置运行时特性覆盖(主要用于测试)
|
|
46
|
+
*/
|
|
47
|
+
export function resetRuntimeFeatureOverrides() {
|
|
48
|
+
runtimeFeatureOverrides = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 探测是否处于已具备自适应/原生移动顶栏的环境中
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function detectMobileShellOrResponsivePlugin() {
|
|
56
|
+
if (typeof window === 'undefined') return false;
|
|
57
|
+
|
|
58
|
+
// 1. 移动端原生/混合壳显式标记
|
|
59
|
+
if (window.__DSH_MOBILE_SHELL__ || window.__TAURI__ || window.__DSH_DESKTOP__) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. 已加载的移动端自适应插件标记
|
|
64
|
+
if (window.__DSH_CLIENT_UI_RESPONSIVE__) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. DOM 标记探测(如 @dsh-android/dsh-client-ui-responsive 挂载的特征属性)
|
|
69
|
+
if (typeof document !== 'undefined') {
|
|
70
|
+
if (document.querySelector('[data-dsh-responsive-mobile]') ||
|
|
71
|
+
document.querySelector('[data-dsh-mobile-shell]')) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 探测页面是否已有主顶栏(避免 body 级双顶栏叠加)
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
export function detectExistingHeader() {
|
|
84
|
+
if (typeof document === 'undefined') return false;
|
|
85
|
+
// 如果页面中已经有 #root 下的 header 或 banner 角色元素
|
|
86
|
+
return !!document.querySelector('#root header, [role="banner"]:not(.dsh-mobile-app-header)');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 解析特性开关(返回 true 表示启用,false 表示禁用/让位)
|
|
91
|
+
*
|
|
92
|
+
* @param {string} featureName 特性名称
|
|
93
|
+
* @param {() => boolean} defaultDetector 自动探测兜底函数
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isFeatureEnabled(featureName, defaultDetector) {
|
|
97
|
+
if (typeof window === 'undefined') {
|
|
98
|
+
return typeof defaultDetector === 'function' ? defaultDetector() : true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 1. URL 应急排障参数(最高优先级,支持 ?dsh_bridge_tweaks=0 或 ?dsh_mobile=off 批量禁用)
|
|
102
|
+
try {
|
|
103
|
+
if (window.location && window.location.search) {
|
|
104
|
+
const params = new URLSearchParams(window.location.search);
|
|
105
|
+
// 总开关:禁用所有页面微调
|
|
106
|
+
const tweaksVal = params.get('dsh_bridge_tweaks') ?? params.get('dsh_mobile_tweaks');
|
|
107
|
+
if (tweaksVal === '0' || tweaksVal === 'false' || tweaksVal === 'off') {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (tweaksVal === '1' || tweaksVal === 'true' || tweaksVal === 'on') {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 单项开关:如 ?dsh_mobile_header=0, ?dsh_mobile_styles=0, ?dsh_composer_collapse=0
|
|
115
|
+
const kebabName = featureName.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
|
116
|
+
const specificVal = params.get(`dsh_${kebabName}`) ?? params.get(`dsh_${featureName}`);
|
|
117
|
+
if (specificVal === '0' || specificVal === 'false' || specificVal === 'off') {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (specificVal === '1' || specificVal === 'true' || specificVal === 'on') {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
|
|
126
|
+
// 2. 宿主注入的全局配置对象 window.__DSH_BRIDGE_CONFIG__
|
|
127
|
+
try {
|
|
128
|
+
const hostConfig = window.__DSH_BRIDGE_CONFIG__;
|
|
129
|
+
if (hostConfig && typeof hostConfig === 'object') {
|
|
130
|
+
if (hostConfig.disablePageTweaks === true || hostConfig.disableMobileTweaks === true) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (typeof hostConfig[featureName] === 'boolean') {
|
|
134
|
+
return hostConfig[featureName];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
// 3. 动态运行时配置(通过反射 API 设置)
|
|
140
|
+
if (typeof runtimeFeatureOverrides[featureName] === 'boolean') {
|
|
141
|
+
return runtimeFeatureOverrides[featureName];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 4. 本地持久化偏好 (localStorage)
|
|
145
|
+
try {
|
|
146
|
+
if (window.localStorage) {
|
|
147
|
+
const stored = window.localStorage.getItem(`dsh_bridge:feature:${featureName}`);
|
|
148
|
+
if (stored === '0' || stored === 'false') return false;
|
|
149
|
+
if (stored === '1' || stored === 'true') return true;
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
|
|
153
|
+
// 5. 自动探测兜底
|
|
154
|
+
if (typeof defaultDetector === 'function') {
|
|
155
|
+
return defaultDetector();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 是否应该让位自绘顶栏 (返回 true 表示让位/不自建顶栏)
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
export function shouldYieldMobileHeader() {
|
|
166
|
+
return !isFeatureEnabled(FEATURES.MOBILE_HEADER, () => {
|
|
167
|
+
// 自动判定:如果是移动端壳层/已有自适应插件/页面已有顶栏,则主动让位
|
|
168
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 是否应该让位移动端全局样式 (返回 true 表示让位/不注入样式)
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
export function shouldYieldMobileStyles() {
|
|
177
|
+
return !isFeatureEnabled(FEATURES.MOBILE_STYLES, () => {
|
|
178
|
+
// 自动判定:如果已被外部响应式插件完全接管,则让位全局样式
|
|
179
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 是否应该让位输入框折叠按钮 (返回 true 表示让位/不挂载折叠按钮)
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
export function shouldYieldComposerCollapse() {
|
|
188
|
+
return !isFeatureEnabled(FEATURES.COMPOSER_COLLAPSE, () => {
|
|
189
|
+
// 自动判定:移动端壳层下输入框折叠按钮容易与原生交互冲突,让位
|
|
190
|
+
return !detectMobileShellOrResponsivePlugin();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// dsh-bridge 设置页左侧导航栏图标定制 (替换默认小齿轮)
|
|
2
|
+
//
|
|
3
|
+
// 背景:
|
|
4
|
+
// DSH 0.1.x 的设置弹窗外壳(@deepseek-ai/dsh-client-ui-settings-general)仅内置了原生
|
|
5
|
+
// 三个 section 的图标(models, agent-presets, plugins),对第三方插件注册的 settings.section,
|
|
6
|
+
// 外壳一律 fallback 显示其默认的小齿轮图标(gear)。
|
|
7
|
+
//
|
|
8
|
+
// 解决方案(对齐 dshmarket 与 dsh-better-sidebar):
|
|
9
|
+
// 监听设置弹窗挂载,定位到文本匹配「远程访问」的 nav 按钮,通过 CSS mask 将原生齿轮
|
|
10
|
+
// 替换为专属的「手机 + 远程无线连接」矢量图标,颜色跟随 currentColor 保持原生悬停与激活态一致。
|
|
11
|
+
|
|
12
|
+
export const SETTINGS_NAV_MARKER = 'data-dsh-bridge-settings-nav';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 远程访问专属图标:手机 + 无线连接电波 (24x24 viewBox)
|
|
16
|
+
*/
|
|
17
|
+
export const 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>`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 生成导航栏图标样式
|
|
21
|
+
* @param {string} [customSvg] 可选自定义 SVG 字符串
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function getNavIconCss(customSvg = REMOTE_NAV_SVG) {
|
|
25
|
+
const maskUrl = `data:image/svg+xml,${encodeURIComponent(customSvg)}`;
|
|
26
|
+
return `
|
|
27
|
+
[${SETTINGS_NAV_MARKER}] > svg:first-child {
|
|
28
|
+
display: none !important;
|
|
29
|
+
}
|
|
30
|
+
[${SETTINGS_NAV_MARKER}]::before {
|
|
31
|
+
content: '';
|
|
32
|
+
flex: none;
|
|
33
|
+
width: 16px;
|
|
34
|
+
height: 16px;
|
|
35
|
+
background-color: currentColor;
|
|
36
|
+
-webkit-mask: url("${maskUrl}") center / contain no-repeat;
|
|
37
|
+
mask: url("${maskUrl}") center / contain no-repeat;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 注册设置页导航图标适配器
|
|
44
|
+
*
|
|
45
|
+
* @param {() => string} [resolveLabel] 获取当前标签文本的回调
|
|
46
|
+
* @returns {() => void} 清理函数
|
|
47
|
+
*/
|
|
48
|
+
export function registerSettingsNavIcon(resolveLabel = () => '远程访问') {
|
|
49
|
+
if (typeof document === 'undefined') return () => {};
|
|
50
|
+
|
|
51
|
+
const styleId = 'dsh-bridge-settings-nav-icon-style';
|
|
52
|
+
if (!document.getElementById(styleId)) {
|
|
53
|
+
const style = document.createElement('style');
|
|
54
|
+
style.id = styleId;
|
|
55
|
+
style.dataset.plugin = '@wenbin_wb/dsh-bridge';
|
|
56
|
+
style.dataset.pluginCss = '@wenbin_wb/dsh-bridge/settings-nav-icon';
|
|
57
|
+
style.textContent = getNavIconCss();
|
|
58
|
+
document.head.appendChild(style);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let disposed = false;
|
|
62
|
+
const sync = () => {
|
|
63
|
+
if (disposed) return;
|
|
64
|
+
const currentLabel = typeof resolveLabel === 'function' ? resolveLabel().trim() : '远程访问';
|
|
65
|
+
const buttons = document.querySelectorAll('[role="dialog"] nav button, div[class*="VOzbGW_nav"] button');
|
|
66
|
+
for (const button of buttons) {
|
|
67
|
+
const text = button.textContent?.trim() || '';
|
|
68
|
+
if (currentLabel.length > 0 && (text === currentLabel || text.includes(currentLabel))) {
|
|
69
|
+
button.setAttribute(SETTINGS_NAV_MARKER, '');
|
|
70
|
+
} else if (button.hasAttribute(SETTINGS_NAV_MARKER)) {
|
|
71
|
+
button.removeAttribute(SETTINGS_NAV_MARKER);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
sync();
|
|
77
|
+
let observer = null;
|
|
78
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
79
|
+
observer = new MutationObserver(sync);
|
|
80
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
disposed = true;
|
|
85
|
+
if (observer) observer.disconnect();
|
|
86
|
+
document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`).forEach((el) => el.removeAttribute(SETTINGS_NAV_MARKER));
|
|
87
|
+
const styleEl = document.getElementById(styleId);
|
|
88
|
+
if (styleEl) styleEl.remove();
|
|
89
|
+
};
|
|
90
|
+
}
|
package/docs/release-process.md
CHANGED
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
|
|
21
21
|
1. **改版本**:`package.json` 的 `version` + `releaseNotes`
|
|
22
22
|
2. **写 CHANGELOG**:`CHANGELOG.md` 顶部新增对应版本条目
|
|
23
|
-
3.
|
|
23
|
+
3. **构建与测试**:
|
|
24
|
+
- `npm run build:client`(**必须**:客户端产物要与源码同步,`npm test` 有一项断言会校验产物内嵌的 CSS 与源码逐字一致)
|
|
25
|
+
- `npm test` + `npm run lint`
|
|
26
|
+
- `npm run verify:mobile-ui`(**发布前必做**:跑真实 GUI 的移动端行为验收,6 个套件。需要 dsh web 正在运行;Chrome 路径可用 `PUPPETEER_EXECUTABLE_PATH` 指定。它不在 CI 里跑,因为 CI 没有宿主)
|
|
27
|
+
- `npm run build:banner`(**仅当 `docs/screenshots/` 素材有变、需要重出 README banner 时执行**;输入没变时应跳过,否则只会产生 JPEG 重编码噪声。注意该脚本默认 Chrome 路径是写死的 Windows 路径,需用 `PUPPETEER_EXECUTABLE_PATH` 覆盖)
|
|
28
|
+
- `npm run build:lark`(**仅在需要重建飞书 vendor bundle 时执行**。它用当前 esbuild 重新打包 `lib/feishu/lark-bundled.mjs`,产物会随 esbuild 版本漂移;**不要放进 `prepack`**,否则每次 `npm publish` 都会让线上 tarball 里的 vendor 文件与 tag 里的不一致)
|
|
24
29
|
4. **提交**:合并到 `main`(fast-forward),提交信息 `release: vX.Y.Z — ...`
|
|
25
30
|
5. **打 tag**:`git tag vX.Y.Z && git push origin vX.Y.Z`(触发 GitHub Actions 自动创建 Release)
|
|
26
31
|
6. **npm 发布**:`npm publish`(prepack 自动构建产物)
|