dsh-mobile-ui 0.1.0

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/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # dsh-mobile-ui
2
+
3
+ Mobile UI enhancements for the DeepSeek Harness Web GUI — turns dsh into a real mobile experience on phones (≤768px viewport):
4
+
5
+ - **Responsive layout** — hides the left icon rail, content goes full-width; session list opens as an overlay drawer
6
+ - **Touch-optimized** — 44px touch targets (iOS HIG), no double-tap zoom delay, no tap highlight, 16px inputs (prevents iOS focus auto-zoom)
7
+ - **Reading enhancements** — message spacing, bubble width, and helper text (tool rows / context-injection / status stats) scaled for small screens
8
+ - **Right-top menu** — the drawer (new session / sessions / settings) opens from a floating round button
9
+ - **Safe-area aware** — `env(safe-area-inset-*)` support for notched displays
10
+ - **Zero desktop impact** — every enhancement lives inside a `max-width: 768px` media query
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ # from the plugin source directory
16
+ bash install.sh # install (idempotent)
17
+ bash install.sh --restart # install + restart dsh web (headless smoke test; aborts on plugin problems)
18
+ bash install.sh --uninstall # remove completely
19
+ ```
20
+
21
+ Or via npm:
22
+
23
+ ```bash
24
+ dsh plugin --profile web add dsh-mobile-ui
25
+ ```
26
+
27
+ What the installer does:
28
+ 1. Source kept at `~/.dsh/plugins/dsh-mobile-ui/`
29
+ 2. Runtime copy at `~/.dsh/profiles/node_modules/dsh-mobile-ui/`
30
+ 3. Web profile bundle wiring (`cordis.patch.yml` insert)
31
+
32
+ ## Development
33
+
34
+ ```bash
35
+ bash scripts/build.sh # syntax + contract preflight (incl. classic-script check)
36
+ bash scripts/package.sh # build dist/dsh-mobile-ui-install.tar.gz
37
+ ```
38
+
39
+ Fast local iteration: copy to `~/.dsh/profiles/node_modules/dsh-mobile-ui/` and refresh the page (client plugins have an HMR channel — client.js edits apply on refresh).
40
+
41
+ ## How it works
42
+
43
+ - **CSS layer**: injects `style[data-plugin-css=dsh-mobile-ui]`, all rules wrapped in `@media (max-width:768px)` — desktop loads none of it
44
+ - **JS layer**: `matchMedia`-driven; on narrow screens the main grid goes single-column, the sidebar is hidden and becomes a fixed overlay drawer with scrim
45
+ - **QA card** (`ask_user_question`, Mbwy4a component): JS detects the card and pins it as a floating panel (`position:fixed; top:100px`) — no `:has()` dependency (works in WeChat/X5-style engines); the footer button row wraps when narrow, so the submit button is always visible; conversation stays visible below
46
+
47
+ ## Maintenance notes (important)
48
+
49
+ - dsh frontend class names are build artifacts (hash prefixes). After a dsh upgrade, if selectors break:
50
+ 1. Verify `body.dsh-mobile-ui` and `#dsh-mobile-menu-btn` exist in the console
51
+ 2. Breakage usually hits layout classes (`[class*=frame]` etc.) — adjust CSS/JS to the new prefix
52
+ 3. Re-run `bash install.sh` after fixing
53
+ - Layout detection uses structural heuristics ("3-column grid + 56px first column"), not hard-coded class names, and buttons prefer `aria-label` — this tolerates class-name drift
54
+ - All DOM work is try/catch wrapped; any failure degrades silently (CSS layer still applies)
55
+ - **Never move React-rendered DOM nodes with JS** (`insertBefore`/`appendChild` on `pXSMma_root` etc.): React's fiber tree still records the old parent, so a re-render throws `removeChild` NotFoundError and the whole conversation view unmounts (blank page). Express every layout need in CSS (flex / order / :has) — the hero title pinning and the QA card are pure CSS for this reason.
56
+
57
+ ## Rollback
58
+
59
+ `bash install.sh --uninstall` + restart dsh removes everything (no residual styles/DOM).
60
+
61
+ ## License
62
+
63
+ MIT. Chinese documentation: [README.zh.md](README.zh.md)
64
+
65
+ ---
66
+
67
+ *Part of [dsh-plugins](https://github.com/TZHR-invest/dsh-plugins) — a small monorepo of DSH plugins: [dsh-lan-gateway](packages/dsh-lan-access), [dsh-vision-tool](packages/dsh-vision), [dsh-mobile-ui](packages/dsh-mobile-ui).*
package/README.zh.md ADDED
@@ -0,0 +1,60 @@
1
+ # dsh-mobile-ui
2
+
3
+ dsh web 移动端 UI 优化插件:在手机(≤768px 视口)上把 dsh 变成真正的移动端体验。
4
+
5
+ - **响应式布局**:隐藏左侧图标栏,内容全宽;会话列表以抽屉形式展开
6
+ - **右上角菜单按钮**:新建 / 会话列表 / 设置 全部入口收纳进抽屉
7
+ - **阅读增强**:消息间距、气泡宽度、辅助文本(工具行/上下文注入/状态统计)字号提升
8
+ - **触摸优化**:防双击缩放延迟、点击高亮去除、输入框 16px(防 iOS focus 自动缩放)
9
+ - **安全区适配**:env(safe-area-inset-*) 支持刘海屏
10
+ - **桌面零影响**:所有增强仅在 ≤768px 媒体查询下生效
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ # 在插件源码目录
16
+ bash install.sh # 安装(幂等)
17
+ bash install.sh --restart # 安装并重启 dsh web(含 headless 冒烟,插件问题自动中止)
18
+ bash install.sh --uninstall # 卸载
19
+ ```
20
+
21
+ 安装内容:
22
+ 1. 源码留档 `~/.dsh/plugins/dsh-mobile-ui/`
23
+ 2. 运行副本 `~/.dsh/profiles/node_modules/dsh-mobile-ui/`
24
+ 3. web profile 组合接线(`cordis.patch.yml` 插入插件行)
25
+
26
+ ## 开发与验证
27
+
28
+ ```bash
29
+ bash scripts/build.sh # 语法 + 契约预检(含 classic-script 校验)
30
+ bash scripts/package.sh # 打包 dist/dsh-mobile-ui-install.tar.gz
31
+ ```
32
+
33
+ 本地快速迭代:复制到 `~/.dsh/profiles/node_modules/dsh-mobile-ui/` 后刷新页面
34
+ (客户端插件有 HMR 通道,改 client.js 后页面自动更新)。
35
+
36
+ ## 工作原理
37
+
38
+ - **CSS 层**:注入 style[data-plugin-css=dsh-mobile-ui],全部规则包在
39
+ @media (max-width:768px) 内,桌面端不加载任何效果。
40
+ - **JS 层**:matchMedia 驱动;窄屏时把主布局 grid 改为单列、隐藏侧边栏,
41
+ 注入右上角菜单按钮 + 遮罩;菜单把侧边栏临时变为 fixed 抽屉(overlay)。
42
+ - **hero 标题置顶**:纯 CSS 实现——composerHero 栈在 hero 态撑满视口
43
+ (flex:1),输入卡 margin-top:auto 沉底,标题/工作区行自然留在顶部。
44
+
45
+ ## 维护须知(重要)
46
+
47
+ - dsh 前端 CSS 类名是构建产物(hash 前缀),升级 dsh 后若选择器失效:
48
+ 1. 浏览器控制台确认 body.dsh-mobile-ui 类与 #dsh-mobile-tabbar 存在;
49
+ 2. 失效通常发生在布局类([class*=frame] 等),按新版类名前缀微调 CSS/JS;
50
+ 3. 修好后重新 bash install.sh。
51
+ - 布局探测采用「三列 grid + 首列 56px」结构识别(非类名硬编码),
52
+ 对类名漂移有一定容错;按钮转发优先 aria-label(新建会话 等)。
53
+ - **严禁用 JS 移动 React 渲染的 DOM 节点**(insertBefore/appendChild 搬动
54
+ pXSMma_root 等):React fiber 树仍记录旧父节点,重渲染时 removeChild 抛
55
+ NotFoundError,整个会话视图被卸载 → 页面空白。布局需求一律用 CSS
56
+ (flex/order/:has)表达;hero 标题置顶即纯 CSS 实现。
57
+
58
+ ## 回滚
59
+
60
+ bash install.sh --uninstall 后重启 dsh 即完全移除(不残留样式/导航 DOM)。
package/client.js ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * dsh-mobile-ui — 浏览器端 bundle(classic script)。
3
+ *
4
+ * 移动端(≤768px 视口)体验增强,两层:
5
+ * 1. CSS 层:响应式阅读/触摸/间距/安全区优化(桌面端完全不受影响);
6
+ * 2. JS 层:隐藏左侧图标栏释放全宽 + 底部导航栏(新建/会话/设置),
7
+ * 会话列表以抽屉(drawer)形式展开。
8
+ *
9
+ * 防崩纪律(MR-022/023):
10
+ * - 本 bundle 无顶层 import/export,由 dsh web 按 classic script 加载;
11
+ * - 全部 DOM 操作 try/catch,任何失败静默降级(CSS 层仍生效);
12
+ * - 选择器优先 aria-label,其次词根类名前缀匹配([class*=xxx]),
13
+ * 降低 dsh 升级后 hash 类名漂移导致的失效风险。
14
+ */
15
+ window.__ModuleLoader__.load({
16
+ id: "dsh-mobile-ui",
17
+ factory: (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
+
22
+ /* ═══════════════════ CSS 层 ═══════════════════ */
23
+ var MOBILE_CSS = [
24
+ "/* dsh-mobile-ui 移动端增强(≤768px 生效,桌面不受影响) */",
25
+ /* 右上角菜单按钮(入口:打开会话抽屉——抽屉内含新建/会话列表/设置) */
26
+ "#dsh-mobile-menu-btn{position:fixed;top:calc(48px + env(safe-area-inset-top,0px));right:12px;z-index:2147483000;display:none;width:42px;height:42px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,rgba(22,23,27,.94));border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.14));color:var(--dsw-alias-label-primary,rgba(255,255,255,.88));align-items:center;justify-content:center;cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);box-shadow:0 2px 12px rgba(0,0,0,.3);transition:transform .12s ease}",
27
+ "#dsh-mobile-menu-btn:active{transform:scale(.92)}",
28
+ "#dsh-mobile-menu-btn svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}",
29
+ /* 抽屉遮罩 */
30
+ "#dsh-mobile-scrim{position:fixed;inset:0;z-index:2147482990;background:rgba(0,0,0,.55);display:none;opacity:0;transition:opacity .2s ease}",
31
+ "#dsh-mobile-scrim.dsh-mobile-visible{display:block;opacity:1}",
32
+ /* 会话抽屉(sidebar 变身 overlay) */
33
+ "body.dsh-mobile-ui [class*=sidebarCol].dsh-mobile-drawer{display:block !important;position:fixed !important;top:0;left:0;bottom:0;width:min(84vw,320px) !important;z-index:2147482995;box-shadow:4px 0 24px rgba(0,0,0,.35);overflow-y:auto;animation:dsh-mobile-drawer-in .18s ease}",
34
+ /* 设置弹层打开时:抽屉释放宽度/overflow 约束(弹层 100vw 全屏,不被抽屉裁剪) */
35
+ "body.dsh-mobile-ui [class*=sidebarCol].dsh-mobile-settings-open{overflow:visible !important;width:100vw !important;max-width:100vw !important;box-shadow:none !important}",
36
+ "@keyframes dsh-mobile-drawer-in{from{transform:translateX(-16px);opacity:0}to{transform:none;opacity:1}}",
37
+ /* 移动端规则 */
38
+ "@media (max-width:768px){",
39
+ " *{touch-action:manipulation;-webkit-tap-highlight-color:transparent}",
40
+ " html{-webkit-text-size-adjust:100%}",
41
+ /* 16px 输入框字号防 iOS focus 自动缩放 */
42
+ " body.dsh-mobile-ui textarea,body.dsh-mobile-ui input,body.dsh-mobile-ui select{font-size:16px !important}",
43
+ /* 布局单列(JS 同步改内联样式;此处 !important 兜底防 React 重渲染还原) */
44
+ " body.dsh-mobile-ui [class*=frame]{grid-template-columns:minmax(0,1fr) !important}",
45
+ " body.dsh-mobile-ui [class*=sidebarCol]{display:none}",
46
+ /* 移动端隐藏右侧详情列(桌面 0px 面板,窄屏下会占行推挤内容) */
47
+ " body.dsh-mobile-ui [class*=detailsCol]{display:none !important}",
48
+ /* 右上角菜单按钮显示(无底部导航栏,输入区直接贴底) */
49
+ " body.dsh-mobile-ui #dsh-mobile-menu-btn{display:flex}",
50
+ " body.dsh-mobile-ui [class*=composerSeat],body.dsh-mobile-ui [class*=composerStack]{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px)) !important}",
51
+ /* 头部紧凑(允许换行,标题不再截断) */
52
+ " body.dsh-mobile-ui [class*=header]{flex-wrap:wrap;row-gap:4px}",
53
+ /* 触摸目标:composer 区按钮/触发器 ≥44px(iOS HIG) */
54
+ " body.dsh-mobile-ui [class*=composerSeat] button,body.dsh-mobile-ui [class*=composerStack] button{min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}",
55
+ " body.dsh-mobile-ui [class*=composerSeat] [class*=trigger],body.dsh-mobile-ui [class*=composerStack] [class*=trigger]{min-height:40px}",
56
+ /* 统计行(LLM 用时/首token)允许换行防截断(分段 nowrap 片段的父容器) */
57
+ " body.dsh-mobile-ui [class*=composerStack] > *,body.dsh-mobile-ui [class*=composerSeat] > *{white-space:normal !important;overflow-wrap:anywhere !important}",
58
+ /* 抽屉会话列表项触摸目标 44px */
59
+ " body.dsh-mobile-ui [class*=sidebarCol] [class*=listArea] button{min-height:44px !important}",
60
+ /* hero 布局:内容顶部对齐可滚动,输入区经 margin-top:auto 沉底贴视口底部 */
61
+ " body.dsh-mobile-ui [class*=scrollBody]{justify-content:flex-start !important}",
62
+ " body.dsh-mobile-ui [class*=composerSeat]{margin-top:auto !important}",
63
+ " body.dsh-mobile-ui [class*=composerSeat] [class*=uV2eYG_root]{margin-top:auto !important}",
64
+ /* hero 标题置顶(纯 CSS,不移动 DOM):composerHero 栈撑满视口,
65
+ 输入卡经 margin-top:auto 沉底,标题/工作区行自然留在顶部。
66
+ 禁止用 JS insertBefore 搬动 React 节点——重渲染时 removeChild 抛
67
+ NotFoundError,整个会话视图会被卸载(页面清空) */
68
+ " body.dsh-mobile-ui [class*=composerSeat]:has([class*=composerHero]){flex:1 1 auto !important;margin-top:0 !important}",
69
+ " body.dsh-mobile-ui [class*=composerHero]{flex:1 1 auto !important}",
70
+ " body.dsh-mobile-ui [class*=composerHero] > [class*=pXSMma_root]{flex:0 0 auto !important;height:auto !important;margin-top:24px !important}",
71
+ " body.dsh-mobile-ui [class*=composerHero] > [class*=heroWorkspaceRow]{flex:0 0 auto !important;height:auto !important}",
72
+ /* 工作区行内按钮豁免 44px 触摸高(保持 28px 原高) */
73
+ " body.dsh-mobile-ui [class*=heroWorkspaceRow] button{min-height:28px !important;min-width:0 !important;height:auto !important}",
74
+ /* QA 问答卡片(Mbwy4a)移动端适配:全屏弹层(与设置面板同构)——
75
+ seat 固定铺满视口 + 内部滚动,footer(跳过/提交)sticky 钉屏幕底部。
76
+ 任何设备/视口/系统字体下提交按钮都必然可见;QA 关闭后自动恢复 */
77
+ " body.dsh-mobile-ui [class*=composerSeat].dsh-mobile-qa{position:fixed !important;top:100px !important;left:8px !important;right:8px !important;bottom:auto !important;margin:0 !important;z-index:2147482999 !important;padding:12px !important;max-height:calc(100vh - 150px) !important;max-height:calc(100dvh - 150px) !important;overflow-y:auto !important;background:transparent !important}",
78
+ " body.dsh-mobile-ui [class*=Mbwy4a_card]{width:100% !important;max-width:none !important}",
79
+ " body.dsh-mobile-ui [class*=Mbwy4a_body]{overflow:visible !important}",
80
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer]{position:sticky !important;bottom:0 !important;background:var(--dsw-alias-bg-layer-1,rgb(44,44,46)) !important}",
81
+ /* QA footer 防溢出:内容超宽时换行(跳过/提交换行后仍可见,提交永不被裁) */
82
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer]{flex-wrap:wrap !important;row-gap:4px !important}",
83
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer] [class*=footerActions]{flex-wrap:wrap !important;min-width:0 !important}",
84
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer] button{flex:0 1 auto !important;min-width:0 !important;font-size:12px !important;padding:0 8px !important}",
85
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer] [class*=iconButton]{flex:0 0 44px !important}",
86
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer] [class*=progress]{flex:0 1 auto !important;min-width:0 !important}",
87
+ " body.dsh-mobile-ui [class*=Mbwy4a_pager]{flex:0 1 auto !important;min-width:0 !important}",
88
+ " body.dsh-mobile-ui [class*=Mbwy4a_footer] button[class*=_primary]{flex:0 0 auto !important}",
89
+ /* 操作行单行紧凑:hero 4 元素 / 会话页 5 元素(含上下文按钮)一行放下。
90
+ 权限按钮移动端图标模式(44px 隐藏文字防撑宽),上下文紧凑 36px,
91
+ 模型选择器 127px 居中显示(轻微截断可接受) */
92
+ " body.dsh-mobile-ui [class*=uV2eYG_row]{flex-wrap:nowrap !important;gap:4px !important}",
93
+ " body.dsh-mobile-ui [class*=uV2eYG_row] > *{flex:0 1 auto !important}",
94
+ " body.dsh-mobile-ui [class*=uV2eYG_tools],body.dsh-mobile-ui [class*=uV2eYG_modes]{padding:0 !important;gap:4px !important}",
95
+ " body.dsh-mobile-ui [class*=uV2eYG_row] [class*=trigger]{display:flex !important;align-items:center !important;line-height:1.2 !important}",
96
+ " body.dsh-mobile-ui button[aria-label*=选择模型]{display:flex !important;align-items:center !important;justify-content:center !important;flex:0 0 auto !important;max-width:136px !important;font-size:11px !important;padding:0 6px !important;overflow:hidden !important;text-overflow:ellipsis !important;white-space:nowrap !important}",
97
+ " body.dsh-mobile-ui button[aria-label*=访问模式]{width:44px !important;flex:0 0 44px !important;padding:0 !important;justify-content:center !important}",
98
+ " body.dsh-mobile-ui button[aria-label*=访问模式] span,body.dsh-mobile-ui button[aria-label*=访问模式] [class*=label]{display:none !important}",
99
+ " body.dsh-mobile-ui button[aria-label*=上下文]{width:36px !important;max-width:36px !important;min-width:36px !important;flex:0 0 36px !important;padding:0 !important;font-size:10px !important;justify-content:center !important;overflow:hidden !important}",
100
+ " body.dsh-mobile-ui button[aria-label*=上下文] span,body.dsh-mobile-ui button[aria-label*=上下文] [class*=label]{max-width:30px !important;overflow:hidden !important;text-overflow:ellipsis !important;white-space:nowrap !important}",
101
+ /* 输入框圆角与内边距(视觉更圆润、输入更舒适;卡左右加宽) */
102
+ " body.dsh-mobile-ui [class*=input]{border-radius:18px !important}",
103
+ " body.dsh-mobile-ui [class*=uV2eYG_root]{padding-left:8px !important;padding-right:8px !important}",
104
+ " body.dsh-mobile-ui textarea{border-radius:18px !important}",
105
+ /* 输入卡片彻底去边框白边(border/box-shadow 全清,背景跟随主题变量) */
106
+ " body.dsh-mobile-ui [class*=card]{border:none !important;box-shadow:none !important}",
107
+ " body.dsh-mobile-ui [class*=uV2eYG_card],body.dsh-mobile-ui [class*=uV2eYG_input]{border:none !important;box-shadow:none !important}",
108
+ /* 阅读:消息流边距与间距(左右 8px,内容更宽) */
109
+ " body.dsh-mobile-ui [class*=scrollBody]{padding:12px 8px 0}",
110
+ " body.dsh-mobile-ui [class*=flowItem]{margin-bottom:14px}",
111
+ " body.dsh-mobile-ui [class*=userBubble],body.dsh-mobile-ui [class*=bubble]{max-width:92% !important}",
112
+ /* 辅助文本字号提升(词根容错:actions/meta/caption/stamp/hint/tools/badge) */
113
+ " body.dsh-mobile-ui [class*=scrollBody] :is([class*=actions],[class*=meta],[class*=caption],[class*=stamp],[class*=hint],[class*=tools],[class*=badge],[class*=timing]){font-size:13px !important;white-space:normal !important;overflow-wrap:anywhere !important}",
114
+ /* 状态统计行可读性(允许换行防截断) */
115
+ " body.dsh-mobile-ui [class*=statsRow],body.dsh-mobile-ui [class*=summaryRow],body.dsh-mobile-ui [class*=metrics]{font-size:12px !important;white-space:normal !important;overflow-wrap:anywhere}",
116
+ /* 头像缩小省空间 */
117
+ " body.dsh-mobile-ui [class*=avatar]{width:28px !important;height:28px !important}",
118
+ /* AI 正文与工具行视觉分隔 */
119
+ " body.dsh-mobile-ui [class*=markdown]{margin-top:6px}",
120
+ /* 头部标题允许换行(防截断) */
121
+ " body.dsh-mobile-ui [class*=header] [class*=title],body.dsh-mobile-ui [class*=crumbs]{white-space:normal;overflow-wrap:anywhere}",
122
+ /* 输入框聚焦反馈 */
123
+ " body.dsh-mobile-ui textarea:focus,body.dsh-mobile-ui [class*=input]:focus-within{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,rgba(79,124,255,.45))}",
124
+ /* 消息流操作按钮(复制/反馈/分享)触摸目标提升 */
125
+ " body.dsh-mobile-ui [class*=scrollBody] [class*=actions] button,body.dsh-mobile-ui [class*=scrollBody] [class*=tools] button{min-width:36px;min-height:36px;display:inline-flex;align-items:center;justify-content:center}",
126
+ "}",
127
+
128
+ /* 320px 小屏: 操作行进一步压缩防重叠 */
129
+ "@media (max-width:360px){",
130
+ " body.dsh-mobile-ui button[aria-label*=选择模型]{max-width:110px !important}",
131
+ " body.dsh-mobile-ui [class*=uV2eYG_row]{gap:2px !important}",
132
+ " body.dsh-mobile-ui [class*=uV2eYG_tools],body.dsh-mobile-ui [class*=uV2eYG_modes]{gap:2px !important}",
133
+ " body.dsh-mobile-ui button[aria-label*=上下文]{margin-left:8px !important}",
134
+ "}",
135
+ /* 抽屉视觉:右侧圆角 + 更实背景 */
136
+ "body.dsh-mobile-ui [class*=sidebarCol].dsh-mobile-drawer{border-radius:0 18px 18px 0;padding-bottom:64px !important;box-sizing:border-box !important}",
137
+ /* 设置面板(VOzbGW 弹层)移动端:全宽 + 菜单收窄 + 内容区加宽 */
138
+ "body.dsh-mobile-ui [class*=VOzbGW_panel]{left:0 !important;right:0 !important;width:100vw !important;max-width:100vw !important;border-radius:0 !important;box-sizing:border-box !important}",
139
+ "body.dsh-mobile-ui [class*=VOzbGW_overlay]{align-items:flex-start !important;padding-top:12px !important;padding-bottom:12px !important;box-sizing:border-box !important}",
140
+ "body.dsh-mobile-ui [class*=VOzbGW_panel]{max-height:calc(100vh - 24px) !important}",
141
+ "body.dsh-mobile-ui [class*=VOzbGW_content],body.dsh-mobile-ui [class*=VOzbGW_options]{flex:1 1 auto !important;min-height:0 !important;max-height:none !important;overflow-y:auto !important}",
142
+ "body.dsh-mobile-ui [class*=VOzbGW_options]{padding-bottom:16px !important;box-sizing:border-box !important}",
143
+ /* 模型选择菜单居中锚定(原 left:-150 固定值,按钮靠左时会出屏幕左侧) */
144
+ "body.dsh-mobile-ui [class*=_7KE1Ra_menu]{left:50% !important;transform:translateX(-50%) !important;min-width:250px !important;max-width:calc(100vw - 24px) !important}",
145
+ /* trailing 基础 gap 2px:hero(无上下文)模型右扩吃 gap,左缘保持不变 */
146
+ "body.dsh-mobile-ui [class*=uV2eYG_trailing]{gap:2px !important}",
147
+ /* 会话页(有上下文按钮):上下文 margin-left 右移 12px(只扩模型↔上下文间距) */
148
+ "body.dsh-mobile-ui [class*=uV2eYG_trailing]:has(button[aria-label*=上下文]) button[aria-label*=上下文]{margin-left:12px !important}",
149
+ "body.dsh-mobile-ui [class*=VOzbGW_nav]{width:110px !important;flex:0 0 110px !important;min-width:110px !important;overflow:visible !important}",
150
+ "body.dsh-mobile-ui [class*=VOzbGW_nav] [class*=navList]{flex:1 1 auto !important;height:auto !important;max-height:none !important;overflow-y:auto !important}",
151
+ "body.dsh-mobile-ui [class*=VOzbGW_content]{flex:1 1 auto !important;min-width:0 !important;max-width:calc(100% - 110px) !important;padding:0 12px !important;overflow:hidden !important}",
152
+ /* 头部防重叠加固:关闭按钮固定右侧 + 打开配置文件允许收缩 */
153
+ "body.dsh-mobile-ui [class*=VOzbGW_header]{display:flex !important;align-items:center !important;justify-content:flex-end !important;gap:8px !important;padding:0 12px !important}",
154
+ "body.dsh-mobile-ui [class*=VOzbGW_close]{flex:0 0 28px !important;margin-left:0 !important}",
155
+ "body.dsh-mobile-ui [class*=VOzbGW_actions]{white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;min-width:0 !important}",
156
+ /* 设置行内文字全宽(防 label 列被 flex 收缩成竖排窄列) */
157
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=section]{width:100% !important;max-width:100% !important}",
158
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=row]{flex-direction:column !important;align-items:stretch !important;gap:4px !important}",
159
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=rowText]{flex:1 1 auto !important;min-width:0 !important;align-items:stretch !important}",
160
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=row] > *:last-child{flex:0 0 auto !important}",
161
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=title],body.dsh-mobile-ui [class*=VOzbGW_content] [class*=rowText]{width:100% !important;max-width:100% !important;white-space:normal !important;overflow-wrap:anywhere !important}",
162
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=title]{display:block !important;flex:1 1 auto !important;align-self:stretch !important}",
163
+ "body.dsh-mobile-ui [class*=VOzbGW_content] [class*=rowText]{flex:1 1 auto !important;min-width:0 !important}",
164
+ ].join("\n");
165
+
166
+ function injectStyle() {
167
+ try {
168
+ if (typeof document === "undefined") return;
169
+ var tagId = "dsh-mobile-ui";
170
+ if (document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]")) return;
171
+ var tag = document.createElement("style");
172
+ tag.dataset.plugin = "dsh-mobile-ui";
173
+ tag.dataset.pluginCss = tagId;
174
+ tag.textContent = MOBILE_CSS;
175
+ document.head.appendChild(tag);
176
+ } catch (e) { /* CSS 注入失败不致命 */ }
177
+ }
178
+
179
+ /* ═══════════════════ JS 增强层 ═══════════════════ */
180
+ var MOBILE_MQ = "(max-width: 768px)";
181
+ var FRAME_GRID_NARROW = "minmax(0, 1fr)";
182
+
183
+ function initMobile() {
184
+ try {
185
+ if (typeof window === "undefined" || typeof document === "undefined") return;
186
+ injectStyle();
187
+ var mq = window.matchMedia(MOBILE_MQ);
188
+ var tabbar = null;
189
+ var scrim = null;
190
+ var drawerOpen = false;
191
+ var settingsOpen = false; /* 设置弹层打开标志(弹层关闭时恢复导航栏) */
192
+ var savedGrid = null; /* 窄屏前的 frame 原始 grid(宽屏恢复用) */
193
+
194
+ /* 主布局 frame:display:grid 的顶层容器(CSS 生效前 3 列 / 生效后 1 列均可) */
195
+ function findFrame() {
196
+ var els = document.querySelectorAll("[class*=frame]");
197
+ for (var i = 0; i < els.length; i++) {
198
+ var el = els[i];
199
+ if (!el || !el.children || el.children.length < 3) continue;
200
+ var cs = window.getComputedStyle(el);
201
+ if (cs.display === "grid") {
202
+ var cols = cs.gridTemplateColumns.split(" ").length;
203
+ if (cols >= 1 && cols <= 3) return el;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ function getSidebar() {
209
+ var frame = findFrame();
210
+ return frame && frame.children[0] ? frame.children[0] : null;
211
+ }
212
+ function findSidebarButton(label) {
213
+ var side = getSidebar();
214
+ if (!side) return null;
215
+ var btns = side.querySelectorAll("button");
216
+ for (var i = 0; i < btns.length; i++) {
217
+ if ((btns[i].getAttribute("aria-label") || "") === label) return btns[i];
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function ensureChrome() {
223
+ if (document.getElementById("dsh-mobile-menu-btn")) {
224
+ tabbar = document.getElementById("dsh-mobile-menu-btn");
225
+ scrim = document.getElementById("dsh-mobile-scrim");
226
+ return;
227
+ }
228
+ scrim = document.createElement("div");
229
+ scrim.id = "dsh-mobile-scrim";
230
+ scrim.addEventListener("click", function () { closeDrawer(); });
231
+ document.body.appendChild(scrim);
232
+
233
+ /* 右上角菜单按钮:打开会话抽屉(抽屉内含 新建/会话列表/设置 全部入口)。
234
+ 复用 tabbar 变量承载,显隐逻辑(模态隐藏/恢复)零改动 */
235
+ tabbar = document.createElement("button");
236
+ tabbar.id = "dsh-mobile-menu-btn";
237
+ tabbar.type = "button";
238
+ tabbar.setAttribute("aria-label", "菜单");
239
+ tabbar.innerHTML = "<svg viewBox='0 0 24 24'><path d='M4 7h16M4 12h16M4 17h10'/></svg>";
240
+ tabbar.addEventListener("click", function () {
241
+ try { openDrawer(); } catch (e) { /* 忽略 */ }
242
+ });
243
+ document.body.appendChild(tabbar);
244
+ }
245
+
246
+ function setTabActive(key) {
247
+ try {
248
+ if (!tabbar) return;
249
+ var btns = tabbar.querySelectorAll("button");
250
+ for (var i = 0; i < btns.length; i++) {
251
+ if (btns[i].getAttribute("aria-label") === key) btns[i].classList.add("dsh-mobile-active");
252
+ else btns[i].classList.remove("dsh-mobile-active");
253
+ }
254
+ } catch (e) { /* 忽略 */ }
255
+ }
256
+ function openDrawer() {
257
+ var side = getSidebar();
258
+ if (!side) return;
259
+ side.classList.add("dsh-mobile-drawer");
260
+ if (scrim) scrim.classList.add("dsh-mobile-visible");
261
+ drawerOpen = true;
262
+ /* 抽屉/设置模态打开时隐藏底部导航栏(避免双层级干扰) */
263
+ if (tabbar) tabbar.style.display = "none";
264
+ setTabActive("会话");
265
+ /* 折叠态 sidebar 只有图标 rail,展开以显示会话列表 */
266
+ var tog = findSidebarButton("打开侧边栏");
267
+ if (tog) { try { tog.click(); } catch (e) {} }
268
+ }
269
+ function closeDrawer() {
270
+ var side = getSidebar();
271
+ if (side) {
272
+ side.classList.remove("dsh-mobile-drawer");
273
+ side.classList.remove("dsh-mobile-settings-open");
274
+ }
275
+ if (scrim) scrim.classList.remove("dsh-mobile-visible");
276
+ drawerOpen = false;
277
+ settingsOpen = false;
278
+ setTabActive("");
279
+ if (tabbar) tabbar.style.display = "";
280
+ }
281
+
282
+ function sync() {
283
+ try {
284
+ if (mq.matches) {
285
+ /* 设置弹层状态检测:出现(用户手动点抽屉设置)→ 释放抽屉约束 + 隐藏菜单;
286
+ 消失 → 恢复抽屉/菜单 */
287
+ {
288
+ var ov2 = document.querySelector("[class*=VOzbGW_overlay]");
289
+ var ovVisible = ov2 && getComputedStyle(ov2).display !== "none";
290
+ if (ovVisible && !settingsOpen) {
291
+ settingsOpen = true;
292
+ var sd = getSidebar();
293
+ if (sd) sd.classList.add("dsh-mobile-settings-open");
294
+ if (tabbar) tabbar.style.display = "none";
295
+ } else if (!ovVisible && settingsOpen) {
296
+ settingsOpen = false;
297
+ var sd2 = getSidebar();
298
+ if (sd2) sd2.classList.remove("dsh-mobile-settings-open");
299
+ if (tabbar) tabbar.style.display = "";
300
+ }
301
+ }
302
+ document.body.classList.add("dsh-mobile-ui");
303
+ /* QA 弹层标记:JS 检测 Mbwy4a 卡片(不依赖 :has,兼容微信 X5 等旧内核)。
304
+ 加 class 到 composerSeat,CSS 据此全屏化;卡片消失即移除 */
305
+ try {
306
+ var qaFrame = document.querySelector("[class*=Mbwy4a_frame]");
307
+ var qaSeat = null;
308
+ if (qaFrame) {
309
+ var qn = qaFrame.parentElement;
310
+ while (qn) { if ((typeof qn.className === "string" ? qn.className : "").indexOf("composerSeat") >= 0) { qaSeat = qn; break; } qn = qn.parentElement; }
311
+ }
312
+ if (qaSeat) qaSeat.classList.add("dsh-mobile-qa");
313
+ else { var qOld = document.querySelector("[class*=composerSeat].dsh-mobile-qa"); if (qOld) qOld.classList.remove("dsh-mobile-qa"); }
314
+ } catch (e) { /* QA 标记失败静默 */ }
315
+ /* hero 标题置顶改由纯 CSS 实现(composerHero 撑满视口 + 输入卡沉底),
316
+ 不再用 insertBefore 搬动 React 节点——移动节点会导致 React 重渲染
317
+ removeChild 抛 NotFoundError,整个会话视图卸载(页面空白) */
318
+ var frame = findFrame();
319
+ if (frame) {
320
+ if (savedGrid === null) savedGrid = frame.style.gridTemplateColumns;
321
+ frame.style.gridTemplateColumns = FRAME_GRID_NARROW;
322
+ }
323
+ var side = getSidebar();
324
+ /* 抽屉态下 sidebar 保持可见,否则隐藏 */
325
+ side.style.display = drawerOpen ? "" : "none";
326
+ ensureChrome();
327
+ if (tabbar) tabbar.style.display = drawerOpen ? "none" : "";
328
+ } else {
329
+ document.body.classList.remove("dsh-mobile-ui");
330
+ var frame2 = findFrame();
331
+ if (frame2) {
332
+ if (savedGrid) frame2.style.gridTemplateColumns = savedGrid;
333
+ savedGrid = null;
334
+ }
335
+ var side2 = getSidebar();
336
+ if (side2) side2.style.display = "";
337
+ closeDrawer();
338
+ if (tabbar) tabbar.style.display = "none";
339
+ }
340
+ } catch (e) { /* 同步失败静默 */ }
341
+ }
342
+
343
+ /* 监听视口变化 */
344
+ if (typeof mq.addEventListener === "function") {
345
+ mq.addEventListener("change", sync);
346
+ } else if (typeof mq.addListener === "function") {
347
+ mq.addListener(sync);
348
+ }
349
+
350
+ /* 首次同步 + 等待 React 渲染出 frame 后重试 */
351
+ sync();
352
+ var tries = 0;
353
+ var retryTimer = setInterval(function () {
354
+ tries++;
355
+ if (findFrame()) { sync(); if (tries > 3) clearInterval(retryTimer); }
356
+ else if (tries > 30) clearInterval(retryTimer);
357
+ }, 500);
358
+
359
+ /* DOM 结构变化(React 重渲染)后重新同步 */
360
+ var mo = null;
361
+ if (typeof MutationObserver !== "undefined") {
362
+ var debounce = null;
363
+ mo = new MutationObserver(function () {
364
+ if (debounce) clearTimeout(debounce);
365
+ debounce = setTimeout(sync, 200);
366
+ });
367
+ mo.observe(document.body, { childList: true, subtree: true });
368
+ }
369
+
370
+ /* 卸载清理 */
371
+ return function () {
372
+ try {
373
+ if (retryTimer) clearInterval(retryTimer);
374
+ if (debounce) clearTimeout(debounce);
375
+ if (mo) mo.disconnect();
376
+ if (tabbar && tabbar.parentNode) tabbar.parentNode.removeChild(tabbar);
377
+ if (scrim && scrim.parentNode) scrim.parentNode.removeChild(scrim);
378
+ document.body.classList.remove("dsh-mobile-ui");
379
+ } catch (e) { /* 清理失败静默 */ }
380
+ };
381
+ } catch (e) {
382
+ return function () { /* 初始化失败,静默降级 */ };
383
+ }
384
+ }
385
+
386
+ /* ═══════════════════ Cordis 插件形状 ═══════════════════ */
387
+ var name = "dsh-mobile-ui";
388
+ var inject = [];
389
+ function apply(ctx) {
390
+ ctx.logger?.info?.("[dsh-mobile-ui] active: browser mobile UI enhancement mounted");
391
+ return initMobile();
392
+ }
393
+ exports.name = name;
394
+ exports.inject = inject;
395
+ exports.apply = apply;
396
+ return module.exports;
397
+ }
398
+ });
@@ -0,0 +1,6 @@
1
+ # dsh-mobile-ui bundle 层:插入插件行。
2
+ # 由 package.json 的 dsh.bundle.patch 声明引用;profile 将该包列入
3
+ # dsh.profile.bundles 时按序应用本层。name 用包名(非相对路径)。
4
+ - insert:
5
+ - id: dsh-mobile-ui
6
+ name: dsh-mobile-ui
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * dsh-mobile-ui — 宿主端(极简占位)。
3
+ *
4
+ * 本插件的实质在浏览器端(./client):移动端视口(≤768px)下的
5
+ * 响应式布局、阅读/触摸优化与底部导航。host half 仅需让 client
6
+ * bundle 随组合装载即可,无需任何宿主服务。
7
+ */
8
+ export const name = "dsh-mobile-ui";
9
+
10
+ export function apply(ctx) {
11
+ ctx.logger?.info?.("[dsh-mobile-ui] active: host half (browser enhancement in ./client)");
12
+ }
package/install.sh ADDED
@@ -0,0 +1,259 @@
1
+ #!/bin/bash
2
+ # install.sh.template — 通用 dsh 插件安装器模板(dsh-plugins)
3
+ # 新插件由 scripts/new-plugin.sh 从本模板生成 packages/<name>/install.sh;
4
+ # 也可手动复制并替换 dsh-mobile-ui。memory-recall-dsh/install.sh 即为本模板的
5
+ # 早期实例化产物(含插件特有逻辑)。
6
+ #
7
+ # 用法(在插件源码目录执行,或 --src 指定):
8
+ # bash install.sh --plugin <插件名> [--src <源码目录>] [--profile web|headless]
9
+ # [--api-key <key>] [--check | --smoke | --restart | --uninstall]
10
+ #
11
+ # 内置防崩(MR-022/023 教训,勿删):
12
+ # 1. 契约预检:node _dsh-common/preflight.mjs(dsh.client.platform /
13
+ # exports["./client"] / classic-script bundle),未通过拒绝安装;
14
+ # 2. headless 试启动冒烟:--restart 前先在隔离 profile 真实 boot,
15
+ # 命中插件加载错误关键字即中止,正式服务不受影响。
16
+ set -u
17
+
18
+ PLUGIN="dsh-mobile-ui"
19
+ SRC=""
20
+ PROFILE="web"
21
+ API_KEY=""
22
+ MODE="apply"
23
+
24
+ i=0
25
+ ARGS=("$@")
26
+ while [ $i -lt ${#ARGS[@]} ]; do
27
+ arg="${ARGS[$i]}"
28
+ case "$arg" in
29
+ --check) MODE="check" ;;
30
+ --restart) MODE="restart" ;;
31
+ --smoke) MODE="smoke" ;;
32
+ --uninstall) MODE="uninstall" ;;
33
+ --plugin=*) PLUGIN="${arg#--plugin=}" ;;
34
+ --src=*) SRC="${arg#--src=}" ;;
35
+ --profile=*) PROFILE="${arg#--profile=}" ;;
36
+ --api-key=*) API_KEY="${arg#--api-key=}" ;;
37
+ --plugin) i=$((i + 1)); PLUGIN="${ARGS[$i]:-}"; [ -n "$PLUGIN" ] || { echo "用法: --plugin <name>"; exit 1; } ;;
38
+ --src) i=$((i + 1)); SRC="${ARGS[$i]:-}" ;;
39
+ --profile) i=$((i + 1)); PROFILE="${ARGS[$i]:-web}" ;;
40
+ --api-key) i=$((i + 1)); API_KEY="${ARGS[$i]:-}" ;;
41
+ --*) echo "未知参数: $arg"; exit 1 ;;
42
+ esac
43
+ i=$((i + 1))
44
+ done
45
+
46
+ [ -n "$PLUGIN" ] || { echo "错误: 缺少 --plugin <name>"; exit 1; }
47
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
48
+ [ -n "$SRC" ] || SRC="$HERE"
49
+ # 契约预检脚本位置:优先所在仓库(packages/<name>/install.sh → ../../scripts),
50
+ # 其次环境变量 DSH_PLUGINS_REPO,最后 $HOME/dsh-plugins
51
+ for CAND in "$HERE/../../scripts" "${DSH_PLUGINS_REPO:-$HOME/dsh-plugins}/scripts"; do
52
+ if [ -n "$CAND" ] && [ -f "$CAND/preflight.mjs" ]; then
53
+ COMMON="$CAND"
54
+ break
55
+ fi
56
+ done
57
+ [ -n "${COMMON:-}" ] || COMMON=""
58
+ DSH="${DSH_HOME:-$HOME/.dsh}"
59
+ DST_PLUGINS="$DSH/plugins/$PLUGIN"
60
+ DST_PROFILE="$DSH/profiles/node_modules/$PLUGIN"
61
+ PATCH="$DSH/profiles/$PROFILE/cordis.patch.yml"
62
+ FAIL=0
63
+
64
+ # 全部关键文件一致(package.json + 所有 .js;.mjs 类构建/预检工具属开发期,不部署)
65
+ FILES_IDENTICAL() {
66
+ local src="$1" dst="$2"
67
+ for f in "$src"/*.js; do
68
+ [ -f "$f" ] || continue
69
+ local base
70
+ base="$(basename "$f")"
71
+ [ -f "$dst/$base" ] || return 1
72
+ diff -q "$f" "$dst/$base" >/dev/null 2>&1 || return 1
73
+ done
74
+ diff -q "$src/package.json" "$dst/package.json" >/dev/null 2>&1 || return 1
75
+ return 0
76
+ }
77
+
78
+ echo "== $PLUGIN 安装(mode: $MODE, profile: $PROFILE)=="
79
+ echo " DSH 目录: $DSH"
80
+ echo " 源码目录: $SRC"
81
+
82
+ # ── 0/4 前置检查 + 契约预检 ─────────────────────────────────────────────
83
+ echo "== 0/4 前置检查 =="
84
+ [ -f "$SRC/package.json" ] || { echo " 错误: 缺少 $SRC/package.json"; exit 1; }
85
+ for f in "$SRC"/*.js; do
86
+ node --check "$f" >/dev/null 2>&1 || { echo " 错误: 语法检查失败 $f"; FAIL=1; }
87
+ done
88
+ [ "$FAIL" = "1" ] && exit 1
89
+ echo " [OK] 源码完整,语法检查通过"
90
+
91
+ echo "== 0.5/4 契约预检($COMMON/preflight.mjs)=="
92
+ if [ -f "$COMMON/preflight.mjs" ]; then
93
+ if node "$COMMON/preflight.mjs" "$SRC"; then
94
+ echo " [OK] 加载器契约检查通过"
95
+ else
96
+ if [ "$MODE" = "check" ]; then
97
+ FAIL=1
98
+ else
99
+ echo " [错误] 契约预检未通过——安装会破坏 dsh 启动,已中止。请修复后重试。"
100
+ exit 1
101
+ fi
102
+ fi
103
+ else
104
+ echo " [跳过] 无共享 preflight.mjs"
105
+ fi
106
+
107
+ # ── 1/4 源码留档 ─────────────────────────────────────────────────────────
108
+ echo "== 1/4 源码留档 =="
109
+ if [ -f "$DST_PLUGINS/index.js" ] && FILES_IDENTICAL "$SRC" "$DST_PLUGINS"; then
110
+ echo " [已有] $DST_PLUGINS(内容一致)"
111
+ else
112
+ if [ "$MODE" = "check" ]; then
113
+ echo " [缺失/不同] $DST_PLUGINS"; FAIL=1
114
+ else
115
+ mkdir -p "$DST_PLUGINS"
116
+ cp "$SRC"/package.json "$SRC"/*.js "$DST_PLUGINS/"
117
+ echo " [已装] $DST_PLUGINS"
118
+ fi
119
+ fi
120
+
121
+ # ── 2/4 运行副本 ─────────────────────────────────────────────────────────
122
+ echo "== 2/4 运行副本 =="
123
+ if [ -f "$DST_PROFILE/index.js" ] && FILES_IDENTICAL "$SRC" "$DST_PROFILE"; then
124
+ echo " [已有] $DST_PROFILE(内容一致)"
125
+ else
126
+ if [ "$MODE" = "check" ]; then
127
+ echo " [缺失/不同] $DST_PROFILE"; FAIL=1
128
+ else
129
+ mkdir -p "$DST_PROFILE"
130
+ cp "$SRC"/package.json "$SRC"/*.js "$DST_PROFILE/"
131
+ echo " [已装] $DST_PROFILE"
132
+ fi
133
+ fi
134
+
135
+ # ── 3/4 profile patch 接线 ───────────────────────────────────────────────
136
+ echo "== 3/4 组合接线($PATCH)=="
137
+ if [ -f "$PATCH" ] && grep -q "$PLUGIN" "$PATCH" 2>/dev/null; then
138
+ echo " [已有] $PATCH 中的 $PLUGIN 接线"
139
+ elif [ "$MODE" = "check" ]; then
140
+ echo " [缺失] $PATCH 中的 $PLUGIN 接线"; FAIL=1
141
+ else
142
+ mkdir -p "$(dirname "$PATCH")"
143
+ if [ -f "$PATCH" ] && grep -q '^\[\]$' "$PATCH" 2>/dev/null; then
144
+ sed -i '/^\[\]$/d' "$PATCH"
145
+ fi
146
+ cat >> "$PATCH" <<PATCHEOF
147
+
148
+ # $PLUGIN:dsh 移动端 UI 插件(安装器: _dsh-common/install-template.sh)
149
+ - insert:
150
+ - id: $PLUGIN
151
+ name: '$PLUGIN'
152
+ PATCHEOF
153
+ echo " [已加] $PATCH"
154
+ fi
155
+
156
+ # ── headless 试启动冒烟(防"启动即崩")──────────────────────────────────
157
+ ROOT=""
158
+ for d in $(ls -dt "$HOME"/.npm/_npx/*/ 2>/dev/null); do
159
+ d=${d%/}
160
+ [ -d "$d/node_modules/@deepseek-ai" ] || continue
161
+ ROOT="$d"
162
+ break
163
+ done
164
+ SMOKE_LOG=/tmp/dsh-plugin-smoke.log
165
+ SMOKE_TEST() {
166
+ local dsh_bin="$1"
167
+ echo "== 冒烟:headless 试启动(隔离环境,不影响正式服务)=="
168
+ if [ ! -d "$DSH/profiles/headless" ]; then
169
+ echo " [跳过] headless profile 未初始化(首次运行 dsh --profile headless 可初始化)"
170
+ return 0
171
+ fi
172
+ if [ -f "$DSH/profiles/headless/cordis.patch.yml" ] && grep -q "$PLUGIN" "$DSH/profiles/headless/cordis.patch.yml" 2>/dev/null; then
173
+ echo " [已有] headless 插件接线"
174
+ else
175
+ mkdir -p "$DSH/profiles/headless"
176
+ [ -f "$DSH/profiles/headless/cordis.patch.yml" ] || printf '[]\n' > "$DSH/profiles/headless/cordis.patch.yml"
177
+ sed -i '/^\[\]$/d' "$DSH/profiles/headless/cordis.patch.yml"
178
+ cat >> "$DSH/profiles/headless/cordis.patch.yml" <<PATCHEOF
179
+
180
+ # $PLUGIN:dsh 移动端 UI 插件(自动接入,供 headless 冒烟试启动)
181
+ - insert:
182
+ - id: $PLUGIN
183
+ name: '$PLUGIN'
184
+ PATCHEOF
185
+ echo " [已接线] headless profile(自动接入,仅用于冒烟)"
186
+ fi
187
+ local start_ts end_ts
188
+ start_ts=$(date +%s)
189
+ if timeout 120 "$dsh_bin" --profile headless "1" > "$SMOKE_LOG" 2>&1; then
190
+ end_ts=$(date +%s)
191
+ echo " [PASS] headless 试启动成功($((end_ts - start_ts))s),插件组合无问题"
192
+ return 0
193
+ fi
194
+ end_ts=$(date +%s)
195
+ echo " [FAIL] headless 试启动失败($((end_ts - start_ts))s)"
196
+ if grep -qiE "client-modules|plugin tree failed|cannot resolve entry|$PLUGIN" "$SMOKE_LOG"; then
197
+ echo " 命中插件组合/加载错误关键字,判定为插件问题:"
198
+ grep -iE "client-modules|plugin tree failed|cannot resolve entry|$PLUGIN" "$SMOKE_LOG" | head -5
199
+ echo " 完整日志: $SMOKE_LOG"
200
+ echo " 回滚:bash install.sh --uninstall 后重启 dsh 即可恢复"
201
+ return 1
202
+ fi
203
+ echo " 未命中插件关键字(疑似 LLM/网络类问题),完整日志: $SMOKE_LOG"
204
+ return 2
205
+ }
206
+
207
+ # ── 4/4 冒烟 + 重启(可选)──────────────────────────────────────────────
208
+ if [ "$MODE" = "smoke" ] || [ "$MODE" = "restart" ]; then
209
+ if [ -n "$ROOT" ] && [ -x "$ROOT/node_modules/.bin/dsh" ]; then
210
+ SMOKE_TEST "$ROOT/node_modules/.bin/dsh"
211
+ SMOKE_RC=$?
212
+ [ "$MODE" = "smoke" ] && exit "$SMOKE_RC"
213
+ if [ "$SMOKE_RC" = "1" ]; then
214
+ echo " [中止] 冒烟判定插件有问题,不重启 dsh(正式服务保持当前状态)"
215
+ exit 1
216
+ fi
217
+ else
218
+ echo " [警告] 无法定位 dsh 可执行文件,冒烟跳过"
219
+ fi
220
+ fi
221
+
222
+ if [ "$MODE" = "restart" ]; then
223
+ echo "== 4/4 重启 dsh =="
224
+ pkill -TERM -f 'node_modules/.bin/dsh web' 2>/dev/null
225
+ pkill -TERM -f 'npm exec @deepseek-ai/dsh web' 2>/dev/null
226
+ sleep 3
227
+ if [ -n "$ROOT" ] && [ -x "$ROOT/node_modules/.bin/dsh" ]; then
228
+ cd "$ROOT" || exit 1
229
+ setsid nohup ./node_modules/.bin/dsh web >> /tmp/dsh-web.log 2>&1 < /dev/null &
230
+ echo " 新进程 PID=$!"
231
+ sleep 8
232
+ curl -s -o /dev/null -w " 127.0.0.1:3080 页面 -> %{http_code}\n" http://127.0.0.1:3080/ || echo " [警告] 页面未就绪,请稍后手动刷新"
233
+ curl -s -o /dev/null -w " $PLUGIN bundle -> %{http_code}\n" "http://127.0.0.1:3080/plugins/$PLUGIN/client.js"
234
+ else
235
+ echo " [警告] 无法定位 dsh 可执行文件,请手动重启 dsh web"
236
+ fi
237
+ fi
238
+
239
+ # ── 卸载 ─────────────────────────────────────────────────────────────────
240
+ if [ "$MODE" = "uninstall" ]; then
241
+ echo "== 卸载 =="
242
+ if [ -f "$PATCH" ]; then
243
+ sed -i "/$PLUGIN/d" "$PATCH"
244
+ echo " [已清] $PATCH 中的接线(含注释行)"
245
+ fi
246
+ rm -rf "$DST_PROFILE" "$DST_PLUGINS"
247
+ echo " [已删] $DST_PROFILE / $DST_PLUGINS"
248
+ echo " 完成。重启 dsh 后插件移除生效。"
249
+ exit 0
250
+ fi
251
+
252
+ if [ "$MODE" = "check" ]; then
253
+ echo "== 检查完成(未改动任何文件)=="
254
+ [ "$FAIL" = "1" ] && echo "(存在缺失项,直接运行 bash install.sh 即可补齐)"
255
+ exit 0
256
+ fi
257
+
258
+ echo "== 完成 =="
259
+ echo " 重启 dsh 后插件生效(bash install.sh --restart 可一键重启验证)。"
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "dsh-mobile-ui",
3
+ "version": "0.1.0",
4
+ "description": "dsh web 移动端 UI 优化:响应式布局 + 底部导航 + 阅读/触摸增强(≤768px 生效,桌面不受影响)",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "bundle": { "patch": "./cordis.patch.yml" },
14
+ "client": {
15
+ "inject": [],
16
+ "platform": "web",
17
+ "immediately": true
18
+ }
19
+ }
20
+ }