# dsh-plugin-mobile-ui · 项目说明 > **完整项目说明**:背景与约束、加载原理、功能全表、模块参考、设计 token、验证体系、上游缺陷反制、维护手册、历史结论。 > 只想上手看根目录 [README.md](../README.md);日常使用与维护看 [MOBILE-UI-GUIDE.md](MOBILE-UI-GUIDE.md)。 > 本文随代码更新;与代码冲突时以代码为准。每条关键结论都注明**实测**或**代码出处**。 | 元信息 | 值 | |---|---| | 包名 / 版本 | `dsh-plugin-mobile-ui` / `0.1.0`(MIT) | | 远端 | https://github.com/loeissu/dsh-plugin-mobile-ui | | 代码基线 | `src/client` 18 个模块 / 5649 行;最大 `DrawerOverlay.tsx` 1672 行 | --- ## 0. 一页速览 | 项 | 说明 | |---|---| | 一句话 | 把 DeepSeek Harness 的 Web 界面改造成**手机能用**的界面 | | 交付形态 | 官方 **slot 客户端插件**:单个 IIFE,经 `window.__ModuleLoader__.load({ id, factory })` 注册,导出 `apply` / `inject` | | 目标设备 | 手机(竖屏 320–430px、横屏 915×412);桌面鼠标环境**行为不变** | | 连接能力 | **不提供**。手机连电脑是 [dsh-plugin-tether](https://github.com/zexadev/dsh-tether) 的职责 | | 表面 | `shell.overlay` ×2(启动页、抽屉)、`settings.section`、`tool.call.toolview` ×5;另加 8 层宿主 chrome 打磨(不注册插槽) | | 验证 | 离线契约检查 + **33 个 CDP 断言套件**(全部通过);3 个已放弃设计的脚本归档在 `tools/retired/` | | 硬约束 | 不 fork dsh-tether、不改 DSH 源码、不替换 `sidebar` 插槽、不提升 tether 内置 DSH 运行时 | | 许可 | MIT;与 DeepSeek 官方无关,不含任何品牌资产 | --- ## 1. 背景与边界 ### 1.1 分层:谁负责什么 ``` 手机 App(cc.zexa.dshtether,tether 的 Android 客户端) └─ 应用内回环代理 http://127.0.0.1: ← 手机上页面的 origin(实机错误页可见) └─ P2P 隧道(NAT 打洞)→ 电脑上的 dsh web └─ DSH Web 客户端(React + Cordis DI) ├─ dsh-plugin-tether :把界面送到手机(连接、配对、隧道) └─ dsh-plugin-mobile-ui :让界面在手机上**好用**(本项目) ``` DSH 的界面是**桌面优先**的:控件尺寸、间距、密度、悬浮交互都按鼠标设计。到了手机上集中表现为三类问题:**命中区过小**、**窄屏排版互相挤压**、**触摸设备没有 hover** 带来的一系列副作用(残留提示、`pointerleave` 缺失、粘滞 hover 样式)。本项目只解决这些,不重新实现界面。 ### 1.2 硬约束(写代码前先读) | 约束 | 原因 | |---|---| | 不 fork `dsh-tether` | 连接/配对/隧道是它的职责;fork 会把两个项目的升级耦合在一起 | | 不修改 DSH 源码 | 升级 DSH 不应有合并成本;一切通过公开插槽与运行时 DOM 完成 | | 不注册 `sidebar` 插槽(默认关闭) | 它是 `single`/`root`:接管会连带屏蔽 ui-sidebar 声明的 6 个座位,且"一个 key 只能有一个声明者"无法用优先级解决 → **全局失败**(`Failed to load plugins`)。见 [drawer-takeover.md](drawer-takeover.md) | | 不提升 tether 内置的 DSH 运行时 | 那是 tether 的发布物(`0.1.5` 的 per-session 写锁依赖 `flock`,在 Android 上硬抛异常);本项目只记录影响面 | | 每步先在隔离环境/模拟器验证 | 真机截图是最终裁判,但每步都要有可复现的实测 | **可无损回退是设计目标**:拔掉本项目,DSH 在手机上仍然能用,只是难用。 --- ## 2. 交付形态与加载原理 ### 2.1 loader 契约 产物是**一个自包含的 IIFE**,不打包 React(宿主提供,产物只声明 `externals`): ```js __ModuleLoader__.load({ id: 'dsh-plugin-mobile-ui', factory: () => ({ apply, inject }) }) ``` `npm run verify` 用模拟 loader 执行产物并打印: ``` loader calls: 1 id: dsh-plugin-mobile-ui factory: function externals requested: react, react-dom, react/jsx-runtime exports: apply, inject inject: ["slots","layout","uiWorkspace","connection"] slots injected: ["settings.section","shell.overlay","shell.overlay","tool.call.toolview"×5] registered -> name=… id=… order=… priority=… rendered -> … ok ``` - 四个服务是**必需依赖**:任一缺失,插件整体不 apply(不是局部失效)。 - 三类错误会被拦住:产物自带 React、缺少 `__ModuleLoader__.load` 外壳、缺少 `apply`/`inject`。 - 另有**陈旧产物守卫**:任何 `src/**.ts(x)` 比 `lib/client.js` 新即 exit 2 —— 防止"对着旧产物验证"。 ### 2.2 插槽注册表 | 插槽 | 形式 | 内容 | 开关 | |---|---|---|---| | `shell.overlay` | list(追加) | 启动页 | `splash` | | `shell.overlay` | list(追加) | 抽屉(「导航」入口 + 面板) | `drawerOverlay` | | `settings.section` | list(追加) | 设置页「移动端」分区 | `settings` | | `tool.call.toolview` | keyed ×5 | 工具卡(`pwsh/read/grep/edit/write`,`priority: -100`) | `toolCards` | | `sidebar` | single(替换) | 接管式抽屉,**默认关闭且违反硬约束** | `replaceSidebar: false` | - keyed 槽位**一个 key 一个优先级只能有一条**:以更低优先级注册即"遮蔽"宿主原卡;同优先级注册会抛错 → 全局 `Failed to load plugins`。`verify-bundle.mjs` 断言每个 keyed 注册都是负优先级。 - `inject` 包裹 `register` 是**强制写法**:未声明的 slot,`inject` 只是待命不报错;裸 `register` 会抛 `slot "X" is not declared`。 ### 2.3 宿主 chrome 打磨(不注册插槽) 对宿主自带界面的改动走"**带标记的样式表 + 少量运行时测量**": - 每张表注入为 `