@feiyang666/dsh-mobile-remote 1.4.6 → 1.5.4
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 +39 -1
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/RELEASE_NOTES.md +35 -22
- package/lib/index.js +425 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
# 更新日志 / Changelog
|
|
2
2
|
|
|
3
|
-
## v1.4
|
|
3
|
+
## v1.5.4 (2026-08-23)
|
|
4
|
+
|
|
5
|
+
### 新增 / New
|
|
6
|
+
- **移动端「上传中 / 发送中」状态指示**(`lib/index.js` MOBILE_CSS + MOBILE_JS):选择图片后发送,DSH 要先 `serializeImages`(读取图片转 base64,移动端大图可能数秒)再 `prompt`,期间会话列表不会立刻出现新消息,用户容易误以为没发出去。本版监听 composer textarea 的 `data-phase`,进入 `submitting` / `adjudicating` 阶段时在 composer 上方显示带 spinner 的浮层提示:有附件图片时显示「正在上传图片并发送…」,纯文本时显示「正在发送…」;phase 恢复后自动隐藏。不依赖 DSH 内部 API,只读 DOM 属性。
|
|
7
|
+
|
|
8
|
+
## v1.5.3 (2026-08-23)
|
|
9
|
+
|
|
10
|
+
### 修复 / Fixed
|
|
11
|
+
- **图片上传从未成功过的根因**(`lib/index.js` MOBILE_JS):图片类型校验正则写为 `/^image\//i`,但 MOBILE_JS 是注入到 `<script>` 的 JS 模板字符串,模板字符串中 `\/` 属于非转义序列,反斜杠会被丢弃,注入后变成 `/^image//i`,被解析成「正则 `/^image/` 除以 `i.test(...)`」,运行时报 `TypeError: i.test is not a function`,导致从 v1.5.0 起手机端选图后输入框一直没有缩略图。本版改用 `indexOf('image/')` 做类型判断,彻底绕开正则转义问题。
|
|
12
|
+
|
|
13
|
+
## v1.5.2 (2026-08-23)
|
|
14
|
+
|
|
15
|
+
### 修复 / Fixed
|
|
16
|
+
- **图片 intake 仍失败(React 事件处理器崩溃)**(`lib/index.js` MOBILE_JS):v1.5.1 的模拟 `clipboardData` 缺少 DSH `onPaste` 会调用的 `getData()`,派发 paste 时抛 `TypeError` 导致 React 事件处理器崩溃、输入框不显示缩略图。本版为模拟 `clipboardData` 补齐 `getData` / `setData` / `clearData`(`getData` 返回空文本,图片走文件分支),paste 链路不再抛错。
|
|
17
|
+
- **模型选择弹窗位置**(`lib/index.js` MOBILE_CSS):底部抽屉底部边距由 8px 上移到 24px,避免贴屏太靠下。
|
|
18
|
+
|
|
19
|
+
## v1.5.1 (2026-08-23)
|
|
20
|
+
|
|
21
|
+
### 修复 / Fixed
|
|
22
|
+
- **手机端仍无法选择图片**(`lib/index.js` MOBILE_JS):v1.5.0 的 intake 依赖 `new DataTransfer()` 构造器,iOS Safari 上构造出的 `DataTransfer` 添加文件后 `files` 返回空 FileList、`item.getAsFile()` 可能返回 null,导致「当前浏览器不支持直接上传 / 图片未添加上」。本版改为**用普通 JS 对象模拟 `clipboardData` / `dataTransfer` 接口**(DSH 的 `onPaste` 只读 `clipboardData.items` 的 `kind` / `getAsFile()`,`onDrop` 只读 `dataTransfer.types` / `.files`,模拟对象即可满足),彻底绕开 DataTransfer / ClipboardEvent 构造器兼容问题;并**优先派发 `paste`**(React 合成事件,兼容性最好)、`drop` 作兜底;隐藏文件选择器改为**等 `document.body` 就绪后再挂载**,避免部分 iOS WebView 拒绝 `.click()` 打开系统相册。
|
|
23
|
+
- **移动端模型选择弹窗遮挡左侧工具区**(`lib/index.js` MOBILE_CSS + MOBILE_JS):ModelSelect 菜单为绝对定位浮层(`right: 0`、宽 240px),在窄屏上从右侧锚点向左展开,把 composer 左侧的 + / 上传图片等按钮盖住。本版将带 `aria-label` 的 `[role=menu]`(模型选择菜单)在移动端改为**底部抽屉(bottom sheet)**:`position: fixed` 贴底、左右留 8px、宽度自适应屏幕、`max-height: 62vh` 内部滚动,不再从锚点向左侧/上方展开遮挡左侧内容;权限选择菜单保持原有浮层 + 水平钳制(`mrClampMenus` 对 fixed 菜单跳过平移)。
|
|
24
|
+
|
|
25
|
+
## v1.5.0 (2026-08-22)
|
|
26
|
+
|
|
27
|
+
### 新增 / Added
|
|
28
|
+
- **移动端 composer 上传图片入口**(`lib/index.js` MOBILE_JS + MOBILE_CSS):DSH 桌面端原生支持图片消息(粘贴 / 拖放,含格式 / 数量 / 大小预检查、缩略图 rail、随消息上传),但移动端没有「选择图片」入口(工具行的 + 按钮只打开命令菜单)。本版在移动端 composer 工具行、+ 加号按钮旁注入一个「上传图片」按钮:
|
|
29
|
+
- 点击唤起手机系统相册 / 文件选择器(`<input type="file" accept="image/*" multiple>`);
|
|
30
|
+
- 选中后把 `File` 装入 `DataTransfer`,向输入框派发 `paste` 事件,DSH 原生 `onPaste` 的 `intakeImages` 自动接管(预检查、缩略图、随消息发送);浏览器不支持构造 `ClipboardEvent` 时自动回退为向 `document` 派发 `drop` 事件;
|
|
31
|
+
- **不依赖 DSH 内部 API**,harness 升级后只要 paste / drop 链路仍在即可正常工作;
|
|
32
|
+
- 按钮禁用态跟随输入框(session 移除 / 锁定 / 提交中自动置灰),选完文件自动重置可重复选图。
|
|
33
|
+
- **移动端图片体验配套优化**(`lib/index.js` MOBILE_CSS):
|
|
34
|
+
- 缩略图 rail 的删除按钮触控目标从 18px 加大到 24px(结构选择器定位,不依赖哈希类名);
|
|
35
|
+
- 自备极简提示气泡(`#dsh-mr-toast`,2.4s 淡入淡出),用于选图失败 / 浏览器不支持时的友好提示,不动 DSH 原生 Toast。
|
|
36
|
+
|
|
37
|
+
### 修复 / Fixed
|
|
38
|
+
- **选图后输入框看不到图片**(`lib/index.js` MOBILE_JS):原先依赖「构造 `ClipboardEvent` / `DragEvent` 并携带 `DataTransfer`」派发 paste 事件,不同浏览器对该 API 的支持不一致(尤其 iOS Safari 的 `new DataTransfer()` 及事件构造器)。现改为**最通用的 `new Event()` + 手动挂载 `dataTransfer` / `clipboardData` 属性**派发,且**优先派发 `drop` 到 `document`**(DSH 的 onDrop 是原生监听器,最可靠),`paste` 到 textarea(React 合成事件)作兜底;成功与否以「缩略图是否真的出现在 rail」为准(而非 `defaultPrevented`,锁定 / 提交中 DSH 也会 preventDefault),全部失败才提示,安卓 / 桌面浏览器均可正常选图。
|
|
39
|
+
- **权限选择弹窗未出现在按钮正上方**(`lib/index.js` MOBILE_CSS + MOBILE_JS):DSH 的 `Menu`(PermissionSelect)是绝对定位浮层(`left: 0`)且无 viewport 钳制,锚点在工具行左区时菜单向右展开容易溢出屏幕右侧。移动端新增 `[role="menu"]` 宽度钳制(`max-width: calc(100vw - 16px)`),并在菜单出现时用 `translateX` 把越界的菜单平移回视口内。
|
|
40
|
+
- **模型选择弹窗向左偏、信息显示不全**(`lib/index.js` MOBILE_CSS + MOBILE_JS):ModelSelect 菜单 `right: 0` 对齐锚点右缘,锚点靠左时菜单左端超出屏幕左侧。同上由 `mrClampMenus` 统一钳制水平位置,保证菜单完整落在视口内。
|
|
41
|
+
|
|
4
42
|
|
|
5
43
|
### 修复 / Fixed
|
|
6
44
|
- **启用远程控制后写坏 profile 的 `cordis.patch.yml`,服务启动即崩溃**(`lib/index.js` 的 `setPatchEnabled`、`lib/external.js` 的 `ensureTrustedHost`):官方模板生成的 profile patch 文件内容是 `# user patch layer for this profile\n[]`(空数组占位),插件往文件**末尾追加** `- id: webserver` 等列表块时未先移除该 `[]` 行,导致同一文件出现两个 YAML 文档且无 `---` 分隔符,js-yaml 解析报 `YAMLException: end of the stream or a document separator is expected`,dsh 服务启动即崩溃。本版在写入前过滤掉裸 `[]` 占位行,patch 文件始终是单一文档,已用官方 `entryListSchema` 验证可解析。
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
`dsh-mobile-remote` is a DeepSeek Harness ecosystem **mobile remote-control plugin** (DSH plugin, Host + Client in one package). After installation, a management panel appears under **Settings → Remote Control** in the Web UI:
|
|
22
22
|
|
|
23
|
+
- **Mobile image upload** (v1.5.0): a new **"Upload image" button next to the + in the composer toolbar** lets phone users pick photos (system gallery / file picker) for DeepSeek's vision models — format/count/size pre-checks, thumbnail rail, and send-time upload all reuse DSH's native pipeline; the desktop UI is untouched.
|
|
23
24
|
- **Connection QR code**: auto-generates a QR for `http://<lan-ip>:<port>` — scan it with your phone's camera / browser to connect.
|
|
24
25
|
- **On / Off toggle**: switches the webserver listen address (`0.0.0.0` ↔ `127.0.0.1`) with one click. Applied via dsh's official HMR hot-reload — **no service restart needed**.
|
|
25
26
|
- **Connected device count**: mobile heartbeats report online devices in real time.
|
|
@@ -149,7 +150,7 @@ So for users, **installing is one command** — no YAML editing, no manual file
|
|
|
149
150
|
- **Toggle**: `POST /__dsh_remote/toggle` → writes/removes a `webserver` override block in the profile's `cordis.patch.yml` → dsh's `watchUserPatches` (Cordis HMR) hot-reloads the webserver entry to re-listen.
|
|
150
151
|
- **Device count**: mobile-injected JS heartbeats every 30s; the host keeps an active-device map (90s TTL). Each heartbeat now reports full device metadata (screen / viewport / DPR / network / current page / language / platform), shown in the expandable device rows of the settings panel.
|
|
151
152
|
- **QR code**: `GET /__dsh_remote/qr?url=...` returns SVG.
|
|
152
|
-
- **Mobile polish**: `tapIndex` injects mobile CSS/JS — composer toolbar wraps on narrow screens, selects get width-capped, iOS inputs get 16px to prevent focus zoom; **DSH's native rail + hamburger drawer interactions are untouched**;
|
|
153
|
+
- **Mobile polish**: `tapIndex` injects mobile CSS/JS — composer toolbar wraps on narrow screens, selects get width-capped, iOS inputs get 16px to prevent focus zoom, and an **"Upload image" button is injected next to the + in the composer toolbar** (system file picker → DSH's native paste/drop image-intake pipeline); **DSH's native rail + hamburger drawer interactions are untouched**;
|
|
153
154
|
- **Mobile settings page** (v1.4.2+): the native settings dialog becomes a full-screen single column on phones — the nav rail turns into a horizontally scrollable tab bar and the content column fills the remaining space; persistent native tooltip bubbles (e.g. the "stop / start / close sidebar" hints that stick on touch) are suppressed. The dialog is detected structurally, **not via harness-internal hashed class names**, so the plugin keeps working after DeepSeek Harness updates without modification. **v1.4.3** fixes the single-column settings layout — the panel was still a row-flex, collapsing the content column to zero width (blank content) and blocking vertical scroll; it now forces `flex-direction: column` plus `min-height: 0` so the options area actually scrolls on phones. It also adds a **`crypto.randomUUID` fallback for insecure contexts (LAN `http://`)**: current DSH clients call `crypto.randomUUID()` on the wire, which is only defined on HTTPS or localhost, so LAN direct access failed with `crypto.randomUUID is not a function` / `settings are unavailable in this browser`; the injected head script now provides an RFC 4122 v4 shim backed by `getRandomValues`.
|
|
154
155
|
|
|
155
156
|
---
|
package/README.zh.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
`dsh-mobile-remote` 是 DeepSeek Harness 生态的**移动端远程控制插件**(DSH plugin,Host + Client 双面一体包)。装好后在 WebUI **设置 → 远程控制** 出现一个管理面板:
|
|
22
22
|
|
|
23
|
+
- **移动端上传图片**(v1.5.0):composer 工具行 **+ 加号旁新增「上传图片」按钮**,手机直接选图(系统相册 / 文件选择器)即可让 DeepSeek 识图模型识别——格式 / 数量 / 大小预检查、缩略图预览、随消息上传全部复用 DSH 原生链路,桌面端不受影响;
|
|
23
24
|
- **连接二维码**:自动生成 `http://<局域网IP>:<端口>` 的二维码,手机相机 / 浏览器扫码即连;
|
|
24
25
|
- **开启 / 关闭开关**:一键切换 webserver 监听地址(`0.0.0.0` ↔ `127.0.0.1`),经 dsh 官方 HMR 热重载**无需重启服务**;
|
|
25
26
|
- **当前连接的设备数量**:移动端心跳上报,实时统计在线设备数;
|
|
@@ -149,7 +150,7 @@ dsh --profile web --patch remote-control.patch.yml
|
|
|
149
150
|
- **开关**:`POST /__dsh_remote/toggle` → 写入 / 移除 profile `cordis.patch.yml` 的 `webserver` 覆盖块 → dsh `watchUserPatches`(Cordis HMR)热重载 webserver 行重新监听。
|
|
150
151
|
- **设备数**:移动端注入 JS 每 30s 心跳上报,host 维护活跃设备表(90s 过期)。每次心跳会上报完整设备元数据(屏幕 / 视口 / DPR / 网络 / 当前页面 / 语言 / 平台),设置面板的设备行可展开查看。
|
|
151
152
|
- **二维码**:`GET /__dsh_remote/qr?url=...` 返回 SVG。
|
|
152
|
-
- **移动端适配**:`tapIndex` 注入移动端 CSS/JS,composer 输入栏窄屏换行、选择器限宽、iOS 输入框 16px
|
|
153
|
+
- **移动端适配**:`tapIndex` 注入移动端 CSS/JS,composer 输入栏窄屏换行、选择器限宽、iOS 输入框 16px 防缩放、**加号旁注入「上传图片」按钮**(文件选择器选图 → 复用 DSH 原生 paste/drop intake 链路);**不破坏 DSH 原生 rail + 汉堡抽屉交互**;
|
|
153
154
|
- **移动端设置页适配**(v1.4.2+):原生设置弹窗在手机上自动变为全屏单栏——导航栏变顶部横向滚动标签、内容区占满剩余空间;同时抑制触屏上常驻的原生 Tooltip 气泡(如「停止 / 开始 / 关闭菜单栏」提示文字)。通过结构标记识别设置弹窗,**不依赖 harness 内部哈希类名**,harness 升级后无需修改插件即可直接适配。**v1.4.3** 修复该单栏布局——面板原本仍是横向 `flex`,内容列被压成 0 宽度(内容空白)且无法上下滚动;现强制 `flex-direction: column` + `min-height: 0`,设置选项区在手机上可正常滚动。并新增**内网(非安全上下文)`crypto.randomUUID` 兜底**:当前 DSH 客户端在连接时直接调用 `crypto.randomUUID()`,该 Web Crypto API 仅在 HTTPS / localhost 存在,内网 http 直连会报 `crypto.randomUUID is not a function` / `settings are unavailable in this browser`;插件注入的头部脚本用 `getRandomValues` 补一个 RFC 4122 v4 实现,内网访问 / 扫码连接恢复正常。
|
|
154
155
|
|
|
155
156
|
---
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# dsh-mobile-remote v1.4
|
|
1
|
+
# dsh-mobile-remote v1.5.4
|
|
2
2
|
|
|
3
|
-
**DeepSeek Harness 移动端远程控制插件** —— 在 Web UI 设置页内置「远程控制」:连接二维码、一键开关、在线设备数,让手机通过局域网访问并操控电脑上的 DeepSeek Harness
|
|
3
|
+
**DeepSeek Harness 移动端远程控制插件** —— 在 Web UI 设置页内置「远程控制」:连接二维码、一键开关、在线设备数,让手机通过局域网访问并操控电脑上的 DeepSeek Harness。移动端提供上传图片入口、底部抽屉式模型选择、发送状态提示等移动优先体验。
|
|
4
4
|
|
|
5
5
|
## 安装方式
|
|
6
6
|
|
|
@@ -10,37 +10,50 @@ dsh plugin --profile web add @feiyang666/dsh-mobile-remote
|
|
|
10
10
|
|
|
11
11
|
安装完成后**重启 dsh web 服务**,打开 `http://127.0.0.1:3080` → **设置 → 远程控制** 即可使用。
|
|
12
12
|
|
|
13
|
-
## 本版更新(v1.4
|
|
13
|
+
## 本版更新(v1.5.4)
|
|
14
14
|
|
|
15
|
-
### 🐛
|
|
15
|
+
### 🐛 修复移动端无法选择图片(v1.5.0 起就存在的根因)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
图片类型校验正则写为 `/^image\//i`,但该代码经 JS **模板字符串**注入 `<script>`,模板字符串中 `\/` 属于非转义序列、反斜杠会被丢弃,注入后变成 `/^image//i`,被解析成「正则 `/^image/` 除以 `i.test(...)`」,运行时报 `TypeError: i.test is not a function`——从 v1.5.0 起手机端选图后输入框一直没有缩略图。本版改用 `indexOf('image/')` 判断图片类型,彻底绕开正则转义问题。
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
同时配套修复:
|
|
20
|
+
- 图片 intake 不再依赖 `new DataTransfer()`(iOS Safari 上 `files` 为空、`getAsFile()` 返回 null),改用普通 JS 对象模拟 `clipboardData` / `dataTransfer` 接口;
|
|
21
|
+
- 模拟 `clipboardData` 补齐 DSH `onPaste` 会调用的 `getData()`,避免 React 事件处理器崩溃;
|
|
22
|
+
- 隐藏文件选择器改为等 `document.body` 就绪后再挂载,避免部分 iOS WebView 拒绝 `.click()` 打开系统相册。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
### 🆕 移动端「上传中 / 发送中」状态指示
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
发送图片消息时,DSH 会先 `serializeImages`(读取图片转 base64,移动端大图可能持续数秒)再 `prompt`,期间会话列表不会立刻出现新消息,用户容易误以为「没发出去」。本版监听 composer textarea 的 `data-phase`,进入 `submitting` / `adjudicating` 阶段时在 composer 上方显示带 spinner 的浮层提示:
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
- 有附件图片时显示「正在上传图片并发送…」;
|
|
29
|
+
- 纯文本时显示「正在发送…」;
|
|
30
|
+
- 发送完成(phase 恢复)后自动隐藏。
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
### 🐛 模型选择弹窗在移动端改为底部抽屉
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
client-modules: bundle /plugins/@feiyang666/dsh-mobile-remote/client.js?rev=...
|
|
36
|
-
loaded without registering "@feiyang666/dsh-mobile-remote" via __ModuleLoader__.load
|
|
37
|
-
```
|
|
34
|
+
ModelSelect 菜单为绝对定位浮层(`right: 0`、宽 240px),窄屏下从右侧锚点向左展开,把 composer 左侧的 + / 上传图片等按钮盖住。本版将带 `aria-label` 的 `[role=menu]`(模型选择菜单)在移动端改为**底部抽屉(bottom sheet)**:贴底、左右留 8px、距底部 24px、宽度自适应屏幕、`max-height: 62vh` 内部滚动,不再遮挡左侧内容;权限选择菜单保持原有浮层 + 水平钳制。
|
|
35
|
+
|
|
36
|
+
## 历史更新(v1.5.0)
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
### 🆕 移动端上传图片(composer 加号旁的「上传图片」按钮)
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
DSH 桌面端原生支持图片消息(粘贴 / 拖放,含格式 / 数量 / 大小预检查、缩略图 rail、随消息上传),但移动端没有「选择图片」的入口——工具行的 + 按钮只打开命令菜单。本版在**移动端 composer 工具行、+ 加号按钮旁**注入一个「上传图片」按钮:
|
|
41
|
+
|
|
42
|
+
- 点击按钮,直接唤起手机**系统相册 / 文件选择器**(支持一次选多张);
|
|
43
|
+
- 选中后把图片喂给 DSH **原生 intake 链路**(`paste` / `drop` 事件),格式 / 数量 / 大小预检查、缩略图预览、随消息上传全部复用官方实现;
|
|
44
|
+
- **不依赖 DSH 内部 API**,harness 升级后只要 paste / drop 链路仍在即可正常工作;
|
|
45
|
+
- 按钮禁用态跟随输入框(session 移除 / 锁定 / 提交中自动置灰);选完文件自动重置,可重复选同一张图;
|
|
46
|
+
- 配套优化:缩略图 rail 删除按钮触控目标加大(18px → 24px),选图失败时自备极简提示气泡。
|
|
47
|
+
|
|
48
|
+
> 仅移动端(`<1024px`)生效,桌面端界面不受任何影响。
|
|
49
|
+
|
|
50
|
+
### 🐛 修复启用远程控制后写坏 profile 的 cordis.patch.yml(服务启动即崩溃)
|
|
51
|
+
|
|
52
|
+
在设置页开启「远程控制」并重启服务后,dsh 启动即崩溃。**根因**:官方模板生成的 profile patch 文件内容是 `[]`(空数组占位),插件往文件末尾追加列表块时未先移除占位行,导致同一文件出现两个 YAML 文档且无 `---` 分隔符。**修复**:写入前过滤掉裸 `[]` 占位行(`setPatchEnabled` / `ensureTrustedHost`)。
|
|
53
|
+
|
|
54
|
+
### 🐛 修复客户端插件加载失败(client-modules: loaded without registering)
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
client 侧 bundle 中 `__ModuleLoader__.load` 的 `id` 写成了无 scope 的裸包名,改为完整包名 `@feiyang666/dsh-mobile-remote`,与官方插件的 client bundle 写法一致。
|
|
44
57
|
|
|
45
58
|
> 已安装旧版本的用户:请先卸载本插件(桌面端「插件管理」或 `dsh plugin --profile web remove @feiyang666/dsh-mobile-remote`),再重新安装本修复版。
|
|
46
59
|
|
package/lib/index.js
CHANGED
|
@@ -1225,6 +1225,141 @@ html[data-dsh-mobile] [data-dsh-settings] > div > div:first-child button {
|
|
|
1225
1225
|
html[data-dsh-mobile] [data-dsh-settings] > div > div:last-child {
|
|
1226
1226
|
padding: 0 12px 16px !important;
|
|
1227
1227
|
}
|
|
1228
|
+
|
|
1229
|
+
/* ----- 移动端上传图片按钮(composer 工具行,+ 加号旁)-----
|
|
1230
|
+
* 由 MOBILE_JS 注入到 [data-composer-card] 工具行。视觉上与 DSH 原生
|
|
1231
|
+
* 加号按钮同款(28px 圆、selector 填充),保证窄屏下排在一起不突兀。
|
|
1232
|
+
*/
|
|
1233
|
+
html[data-dsh-mobile] [data-composer-card] .dsh-mr-attach {
|
|
1234
|
+
display: grid;
|
|
1235
|
+
place-items: center;
|
|
1236
|
+
flex: none;
|
|
1237
|
+
width: 28px;
|
|
1238
|
+
height: 28px;
|
|
1239
|
+
margin: 0;
|
|
1240
|
+
padding: 0;
|
|
1241
|
+
border: none;
|
|
1242
|
+
border-radius: 999px;
|
|
1243
|
+
background: var(--dsw-specific-selector, #ececee);
|
|
1244
|
+
color: var(--dsw-alias-label-primary, #1f1f1f);
|
|
1245
|
+
cursor: pointer;
|
|
1246
|
+
-webkit-tap-highlight-color: transparent;
|
|
1247
|
+
transition: background-color 100ms ease;
|
|
1248
|
+
}
|
|
1249
|
+
html[data-dsh-mobile] [data-composer-card] .dsh-mr-attach:active {
|
|
1250
|
+
background: var(--dsw-alias-interactive-bg-hover-solid, #e0e0e3);
|
|
1251
|
+
}
|
|
1252
|
+
html[data-dsh-mobile] [data-composer-card] .dsh-mr-attach:disabled {
|
|
1253
|
+
opacity: 0.45;
|
|
1254
|
+
cursor: default;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* 发送状态指示:composer 上方的「上传中 / 发送中」小浮层(含旋转 spinner)。
|
|
1258
|
+
* 位置:absolute 相对 [data-composer-card](其 position:relative 且无
|
|
1259
|
+
* overflow 裁剪),贴在卡片上沿外侧。 */
|
|
1260
|
+
html[data-dsh-mobile] [data-composer-card] .dsh-mr-sending {
|
|
1261
|
+
position: absolute;
|
|
1262
|
+
left: 12px;
|
|
1263
|
+
right: 12px;
|
|
1264
|
+
bottom: calc(100% + 8px);
|
|
1265
|
+
z-index: 30;
|
|
1266
|
+
display: flex;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
gap: 8px;
|
|
1269
|
+
padding: 8px 12px;
|
|
1270
|
+
border-radius: 10px;
|
|
1271
|
+
background: var(--dsw-alias-surface-solid, #ffffff);
|
|
1272
|
+
border: 1px solid var(--dsw-alias-divider, rgba(0, 0, 0, 0.08));
|
|
1273
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
1274
|
+
font-size: 13px;
|
|
1275
|
+
line-height: 1.4;
|
|
1276
|
+
color: var(--dsw-alias-text-tertiary, #666);
|
|
1277
|
+
}
|
|
1278
|
+
html[data-dsh-mobile] [data-composer-card] .dsh-mr-sending .dsh-mr-spinner {
|
|
1279
|
+
width: 14px;
|
|
1280
|
+
height: 14px;
|
|
1281
|
+
flex: none;
|
|
1282
|
+
border: 2px solid var(--dsw-alias-divider, rgba(0, 0, 0, 0.12));
|
|
1283
|
+
border-top-color: currentColor;
|
|
1284
|
+
border-radius: 50%;
|
|
1285
|
+
animation: dsh-mr-spin 0.8s linear infinite;
|
|
1286
|
+
}
|
|
1287
|
+
@keyframes dsh-mr-spin {
|
|
1288
|
+
to { transform: rotate(360deg); }
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/* ----- 移动端上传轻提示气泡(自备极简 Toast)----- */
|
|
1292
|
+
#dsh-mr-toast {
|
|
1293
|
+
position: fixed;
|
|
1294
|
+
left: 50%;
|
|
1295
|
+
bottom: 96px;
|
|
1296
|
+
transform: translateX(-50%);
|
|
1297
|
+
z-index: 2147483000;
|
|
1298
|
+
max-width: min(86vw, 340px);
|
|
1299
|
+
padding: 9px 16px;
|
|
1300
|
+
border-radius: 10px;
|
|
1301
|
+
font-size: 13px;
|
|
1302
|
+
line-height: 1.5;
|
|
1303
|
+
text-align: center;
|
|
1304
|
+
color: var(--dsw-alias-label-primary-inverted, #fff);
|
|
1305
|
+
background: var(--dsw-alias-button-contrast-fill, rgba(0, 0, 0, 0.82));
|
|
1306
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
|
|
1307
|
+
pointer-events: none;
|
|
1308
|
+
opacity: 0;
|
|
1309
|
+
animation: dsh-mr-toast-in 2.4s ease forwards;
|
|
1310
|
+
}
|
|
1311
|
+
@keyframes dsh-mr-toast-in {
|
|
1312
|
+
0% { opacity: 0; transform: translateX(-50%) translateY(8px); }
|
|
1313
|
+
8% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1314
|
+
86% { opacity: 1; }
|
|
1315
|
+
100% { opacity: 0; transform: translateX(-50%) translateY(-4px); }
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/* ----- 移动端图片缩略图 rail:删除按钮触控目标加大 -----
|
|
1319
|
+
* DSH 原生 AttachmentRail 的删除按钮只有 18px,触屏上太小。
|
|
1320
|
+
* 结构:rail([role=group]) > item(div) > button(缩略图) + button(删除)。
|
|
1321
|
+
*/
|
|
1322
|
+
html[data-dsh-mobile] [data-composer-card] [role='group'] > div > button:last-child {
|
|
1323
|
+
width: 24px !important;
|
|
1324
|
+
height: 24px !important;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/* ----- 移动端弹窗(权限选择 / 模型选择菜单)适配 -----
|
|
1328
|
+
* DSH 的 Menu(PermissionSelect 权限选择)与 ModelSelect(模型选择)都是
|
|
1329
|
+
* 绝对定位浮层且不含 viewport 水平钳制:
|
|
1330
|
+
* - 权限菜单 .list:left:0 + min-width:218px / max-width:360px——锚点在
|
|
1331
|
+
* 左区时菜单向右展开,容易超出屏幕右侧;
|
|
1332
|
+
* - 模型菜单 .menu:right:0 + width:min(240px, calc(100vw-32px))——锚点
|
|
1333
|
+
* 在右区时向左展开,容易向左偏/遮挡左侧工具区。
|
|
1334
|
+
* 这里:
|
|
1335
|
+
* - 权限菜单(无 aria-label)统一限制宽度不超屏;水平位置由 MOBILE_JS
|
|
1336
|
+
* 在菜单出现时用 translateX 钳制到视口内(无 transform 动画,可安全平移);
|
|
1337
|
+
* - 模型菜单(带 aria-label 的 [role=menu])在移动端改为「底部抽屉」
|
|
1338
|
+
* (bottom sheet):贴底、宽度自适应屏幕、高度受限内部滚动,不再从
|
|
1339
|
+
* 锚点向左侧/上方展开遮挡 composer 左侧的 + / 上传图片等按钮。
|
|
1340
|
+
*/
|
|
1341
|
+
html[data-dsh-mobile] [data-composer-card] [role='menu'] {
|
|
1342
|
+
max-width: calc(100vw - 16px) !important;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/* 模型选择菜单(ModelSelect 的 .menu 带 aria-label,可据此与权限菜单区分;
|
|
1346
|
+
* 不依赖 harness 内部哈希类名)——移动端底部抽屉样式 */
|
|
1347
|
+
html[data-dsh-mobile] [data-composer-card] [role='menu'][aria-label] {
|
|
1348
|
+
position: fixed !important;
|
|
1349
|
+
top: auto !important;
|
|
1350
|
+
left: 8px !important;
|
|
1351
|
+
right: 8px !important;
|
|
1352
|
+
bottom: 24px; /* fallback:不支持 max()/env() 的旧浏览器也保持上移 */
|
|
1353
|
+
bottom: max(24px, env(safe-area-inset-bottom, 24px)) !important;
|
|
1354
|
+
width: auto !important;
|
|
1355
|
+
min-width: 0 !important;
|
|
1356
|
+
max-width: none !important;
|
|
1357
|
+
max-height: 62vh !important;
|
|
1358
|
+
z-index: 1000 !important;
|
|
1359
|
+
border-radius: 16px !important;
|
|
1360
|
+
overflow: hidden !important;
|
|
1361
|
+
overscroll-behavior: contain !important;
|
|
1362
|
+
}
|
|
1228
1363
|
`
|
|
1229
1364
|
|
|
1230
1365
|
/* ------------------------------------------------------------------ */
|
|
@@ -1353,6 +1488,296 @@ const MOBILE_JS = `
|
|
|
1353
1488
|
dpr: window.devicePixelRatio || 1,
|
|
1354
1489
|
lanEnabled: cfg.lanEnabled === true,
|
|
1355
1490
|
};
|
|
1491
|
+
|
|
1492
|
+
/* ==================================================================
|
|
1493
|
+
* 移动端上传图片(composer 加号旁的「上传图片」入口)
|
|
1494
|
+
* ----------------------------------------------------------------
|
|
1495
|
+
* DSH 桌面端原生支持图片消息(粘贴 / 拖放均可,格式/数量/大小预检查、
|
|
1496
|
+
* 缩略图 rail、随消息上传全部由官方链路完成),但移动端没有「选择图片」
|
|
1497
|
+
* 的入口:工具行的 + 按钮只打开命令菜单。
|
|
1498
|
+
*
|
|
1499
|
+
* 此处注入一个图片按钮 + 隐藏的 <input type=file>。用户在手机上通过
|
|
1500
|
+
* 系统相册 / 文件选择器选图后,用普通对象模拟 clipboardData / dataTransfer
|
|
1501
|
+
* (不依赖 new DataTransfer(),iOS Safari 兼容性差)派发 paste 事件——
|
|
1502
|
+
* DSH 的 onPaste 原生 intakeImages 自动接管;若 paste 未生效,再回退向
|
|
1503
|
+
* document 派发 drop 事件(DSH 的文档级 onDrop 同样走 intakeImages)。
|
|
1504
|
+
* 全程不依赖 DSH 内部 API,harness 升级后只要 paste / drop 链路还在即可
|
|
1505
|
+
* 正常工作。
|
|
1506
|
+
* ================================================================== */
|
|
1507
|
+
|
|
1508
|
+
// 轻提示:DSH 的 Toast 是内部组件,这里自备一个极简气泡。
|
|
1509
|
+
var mrToastTimer = null;
|
|
1510
|
+
function mrToast(text) {
|
|
1511
|
+
var old = document.getElementById('dsh-mr-toast');
|
|
1512
|
+
if (old) { try { old.parentNode.removeChild(old); } catch (e) {} }
|
|
1513
|
+
var t = document.createElement('div');
|
|
1514
|
+
t.id = 'dsh-mr-toast';
|
|
1515
|
+
t.textContent = text;
|
|
1516
|
+
(document.body || document.documentElement).appendChild(t);
|
|
1517
|
+
if (mrToastTimer) clearTimeout(mrToastTimer);
|
|
1518
|
+
mrToastTimer = setTimeout(function () {
|
|
1519
|
+
try { t.parentNode && t.parentNode.removeChild(t); } catch (e) {}
|
|
1520
|
+
}, 2400);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// 隐藏的文件选择器(全局唯一,选完即重置,可重复选同一张图)。
|
|
1524
|
+
// 注意:本脚本注入在 <head>,此时 document.body 可能尚未创建;若直接
|
|
1525
|
+
// 挂到 documentElement,部分 iOS WebView 会拒绝 .click() 打开选择器,
|
|
1526
|
+
// 因此等 body 就绪后再挂载(change 监听与挂载时机无关)。
|
|
1527
|
+
var mrFileInput = document.createElement('input');
|
|
1528
|
+
mrFileInput.type = 'file';
|
|
1529
|
+
mrFileInput.accept = 'image/*';
|
|
1530
|
+
mrFileInput.multiple = true;
|
|
1531
|
+
mrFileInput.tabIndex = -1;
|
|
1532
|
+
mrFileInput.setAttribute('aria-hidden', 'true');
|
|
1533
|
+
mrFileInput.style.cssText = 'position:fixed;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;';
|
|
1534
|
+
mrFileInput.addEventListener('change', function () {
|
|
1535
|
+
var files = [];
|
|
1536
|
+
try { files = Array.prototype.slice.call(mrFileInput.files || []); } catch (e) {}
|
|
1537
|
+
if (files.length > 0) mrIntakeFiles(files);
|
|
1538
|
+
try { mrFileInput.value = ''; } catch (e) {}
|
|
1539
|
+
});
|
|
1540
|
+
function mrMountFileInput() {
|
|
1541
|
+
var host = document.body || document.documentElement;
|
|
1542
|
+
if (host && !mrFileInput.parentNode) host.appendChild(mrFileInput);
|
|
1543
|
+
}
|
|
1544
|
+
if (document.body) mrMountFileInput();
|
|
1545
|
+
else document.addEventListener('DOMContentLoaded', mrMountFileInput, { once: true });
|
|
1546
|
+
|
|
1547
|
+
// ===== 图片 intake:兼容各种浏览器的事件派发 =====
|
|
1548
|
+
// DSH 原生 intake 读两个事件:
|
|
1549
|
+
// - onPaste(React 合成事件):e.clipboardData.items —— 只要求 items
|
|
1550
|
+
// 可迭代、每项 kind==='file' 且 getAsFile() 返回真实 File;
|
|
1551
|
+
// - onDrop(document 原生监听器):e.dataTransfer.types / .files。
|
|
1552
|
+
// 我们不构造 DataTransfer / ClipboardEvent(iOS Safari 上
|
|
1553
|
+
// new DataTransfer() + items.add() 之后 files 返回空 FileList、
|
|
1554
|
+
// item.getAsFile() 可能返回 null,兼容性差),而是用普通 JS 对象
|
|
1555
|
+
// 模拟这两个接口——任何浏览器都可靠。结果用「缩略图是否真的出现」
|
|
1556
|
+
// 判断(不依赖 defaultPrevented——locked/machineBusy 时 DSH 也会
|
|
1557
|
+
// preventDefault)。
|
|
1558
|
+
|
|
1559
|
+
// 当前 composer 附件缩略图数量(rail 渲染后计数)。
|
|
1560
|
+
function mrCountRailImages() {
|
|
1561
|
+
try { return document.querySelectorAll('[data-composer-card] [role="group"] img').length; } catch (e) { return 0; }
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// 构造模拟 clipboardData(供 onPaste 使用):items 是普通数组,每项
|
|
1565
|
+
// 的 kind='file'、getAsFile() 返回真实 File;getData/setData/clearData
|
|
1566
|
+
// 补齐 DSH onPaste 会调用的接口(缺 getData 会抛 TypeError 导致 React
|
|
1567
|
+
// 事件处理器崩溃)。绕开 DataTransfer 构造器在 iOS Safari 上的 files
|
|
1568
|
+
// 为空 / getAsFile 返回 null 等兼容问题。
|
|
1569
|
+
function mrBuildClipboardData(images) {
|
|
1570
|
+
var items = [];
|
|
1571
|
+
for (var j = 0; j < images.length; j++) {
|
|
1572
|
+
(function (file) {
|
|
1573
|
+
items.push({
|
|
1574
|
+
kind: 'file',
|
|
1575
|
+
type: file.type || '',
|
|
1576
|
+
getAsFile: function () { return file; },
|
|
1577
|
+
});
|
|
1578
|
+
})(images[j]);
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1581
|
+
items: items,
|
|
1582
|
+
files: images,
|
|
1583
|
+
types: ['Files'],
|
|
1584
|
+
getData: function () { return ''; },
|
|
1585
|
+
setData: function () {},
|
|
1586
|
+
clearData: function () {},
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// 构造模拟 dataTransfer(供 onDrop 使用):只读 .types / .files。
|
|
1591
|
+
function mrBuildDataTransfer(images) {
|
|
1592
|
+
return { files: images, types: ['Files'] };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// 向 document 派发 drop(DSH 的原生 onDrop 监听器会 intakeImages)。
|
|
1596
|
+
function mrDispatchDrop(dt) {
|
|
1597
|
+
try {
|
|
1598
|
+
var evt = new Event('drop', { bubbles: true, cancelable: true });
|
|
1599
|
+
try { evt.dataTransfer = dt; } catch (e) {}
|
|
1600
|
+
document.dispatchEvent(evt);
|
|
1601
|
+
return true;
|
|
1602
|
+
} catch (e) { return false; }
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// 向 textarea 派发 paste(React 合成 onPaste 会 intakeImages)。
|
|
1606
|
+
function mrDispatchPaste(cd, textarea) {
|
|
1607
|
+
try {
|
|
1608
|
+
var evt = new Event('paste', { bubbles: true, cancelable: true });
|
|
1609
|
+
try { evt.clipboardData = cd; } catch (e) {}
|
|
1610
|
+
textarea.dispatchEvent(evt);
|
|
1611
|
+
return true;
|
|
1612
|
+
} catch (e) { return false; }
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// 把选中的图片喂给 DSH 原生 intake。
|
|
1616
|
+
function mrIntakeFiles(files) {
|
|
1617
|
+
var textarea = document.querySelector('[data-composer-card] [data-input-scroll] textarea');
|
|
1618
|
+
if (!textarea) { mrToast('未找到输入框,请重试'); return; }
|
|
1619
|
+
if (textarea.disabled || textarea.readOnly) { mrToast('当前输入框不可用'); return; }
|
|
1620
|
+
var images = [];
|
|
1621
|
+
for (var i = 0; i < files.length; i++) {
|
|
1622
|
+
// 注意:本函数经模板字符串注入 <script>,正则里的反斜杠转义(\/)
|
|
1623
|
+
// 会被当作非转义序列丢弃(/^image\//i 变成 /^image//i,解析成除法,
|
|
1624
|
+
// 运行时抛 i.test is not a function)。因此这里用 indexOf 判断类型。
|
|
1625
|
+
if ((files[i].type || '').indexOf('image/') === 0) images.push(files[i]);
|
|
1626
|
+
}
|
|
1627
|
+
if (images.length === 0) { mrToast('请选择图片文件'); return; }
|
|
1628
|
+
var before = mrCountRailImages();
|
|
1629
|
+
// 方式一:paste(React onPaste 读 clipboardData.items,模拟对象对
|
|
1630
|
+
// iOS Safari / 安卓 / 桌面全兼容)。
|
|
1631
|
+
mrDispatchPaste(mrBuildClipboardData(images), textarea);
|
|
1632
|
+
setTimeout(function () {
|
|
1633
|
+
if (mrCountRailImages() > before) return;
|
|
1634
|
+
// 方式二:drop 到 document(原生监听器兜底)。
|
|
1635
|
+
mrDispatchDrop(mrBuildDataTransfer(images));
|
|
1636
|
+
setTimeout(function () {
|
|
1637
|
+
if (mrCountRailImages() > before) return;
|
|
1638
|
+
mrToast('图片未添加上,请长按输入框粘贴图片');
|
|
1639
|
+
}, 300);
|
|
1640
|
+
}, 300);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// 图片按钮:与 DSH 原生 + 按钮同风格的小圆钮(SVG 图片图标)。
|
|
1644
|
+
function mrMakeAttachBtn() {
|
|
1645
|
+
var btn = document.createElement('button');
|
|
1646
|
+
btn.type = 'button';
|
|
1647
|
+
btn.className = 'dsh-mr-attach';
|
|
1648
|
+
btn.setAttribute('aria-label', '上传图片');
|
|
1649
|
+
btn.title = '上传图片';
|
|
1650
|
+
btn.innerHTML =
|
|
1651
|
+
'<svg viewBox="0 0 16 16" width="15" height="15" fill="none" aria-hidden="true">' +
|
|
1652
|
+
'<rect x="1.6" y="2.7" width="12.8" height="10.6" rx="2.3" stroke="currentColor" stroke-width="1.35"/>' +
|
|
1653
|
+
'<circle cx="5.4" cy="6.1" r="1.3" fill="currentColor"/>' +
|
|
1654
|
+
'<path d="M2.4 12.4L6.3 8.3a1.05 1.05 0 0 1 1.5 0l1.15 1.2 2.2-2.35a1.05 1.05 0 0 1 1.55 0l1.9 2.1" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
1655
|
+
'</svg>';
|
|
1656
|
+
btn.addEventListener('click', function (e) {
|
|
1657
|
+
e.preventDefault();
|
|
1658
|
+
if (btn.disabled) return;
|
|
1659
|
+
try { mrFileInput.value = ''; } catch (err) {}
|
|
1660
|
+
mrFileInput.click();
|
|
1661
|
+
});
|
|
1662
|
+
return btn;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// 同步按钮禁用态:跟随 composer 的 textarea(session 移除 / 锁定 / 提交中)。
|
|
1666
|
+
function mrSyncAttachState(btn) {
|
|
1667
|
+
if (!btn) return;
|
|
1668
|
+
var textarea = document.querySelector('[data-composer-card] [data-input-scroll] textarea');
|
|
1669
|
+
btn.disabled = !textarea || textarea.disabled || textarea.readOnly;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// 把上传按钮插到工具行(+ 加号按钮之后)。
|
|
1673
|
+
function mrInjectAttach() {
|
|
1674
|
+
var card = document.querySelector('[data-composer-card]');
|
|
1675
|
+
if (!card || card.querySelector('.dsh-mr-attach')) return;
|
|
1676
|
+
var tools = card.querySelector(':scope > div:last-child > div:first-child');
|
|
1677
|
+
if (!tools) return;
|
|
1678
|
+
var btn = mrMakeAttachBtn();
|
|
1679
|
+
tools.insertBefore(btn, tools.children[1] || null);
|
|
1680
|
+
mrSyncAttachState(btn);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// ===== 移动端弹窗水平钳制 =====
|
|
1684
|
+
// 权限选择菜单是绝对定位浮层,不含 viewport 钳制,窄屏下容易向右溢出
|
|
1685
|
+
// 或向左偏(模型选择菜单在移动端已改为底部抽屉,由 CSS 定位并自带
|
|
1686
|
+
// 钳制,见 mrClampMenus 内的 fixed 跳过)。菜单出现/布局变化时读取
|
|
1687
|
+
// rect,用 translateX 把它平移回视口内(菜单无 transform 动画,平移
|
|
1688
|
+
// 安全;每次覆盖式设置,不会累积偏移)。隐藏中的菜单 rect 为 0,跳过。
|
|
1689
|
+
function mrClampMenus() {
|
|
1690
|
+
var menus = document.querySelectorAll('[data-composer-card] [role="menu"]');
|
|
1691
|
+
for (var i = 0; i < menus.length; i++) {
|
|
1692
|
+
var menu = menus[i];
|
|
1693
|
+
try {
|
|
1694
|
+
// 模型选择菜单在移动端已改为固定底部抽屉(bottom sheet),
|
|
1695
|
+
// 位置由 CSS 决定且自带视口钳制,跳过平移。
|
|
1696
|
+
if (window.getComputedStyle(menu).position === 'fixed') continue;
|
|
1697
|
+
var r = menu.getBoundingClientRect();
|
|
1698
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
1699
|
+
var vw = window.innerWidth;
|
|
1700
|
+
var dx = 0;
|
|
1701
|
+
if (r.left < 8) dx = 8 - r.left;
|
|
1702
|
+
else if (r.right > vw - 8) dx = (vw - 8) - r.right;
|
|
1703
|
+
if (dx !== 0) menu.style.transform = 'translateX(' + dx + 'px)';
|
|
1704
|
+
} catch (e) { /* ignore */ }
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// ===== 发送状态指示 =====
|
|
1709
|
+
// DSH 点击发送后,machine 进入 'submitting'(消息)/ 'adjudicating'(命令
|
|
1710
|
+
// 裁决)phase:发送前要先把附件图片 serializeImages 成 base64 再 prompt,
|
|
1711
|
+
// 移动端大图这一步可能持续数秒,而会话列表要等发送成功才出现新消息,
|
|
1712
|
+
// 用户容易误以为「没发出去」而重发或离开。这里在 composer 上方显示
|
|
1713
|
+
// 「上传中 / 发送中」提示,phase 恢复后自动隐藏。只读 textarea 的
|
|
1714
|
+
// data-phase,不依赖 DSH 内部 API。
|
|
1715
|
+
var mrSendingEl = null;
|
|
1716
|
+
|
|
1717
|
+
function mrMakeSendingEl() {
|
|
1718
|
+
var el = document.createElement('div');
|
|
1719
|
+
el.className = 'dsh-mr-sending';
|
|
1720
|
+
el.setAttribute('role', 'status');
|
|
1721
|
+
el.setAttribute('aria-live', 'polite');
|
|
1722
|
+
return el;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function mrShowSending() {
|
|
1726
|
+
var card = document.querySelector('[data-composer-card]');
|
|
1727
|
+
if (!card) return;
|
|
1728
|
+
// 会话切换时 composer 卡片会被重建,旧浮层节点会脱离 DOM,需重建挂载。
|
|
1729
|
+
if (!mrSendingEl || !mrSendingEl.isConnected) {
|
|
1730
|
+
mrSendingEl = mrMakeSendingEl();
|
|
1731
|
+
card.appendChild(mrSendingEl);
|
|
1732
|
+
}
|
|
1733
|
+
var hasImages = mrCountRailImages() > 0;
|
|
1734
|
+
mrSendingEl.innerHTML = '<span class="dsh-mr-spinner"></span>' +
|
|
1735
|
+
(hasImages ? '正在上传图片并发送…' : '正在发送…');
|
|
1736
|
+
mrSendingEl.style.display = '';
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function mrHideSending() {
|
|
1740
|
+
if (mrSendingEl) mrSendingEl.style.display = 'none';
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function mrSyncSending() {
|
|
1744
|
+
var textarea = document.querySelector('[data-composer-card] [data-input-scroll] textarea');
|
|
1745
|
+
if (!textarea) { mrHideSending(); return; }
|
|
1746
|
+
var phase = textarea.getAttribute('data-phase') || '';
|
|
1747
|
+
if (phase === 'submitting' || phase === 'adjudicating') mrShowSending();
|
|
1748
|
+
else mrHideSending();
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function mrScan() {
|
|
1752
|
+
mrInjectAttach();
|
|
1753
|
+
var btn = document.querySelector('[data-composer-card] .dsh-mr-attach');
|
|
1754
|
+
mrSyncAttachState(btn);
|
|
1755
|
+
mrClampMenus();
|
|
1756
|
+
mrSyncSending();
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// composer 是动态挂载/重建的(会话切换),用 MutationObserver 跟踪挂载
|
|
1760
|
+
// 与 textarea 锁定态变化;requestAnimationFrame 合并高频回调。
|
|
1761
|
+
mrScan();
|
|
1762
|
+
var mrScanPending = false;
|
|
1763
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
1764
|
+
var mrObs = new MutationObserver(function () {
|
|
1765
|
+
if (mrScanPending) return;
|
|
1766
|
+
mrScanPending = true;
|
|
1767
|
+
requestAnimationFrame(function () {
|
|
1768
|
+
mrScanPending = false;
|
|
1769
|
+
mrScan();
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
try {
|
|
1773
|
+
mrObs.observe(document.documentElement, {
|
|
1774
|
+
childList: true,
|
|
1775
|
+
subtree: true,
|
|
1776
|
+
attributes: true,
|
|
1777
|
+
attributeFilter: ['disabled', 'readonly', 'data-phase'],
|
|
1778
|
+
});
|
|
1779
|
+
} catch (e) {}
|
|
1780
|
+
}
|
|
1356
1781
|
})();
|
|
1357
1782
|
`
|
|
1358
1783
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feiyang666/dsh-mobile-remote",
|
|
3
|
-
"version": "1.4
|
|
4
|
-
"description": "DeepSeek Harness mobile remote-control plugin: LAN/external remote control (QR / on-off toggle / remote-access password gate), rich telemetry (expandable device details, system CPU/memory/load, DSH app state: version/sessions/workspaces/plugins/models), real external-tunnel status detection (frpc token / reconnect diagnostics),
|
|
3
|
+
"version": "1.5.4",
|
|
4
|
+
"description": "DeepSeek Harness mobile remote-control plugin: LAN/external remote control (QR / on-off toggle / remote-access password gate), rich telemetry (expandable device details, system CPU/memory/load, DSH app state: version/sessions/workspaces/plugins/models), real external-tunnel status detection (frpc token / reconnect diagnostics), mobile-first UI polish (composer image upload entry via system picker, settings single-column layout) and faster remote loading.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
7
7
|
"harness",
|