@xiaopb/dsh-mobile-compose 0.0.0-stage → 0.6.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 +121 -0
- package/LICENSE +21 -0
- package/README.md +164 -2
- package/cordis.patch.yml +7 -0
- package/lib/capabilities.js +109 -0
- package/lib/client.js +1380 -0
- package/lib/config.js +93 -0
- package/lib/index.js +97 -0
- package/lib/settings.js +46 -0
- package/package.json +75 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.4
|
|
4
|
+
|
|
5
|
+
**工程化版本,插件运行时行为无变化。**
|
|
6
|
+
|
|
7
|
+
- **CI(`.github/workflows/ci.yml`)**:push main / PR 时在 Node 22 与 24 上跑 `npm run check`(28 项单测 + 离线冒烟 + 包清单 / patch / bundle 契约自检),再做 `npm pack` + 发布说明生成器校验 + **消费端安装测试**(把打出来的 tarball 装进干净目录,校验 `dsh.bundle.patch`、`dsh.client.platform`、宿主 `apply` 可加载、开发目录未混入包里)。
|
|
8
|
+
- **Release(`.github/workflows/release.yml`)**:打 `v*` tag 或手动触发 → 校验 tag ↔ 版本 → 自检 → 发布 npm(优先 Trusted Publishing / OIDC,失败回退 `NPM_TOKEN`,均带 `--provenance`)→ 校验版本已在 npm 公开 → 打包 tarball + SHA-256 → 用 `scripts/release-notes.sh` 生成发布说明(版本信息 / 安装命令 / 产物校验和 / 按 conventional commit 分类的变更 / 质量校验 / 贡献者 / 对比链接)→ 创建 GitHub Release。
|
|
9
|
+
- **UI verify(`.github/workflows/ui-verify.yml`,可选)**:手动触发或每天定时,用真实 DSH 引擎跑无头验收(面板全流程 / 换页交互 / 指令决策表 / 语言跟随);`dsh_url` 留空时 CI 自己 `npm i -g @deepseek-ai/dsh` 起引擎并把本仓库装进 web profile。
|
|
10
|
+
- **新增 `scripts/verify-package.mjs`**:离线校验清单字段、exports 指向、files 白名单、`cordis.patch.yml` roster 契约、客户端 bundle 的 `__ModuleLoader__` 包装与关键标记、宿主导出、CHANGELOG 版本小节。
|
|
11
|
+
- **新增 `scripts/release-notes.sh`**:发布说明生成器(CI 与 Release 共用同一脚本,生成器坏掉会在 CI 先失败而不是发版时)。
|
|
12
|
+
- **打包元数据**:`publishConfig`(public + 官方 registry)、`repository` / `homepage` / `bugs` / `keywords` / `author`,`files` 增加 `CHANGELOG.md`;`package-lock.json` 入库(`npm ci` 可复现),新增 `.gitattributes`(统一 LF、PNG 视为二进制)。
|
|
13
|
+
- 验收脚本改用 `tools/lib/browser.js` 统一解析 Chromium 路径(Windows 仍用本机 Playwright 缓存,CI/Linux 交给 Playwright 自带浏览器,可用 `DSH_CHROMIUM` 覆盖)。
|
|
14
|
+
|
|
15
|
+
## 0.6.3
|
|
16
|
+
|
|
17
|
+
- **去掉卡片底部提示行**:面板内不再渲染「选中的文件走官方上传通道」等提示文案,卡片高度明显降低;采集指令期间的加载反馈改为分页条位置的 6px 呼吸点(`.dmc-loading`,只占 8px,不增高)。
|
|
18
|
+
- **「+」更好按、展开即「×」**:入口按钮 26px → **32px** 触摸区(图标 14 → 18),圆角 8px;面板展开时按钮带 `.dmc-open`,图标旋转 45° 变成「×」(0.18s 过渡),点它或 Esc 均可收起,`aria-expanded` 同步。
|
|
19
|
+
- **瓦片整体收紧**:瓦片 84px → **72px**(图标盒 52 → 48、行距 14px),卡片内边距 12/10 → 10/8 —— 1280px 视口下整卡高度约 247px → **194px**。
|
|
20
|
+
- 测试:28 项(新增展开态 `dmc-open` / `aria-expanded` 断言、去提示断言);真机截图核对收起/展开两种状态。
|
|
21
|
+
|
|
22
|
+
## 0.6.2
|
|
23
|
+
|
|
24
|
+
- **适配 DSH Android v1.4.27 的系统 Photo Picker 路由(上游 issue #54)**:host 的 `onShowFileChooser` 对「accept 全为图片 MIME」的请求改走系统照片选择器(缩略图网格 / 原生多选 / 免存储权限),判据是 `acceptTypes` 每个条目都以 `image/` 开头——扩展名条目(`.jpg` 等)会使判定失败。
|
|
25
|
+
- `pick-image`(安卓):accept 从「MIME+扩展名混写」收敛为纯 MIME `image/*` → 命中 Photo Picker;旧版 host 与桌面走 `createIntent()` / 原生对话框,`image/*` 过滤同样成立,无行为回归。
|
|
26
|
+
- `take-photo`(安卓):**保留**混写 accept——混入扩展名让 host 全图片判定失败、回落 `createIntent()`,`capture` 属性得以保留(照常拉起相机,而不是被 Photo Picker 抢走)。
|
|
27
|
+
- 桌面端维持原样(MIME+后缀,供原生文件对话框按后缀过滤)。
|
|
28
|
+
- 测试:新增「安卓 host Photo Picker accept 适配」用例(28 项全过)。
|
|
29
|
+
|
|
30
|
+
## 0.6.1
|
|
31
|
+
|
|
32
|
+
- **去掉瓦片第二行**:指令瓦片不再显示「中文主标签 + `/别名` 次行小字」,只保留**一行**当前语言的名字;斜杠别名与描述移入 `title`(悬停可见,不占行)。所有瓦片固定等高(84px),面板内卡片高度完全一致。
|
|
33
|
+
- **文案跟随 DSH 语言**:动作名(图片/文件/拍照/指令)、提示、无障碍标签全部内置 zh/en 双语,语言取官方 locale 服务快照(`active`,兜底 `<html lang>`);`locale/change` 事件触发即时重渲染,切语言无需刷新页面。指令标签本来就走官方 locale(中文环境「目标/压缩/…」,英文环境官方英文名),现在整体一致。
|
|
34
|
+
- 入口/面板 `aria-label` 跟随语言,选择器测试改用稳定的 `button.dmc-entry-btn` / `data-dmc-entry`。
|
|
35
|
+
- 测试:新增「en → 英文单行标签,切回 zh 即时更新」用例(27 项);`tools/verify-lang.js` 真机验证:中文基线(单行、12 瓦片全部 84px)→ 设置页切 English(`Add content` / `Images` / `Files` / `Goal`,html lang=en)→ 切回中文即时恢复,全程零页面错误。
|
|
36
|
+
|
|
37
|
+
## 0.6.0
|
|
38
|
+
|
|
39
|
+
- **统一面板形态**:移除窄窗口 / 触屏设备的「底部滑出面板 + 蒙层」形态,**所有窗口宽度都用 dock 形态**(面板插在输入框正下方、与其等宽,文档流内不遮挡内容)——窄宽度下 card 位置不再切换。
|
|
40
|
+
- 关闭方式统一:Esc / 系统返回键 / 点击面板外(原先窄窗口靠蒙层点击关闭)。
|
|
41
|
+
- 清理:删除 `isSheetPresentation`、`.dmc-mask`、`.dmc-panel`、`dmc-rise` 动画与 `SHEET_Z` 层级常量;相关 effect 依赖收敛为 `[open]`。
|
|
42
|
+
- 测试:窄窗口用例改为断言 dock 形态 + 无蒙层 + 等宽 + 点击面板外关闭;`tools/verify-paging.js` 移动端期望 dock;`tools/diag-browser.js` ⑤ 改为「窄窗口 dock 形态 + 等宽硬断言 + 面板外点击关闭」。
|
|
43
|
+
|
|
44
|
+
## 0.5.3
|
|
45
|
+
|
|
46
|
+
- **换页交互按端分流**:移动端用**左右滑**(横向 ≥40px 且明显大于纵向位移,斜向滚动不误翻页),桌面端用**鼠标滚轮**(向下=下一页、向上=上一页,触控板横向 `deltaX` 同样生效)。取代 0.5.2 的上下滑。
|
|
47
|
+
- 纵向手势交还页面:多页时面板只加 `.dmc-swipe{touch-action:pan-y}`(横向归脚本、纵向仍可滚页面),不再用 `touch-action:none` 抢走纵向滚动。
|
|
48
|
+
- 滚轮实现用**原生非 passive** 监听(React 的 `onWheel` 是 passive,`preventDefault` 无效):累积 40 像素翻一页、翻页后冷却 260ms、`deltaMode` 行/页模式统一折算,并把 `wheel` 事件按 ref 回调绑到面板节点(卸载即解绑)。
|
|
49
|
+
- 测试:单测改为「纵向滑动不翻页 / 右滑 / 左滑 / 滚轮上下 / 冷却期不连翻 / 滚轮阻止页面滚动」;`tools/verify-paging.js` 用真机触摸(CDP)+ `mouse.wheel` 在移动 sheet(390×844)与桌面 dock(1280×900)验证,页面零滚动。
|
|
50
|
+
|
|
51
|
+
## 0.5.2
|
|
52
|
+
|
|
53
|
+
- **上滑手势翻页**(0.5.3 起改为「桌面滚轮 / 移动端左右滑」):在面板上**上滑 = 下一页、下滑 = 上一页**(纵向阈值 36px),与原有左右滑(40px)并存;按主导轴判定方向,斜向滑动不会误翻。
|
|
54
|
+
- 多页时面板加 `.dmc-swipe{touch-action:none}`,把纵向手势从浏览器页面滚动中接管过来——否则一次上滑会先滚动页面再翻页;单页时不加该类,不影响页面滚动。
|
|
55
|
+
- 手势翻页后的 350ms 内忽略由同一次触摸派生的 click(部分 WebView 会在 `touch-action:none` 下继续派发 click),避免「翻页顺手执行了一条指令」。
|
|
56
|
+
- 测试:单测新增上滑/下滑/阈值内不翻页三条断言;真机(CDP 真实触摸)在移动 sheet 形态(390×844)与桌面 dock 形态(1280×900)验证上滑、下滑、左滑全部生效且页面零滚动。
|
|
57
|
+
|
|
58
|
+
## 0.5.1
|
|
59
|
+
|
|
60
|
+
- **面板宽度与输入框严格同步**:宽度改以官方输入卡片 `[data-composer-card]` 的实测外框为准(此前按「向上找够宽的祖先」估算,宽窗口下量到的是含 side-clearance 的容器,面板会比输入框宽)。
|
|
61
|
+
- **独占一行的实现换成 slot 容器**:外层 `.dmc-dock-slot`(`flex-basis:100%`)负责强制换行,内层卡片用实测像素宽度 —— 不再用 `flex-basis:100%` 撑满,避免容器比卡片宽时面板被撑大。
|
|
62
|
+
- **窗口缩放 / 侧栏开合 / 换会话实时跟随**:`resize` + `ResizeObserver(card)` 触发重算(此前只在打开瞬间算一次)。
|
|
63
|
+
- 修掉一个自锁陷阱:不再用 dock 容器宽度做上限(容器宽度由面板自身内容撑开,会导致放大窗口后面板停在旧宽度)。
|
|
64
|
+
- 验收脚本 `tools/diag-browser.js` 增加「面板与输入卡片等宽(±1px)」硬断言;多视口(1920/1440/1280/1024/820)实测 Δwidth ≤ 0.4px、左边缘对齐。
|
|
65
|
+
|
|
66
|
+
## 0.5.0
|
|
67
|
+
|
|
68
|
+
- **指令中文标签修复**:RPC `commands.list` 只返回英文名,主标签改由**官方 locale**(`command` namespace:目标/计划/反馈/压缩/权限/下载日志)+ 原生菜单采集兜底;瓦片 = 中文主标签 + `/别名` 次行小字。
|
|
69
|
+
- **指令执行对齐原生流程(决策表)**:带 `input` 的指令(如 /goal)→ 只把本地化 token(`/目标 `)写入草稿,**不代发**,用户补参数后自行发送;裸指令(如 /compact)→ `setDraft('/x')+submit()` 官方 Enter 通道分离执行(与官方菜单点选行为一致);目录来源缺失时一律按 claim 处理。
|
|
70
|
+
- **图片/文件统一为 file 的两个视图**:图片 = file + 显式图片后缀 accept;文件 = file、accept 留空(全部后缀)。自带 input 点击打开选择器,选择期间面板保持挂载(避免部分浏览器取消对话框),onChange 后统一回流官方上传通道。
|
|
71
|
+
- **默认隐藏 `/file` 指令**:它是官方客户端注册的「添加文件」动作,与面板的 图片/文件 瓦片完全重复;新增设置页 `hiddenCommands`(逗号分隔指令名,清空 = 全部展示)。
|
|
72
|
+
- **dock 面板独占一行**:官方 dock 容器开启期间允许换行,面板 `flex-basis:100%` + `order:-1` —— 会话进度等其他插件条目换行到最后一行,不再与面板挤兑。
|
|
73
|
+
- 测试:26 项单测(新增指令执行决策表 + hiddenCommands 用例)。
|
|
74
|
+
|
|
75
|
+
## 0.4.0
|
|
76
|
+
|
|
77
|
+
- **指令与技能全量接入官方 RPC**(不再依赖原生菜单能显示多少条):
|
|
78
|
+
- 指令:`remote.commands.list(sessionId)`(包在 `sessions.using(...)` 会话保留上下文内,与官方 ui-commands 一致);
|
|
79
|
+
- 技能:`remote.skills.list({sessionId})`(同官方 ui-skill)。
|
|
80
|
+
- 原生菜单 DOM 采集降级为**兜底 + 中文标签补全 + 官方顺序**来源;实测面板拿到 8 条指令 + 7 个技能 = 15 条,与官方 `/` 菜单完全一致。
|
|
81
|
+
- **去掉「常用 / 指令」分页语义**:单一列表,常用动作在最前,指令/技能紧随其后;超出单页容量自动生成页码点(可点、可左右滑),不再用 tab 标签区分类型。
|
|
82
|
+
- **每条指令专属图标**:文件 / 目标 / 计划 / 反馈 / 压缩 / 权限 / 模型 / 下载日志 各自独立 SVG,未知指令回退通用图标;技能使用独立图标。
|
|
83
|
+
- **技能点选只插入草稿**:写入 `/技能名 ` 到聊天框(不提交),与官方 `/` 菜单行为一致;指令仍走 `setDraft('/x')` + `submit()` 裁决通道执行。
|
|
84
|
+
- 打开面板即预取(RPC 就绪后自动重拉),`commands/change`、`plugin-manager/changed` 事件自动失效缓存。
|
|
85
|
+
- 测试:22 项单测(新增 RPC 全量 + 自动分页用例)+ smoke + 无头 Chromium 真机验收全绿。
|
|
86
|
+
|
|
87
|
+
## 0.3.1
|
|
88
|
+
|
|
89
|
+
- **修复「+」点击无反应(hero 态)**:官方 `conversation.composer.dock` 插槽只在 `variant==="composer"` 时渲染,新建/空会话(hero 态)下面板组件根本不挂载。面板改为由「+」入口组件内渲染并 portal 到官方 `[data-composer-dock]` 容器(hero / composer 两态都存在),不再依赖 dock 插槽。
|
|
90
|
+
- **修复点击后「+」消失(钩子链表损坏)**:render 期 `try { props.useInput() } catch {}` —— `useInput` 是 hook,中途抛错被吞后 React 钩子链表损坏,第二次渲染 `areHookInputsEqual` 读到 undefined 崩溃,官方错误边界卸载整个入口。移除 render 期 hook 调用。
|
|
91
|
+
- **修复指令采集**:适配 dsh 0.2.1 菜单结构(`[role=listbox]` 取含 option 的那个;option = svg/中文标签/别名/描述 四段 span);轮询等浮层渲染(至多 1.6s);收尾顺序修正——原生菜单关闭完成后才复位 `harvesting`,避免自身派发的 Escape 关掉面板。
|
|
92
|
+
- **面板布局重做**:面板与聊天框**等宽**(从「+」按钮向上测宽),**固定 2 行**,列数按宽度自动计算(88px/列,2-6 列);指令渲染为**同款选项瓦片**(不再竖排列表),放不下时**自动生成「指令·2」「指令·3」…多个 tab**,支持左右滑动切换。
|
|
93
|
+
- 修复图片/文件瓦片无图标(动作 id 与图标 key 映射错位)。
|
|
94
|
+
- 打开面板即自动预取指令表(无需先切到指令页)。
|
|
95
|
+
- 验收:无头 Chromium 真机全流程通过(入口 → 面板 → 指令采集 8 条 → Esc/外部点击关闭 → 底部面板形态),21 项单测 + smoke 全绿。
|
|
96
|
+
|
|
97
|
+
## 0.3.0
|
|
98
|
+
|
|
99
|
+
- **「+」原位替换**:入口经 flex `order:-1` 站到工具行最左,占据原生「添加文件」的原位置(不再是行尾后增)。
|
|
100
|
+
- **面板 dock 化**:桌面端动作面板改挂在官方 `conversation.composer.dock` 插槽 —— 对话框卡片**正下方的文档流内**,不再浮动遮挡;手机宽度保持底部滑出面板(蒙层 + 返回键关闭)。
|
|
101
|
+
- **指令接管**:面板新增「指令」tab —— 首次打开时从原生命令菜单**隐蔽采集**指令表(名称 + 描述,不弹原生列表),执行走官方裁决通道(`setDraft('/x')` + `submit()`);采集失败自动回退原生命令菜单点选。输入机忙时禁点。
|
|
102
|
+
- **多 tab + 左右滑**:常用 / 指令两个 tab,支持触摸横向滑动切换;宫格列数随面板实际宽度自适应(`auto-fill`,默认约 4 列起步)。
|
|
103
|
+
- 客户端 jsdom 行为测试扩到覆盖双插槽注册、tab 切换、指令采集/执行、底部面板形态、卸载清理(共 21 项单测)。
|
|
104
|
+
- **真机验证**:dsh 0.2.1-rc.4 引擎 —— capabilities 路由在线、页面 roster 与 bundle 服务正常、引擎无报错。
|
|
105
|
+
|
|
106
|
+
## 0.2.0
|
|
107
|
+
|
|
108
|
+
- **DSH 设置页配置**:注册 `dsh-mobile-compose` namespace(schemastery schema,设置页自动渲染表单)——`hideNativeButtons` 与 4 个动作开关;改动实时生效(客户端每次打开面板重拉 capabilities),无需刷新页面。
|
|
109
|
+
- capabilities 路由改为读取设置页实时来源(`installSection` hooks + thunk)。
|
|
110
|
+
- 新增 `docs/android-bridge.md`:插件 ↔ DSH Android 端对接契约(职责划分、JS 桥协议、返回数据格式、错误码、安全要求)。
|
|
111
|
+
- 桌面端图片动作对接原始文件打开器:`accept` 改为显式图片后缀(`image/png,image/jpeg,image/webp,image/gif,.png,.jpg,.jpeg,.webp,.gif`)。
|
|
112
|
+
- 修复 `setCaps` 对 Set 误用 `splice` 导致能力协商静默失效的 bug(jsdom 实测暴露)。
|
|
113
|
+
- devDependencies 引入 react / react-dom / jsdom,客户端 bundle 可离线实测。
|
|
114
|
+
|
|
115
|
+
## 0.1.0
|
|
116
|
+
|
|
117
|
+
- 首版:「+」入口(`conversation.input.left` 插槽)+ QQ 式动作面板(图片 / 拍照 / 文件 / 指令)。
|
|
118
|
+
- 文件来源细分(gallery / camera / document / internal),为安卓专门路由留缝。
|
|
119
|
+
- 文件回流官方通道:DataTransfer 注入官方隐藏 `input[type=file]` + change 派发。
|
|
120
|
+
- 宿主端 capabilities 路由(`/api/dsh-mobile-compose/capabilities`)。
|
|
121
|
+
- 原生「添加文件 / 调用指令」按钮作用域 CSS 隐藏(`hideNativeButtons` 可关)。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XiaoPb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-mobile-compose
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/XiaoPb/dsh-mobile-compose/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/XiaoPb/dsh-mobile-compose/actions/workflows/release.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@xiaopb/dsh-mobile-compose)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
DSH 网页界面插件:**手机端输入体验** —— 把输入框左下角的「添加文件」「调用指令」收编为一个「+」入口,点击弹出 **QQ 式动作面板**(宫格瓦片,插在输入框正下方、与其等宽;所有窗口宽度表现一致;返回键 / Esc / 点击面板外关闭)。展开时「+」旋转为「×」,再点或 Esc 关闭。面板动作按**文件来源细分**(相册 / 拍照 / 文档 / 指令),为安卓专门路由预留标准缝;选中的文件全部回流**官方上传通道**(校验 / 上传 / 附件预览零重复)。
|
|
9
|
+
|
|
10
|
+
当前版本 0.6.x:**指令/技能全量接入官方 RPC**(单列表自动分页、每条指令专属图标、**文案跟随 DSH 语言**、单行等高瓦片)、**执行决策表对齐原生菜单**(带参数的指令只写草稿不代发;裸指令走官方 Enter 通道分离执行)、**面板 dock 化且独占一行**(不与进度等条目挤兑)、**DSH 设置页配置项**(动作开关 / 隐藏原生按钮 / 隐藏指令,改动实时生效)与**安卓端对接契约**([docs/android-bridge.md](docs/android-bridge.md))。
|
|
11
|
+
|
|
12
|
+
## 效果预览
|
|
13
|
+
|
|
14
|
+
**移动端(MuMu 模拟器,390×844 逻辑宽)** —— 面板插在输入框正下方、与其等宽,展开时「+」已旋转为「×」;瓦片等高 72px、底部无提示行,条目超出一页时显示页码点(左右滑 / 滚轮换页):
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
**桌面端(1280 宽)** —— 同一形态:面板仍插在输入框正下方、与其等宽(712px),不遮挡任何内容;常用动作(图片/文件)在最前,指令与技能紧随其后:
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
## 职责划分:插件做什么,外部做什么
|
|
23
|
+
|
|
24
|
+
| 职责 | 归属 | 说明 |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| 输入框「+」入口、动作面板 UI、动画、返回键关闭 | **插件** | 全部走官方插槽(`conversation.input.left` + `conversation.composer.dock`),零 DOM 注入 |
|
|
27
|
+
| 动作注册表与来源标记(gallery/camera/document/internal) | **插件** | 面板宫格的单一事实来源 |
|
|
28
|
+
| 指令接管 | **插件** | 指令/技能目录来自官方 RPC(`remote.commands.list` / `remote.skills.list`),标签来自官方 locale + 原生菜单采集兜底(跟随 DSH 语言);执行按**官方 dispatch 决策表**:带 `input` 的指令只把 `/token ` 写进草稿(用户补参数后自行发送),裸指令走 `setDraft('/x')+submit()` 官方 Enter 通道 |
|
|
29
|
+
| 桌面端文件选取 | **插件** | 图片 / 文件都是同一个 file 输入的两个视图:图片附**显式图片后缀过滤**(`image/png,image/jpeg,image/webp,image/gif,.png,.jpg,.jpeg,.webp,.gif`);文件**不限后缀**(= 原始 file,处理全部类型) |
|
|
30
|
+
| 文件回流与上传 | **插件** | DataTransfer 注入官方隐藏 input → 官方 onPickFiles → addFiles 校验/上传/预览 |
|
|
31
|
+
| 动作能力协商 | **插件(宿主)** | `GET /api/dsh-mobile-compose/capabilities`,实时反映设置页配置 |
|
|
32
|
+
| 面板配置 UI | **插件(宿主)** | DSH 设置页 `dsh-mobile-compose` namespace,schema-form 自动渲染 |
|
|
33
|
+
| 错误 toast、加载态 | **插件** | 安卓端只返回结构化结果,不画 UI |
|
|
34
|
+
| 原生相机 / 相册 Intent、运行时权限、图片压缩 | **安卓端(外部)** | 按 [docs/android-bridge.md](docs/android-bridge.md) 注入 `window.DshComposeBridge` |
|
|
35
|
+
| 选取结果的回传 | **安卓端(外部)** | `{status, code, files:[{name, mime, size, width?, height?, data(base64)}]}`,契约见文档 §3 |
|
|
36
|
+
|
|
37
|
+
安卓端未实现(或 `routed:"web"`)时,一切照旧走浏览器方案——**安卓接入是渐进增强,不是门槛**。
|
|
38
|
+
|
|
39
|
+
## 安装
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# 从 npm 安装(推荐)
|
|
43
|
+
dsh plugin --profile web add @xiaopb/dsh-mobile-compose
|
|
44
|
+
|
|
45
|
+
# 本地路径安装(开发验证)
|
|
46
|
+
dsh plugin --profile web add /path/to/dsh-mobile-compose
|
|
47
|
+
|
|
48
|
+
# 固定到某个 Release 的 tarball(离线)
|
|
49
|
+
dsh plugin --profile web add /path/to/xiaopb-dsh-mobile-compose-0.6.4.tgz
|
|
50
|
+
|
|
51
|
+
# 之后重启引擎(bundle 插件不热加载)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
装好后打开桌面端 web GUI:输入框左下角出现「+」按钮;原生「添加文件(回形针)」「调用指令」按钮默认隐藏(可在设置页关闭)。
|
|
55
|
+
|
|
56
|
+
## 桌面端验证清单
|
|
57
|
+
|
|
58
|
+
1. 宽窗口点击「+」→ **对话框卡片正下方**展开 dock 面板(文档流内,不遮挡任何内容);「+」位于工具行**最左**(原「添加文件」的位置);
|
|
59
|
+
2. 单一列表:常用动作在最前,指令/技能紧随其后;宫格列数随面板宽度自适应,放不下自动生成页码点(可点;**移动端左右滑**、**桌面端鼠标滚轮**换页);
|
|
60
|
+
3. 「图片」→ 原生文件打开器弹出且**默认过滤到图片类型**(显式后缀 accept);选 1-2 张 → 附件预览条出现、走官方上传(进度 / receipt);
|
|
61
|
+
4. 「文件」→ 原生文件打开器(不限后缀,即原始 file);
|
|
62
|
+
5. 指令瓦片与动作同列:RPC 全量目录 + **单行标签**(中文或英文,跟随 DSH 语言;所有瓦片等高 72px,卡片底部无提示行,斜杠别名只在悬停 title 里);点带参数的指令(如 /goal)→ 只插入 `/目标 ` 到聊天框(**不发送**,与原生菜单一致);点裸指令(如 /compact)→ 官方 Enter 通道分离执行;
|
|
63
|
+
6. 把窗口缩到手机宽度(或 DevTools 设备模拟)→ 面板形态**不变**(仍插在输入框下方、与其等宽),Esc / 系统返回键 / 点击面板外关闭;
|
|
64
|
+
7. 设置页 → Plugins → dsh-mobile-compose:关掉任一动作 → 不重载页面,再点「+」该瓦片已消失。
|
|
65
|
+
|
|
66
|
+
## 架构
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
web GUI ──▶ __ModuleLoader__ 装载 lib/client.js(closure factory,形状对齐 dsh-hardssh 客户端产物)
|
|
70
|
+
├─ ctx.slots 注册 conversation.input.left ←「+」入口(flex order 最左 = 原生两键原位置)
|
|
71
|
+
├─ ctx.slots 注册 conversation.composer.dock ← 动作面板(对话框下方文档流内;
|
|
72
|
+
│ 所有窗口宽度表现一致;关闭 = Esc / 系统返回键 / 点击面板外)
|
|
73
|
+
├─ 面板 = 单一列表自动分页(页码点 + 移动端左右滑 / 桌面滚轮);宫格列数随宽度自适应
|
|
74
|
+
├─ 动作注册表:pick-image(相册) / take-photo(拍照) / pick-file(文档) / command(指令)
|
|
75
|
+
│ 每项带 source 标记(gallery/camera/document/internal)——安卓专门路由的缝
|
|
76
|
+
├─ 指令接管:RPC 全量目录为权威;原生菜单采集(visibility:hidden)仅补标签/顺序;
|
|
77
|
+
│ 执行 = 官方 dispatch 决策表(input→claim 写草稿不代发;裸指令→setDraft+submit)
|
|
78
|
+
│ (官方裁决通道);采集失败回退原生菜单点选
|
|
79
|
+
├─ 桌面:图片动作 = 自带 input + 显式图片后缀 accept(原始文件打开器按类型过滤);
|
|
80
|
+
│ 文件动作 = 自带 input、accept 留空(全部后缀,即原始 file)
|
|
81
|
+
├─ 文件回流:DataTransfer 注入官方隐藏 input[type=file] → 派发 change
|
|
82
|
+
│ → 官方 onPickFiles → addFiles 校验/上传/预览全链路原样生效
|
|
83
|
+
└─ 能力协商:每次打开面板拉取 GET /api/dsh-mobile-compose/capabilities(失败走内置默认集)
|
|
84
|
+
|
|
85
|
+
宿主端(lib/index.js,cordis 插件)
|
|
86
|
+
├─ GET /api/dsh-mobile-compose/capabilities(动态 inject webServer,实时读设置页来源)
|
|
87
|
+
└─ dsh-mobile-compose 设置页 namespace(ctx.settings.installSection + schemastery schema,
|
|
88
|
+
DSH 设置页 schema-form 自动渲染;settings 服务缺席时静默降级为插件装载配置)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**原生按钮隐藏原理**(零 DOM 改动,插件样式卸载即恢复):
|
|
92
|
+
|
|
93
|
+
```css
|
|
94
|
+
/* 调用指令按钮:aria-haspopup="listbox" 是它在工具行里的稳定标识 */
|
|
95
|
+
body.dmc-hide-native button[aria-haspopup="listbox"]{display:none!important}
|
|
96
|
+
/* 添加文件按钮:紧跟官方隐藏 input[type=file][multiple][hidden] 的那个按钮 */
|
|
97
|
+
body.dmc-hide-native button:has(+ input[type="file"][multiple][hidden]:not([data-dmc-input])){display:none!important}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 安卓专门路由(外部工作,契约见 docs/android-bridge.md)
|
|
101
|
+
|
|
102
|
+
- 插件已备好**接缝**:`capabilities` 响应的 `routed` 字段 + `window.DshComposeBridge` 桥协议 + 结构化返回/错误码 + 失败自动回退 web 路径;
|
|
103
|
+
- 安卓端需要完成:WebView 注入桥、take-photo/pick-image 路由到原生 Intent、运行时权限、图片压缩、base64 回传(全部要求在 [docs/android-bridge.md](docs/android-bridge.md) §2–§5);
|
|
104
|
+
- 安卓运行时探测与 dsh-hardssh 同约定(`ANDROID_DATA` / `DSH_ANDROID_USER_DATA` / `/system/build.prop`),capabilities 响应里的 `platform.android` 已就绪——客户端用它决定「拍照」瓦片只在安卓展示。
|
|
105
|
+
|
|
106
|
+
## 配置
|
|
107
|
+
|
|
108
|
+
**推荐入口:DSH 设置页**(实时生效,落盘持久化)。插件装载配置仅作初始默认。
|
|
109
|
+
|
|
110
|
+
| 键 | 默认 | 说明 |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `enabled`(仅装载配置) | `true` | `false` 时装载但零路由,客户端走内置默认动作集 |
|
|
113
|
+
| `hideNativeButtons` | `true` | 隐藏原生「添加文件 / 调用指令」小按钮;`false` 时两套入口并存 |
|
|
114
|
+
| `actions.pick-image` | `true` | 相册动作开关 |
|
|
115
|
+
| `actions.take-photo` | `true` | 拍照动作开关(仅安卓运行时展示) |
|
|
116
|
+
| `actions.pick-file` | `true` | 文件动作开关 |
|
|
117
|
+
| `actions.command` | `true` | 指令动作开关 |
|
|
118
|
+
| `hiddenCommands` | `file` | 不展示的指令名(逗号分隔,不带斜杠)。`/file` 是官方「添加文件」指令,与图片/文件瓦片重复,默认剔除;清空 = 全部展示 |
|
|
119
|
+
|
|
120
|
+
## CI/CD
|
|
121
|
+
|
|
122
|
+
| 工作流 | 触发 | 做什么 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| [`ci.yml`](.github/workflows/ci.yml) | push `main` / PR / 手动 | Node 22 与 24 矩阵:`npm ci` → `npm run check`(28 项单测 + 离线冒烟 + 包契约自检)→ `npm pack --dry-run` → 打 tarball → 校验发布说明生成器 → **消费端安装测试**(干净目录安装 tarball,校验 `dsh.bundle.patch` / `dsh.client.platform` / 宿主 `apply` 可加载 / 开发目录未混入包) |
|
|
125
|
+
| [`release.yml`](.github/workflows/release.yml) | push `v*` tag / 手动 | 校验 tag ↔ `package.json` 版本 → 自检 → 发布 npm(优先 **Trusted Publishing**/OIDC,失败回退 `NPM_TOKEN`,均带 `--provenance`)→ 确认版本已在 npm 公开 → `npm pack` + SHA-256 → 生成发布说明(`scripts/release-notes.sh`)→ 创建 GitHub Release(附 tarball 与校验和) |
|
|
126
|
+
| [`ui-verify.yml`](.github/workflows/ui-verify.yml) | 手动 / 每天 04:00(UTC+8) | 真机验收:用真实 DSH 引擎跑 `diag-browser`(面板全流程)、`verify-paging`(左右滑 / 滚轮)、`verify-decision`(`/goal` 只写草稿)、`verify-lang`(zh ↔ en)。`dsh_url` 留空时 CI 自建引擎并把本仓库装进 web profile;也可传入局域网引擎地址 |
|
|
127
|
+
|
|
128
|
+
发布一个版本:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npm version patch # 或 minor / major,自动改 package.json 并打 v* tag
|
|
132
|
+
git push origin main --follow-tags
|
|
133
|
+
# → Release 工作流接管:npm 发布 + GitHub Release(产物与 SHA-256)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**npm 发布凭据**:默认走 Trusted Publishing(在 npm 包设置里登记本仓库 + `release.yml`,允许 `id-token: write`,无需 secret);登记前的兜底方式是仓库 Secret `NPM_TOKEN`。
|
|
137
|
+
**本机跑真机验收**:`DSH_URL="http://127.0.0.1:3080/?token=..." node tools/diag-browser.js`(其余脚本同理,见 `npm run verify:paging` / `verify:lang`)。
|
|
138
|
+
|
|
139
|
+
## 验证
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
npm run check # 一条命令跑完下面三项(CI 与 Release 用的同一入口)
|
|
143
|
+
npm test # 单测(node:test,28 项:config / capabilities / 宿主接线含设置页 / jsdom 客户端行为)
|
|
144
|
+
npm run smoke # E2E 冒烟:真实 HTTP 往返 + 客户端产物形状 + patch 形状
|
|
145
|
+
node scripts/verify-package.mjs # 包清单 / cordis patch / bundle 契约 / 宿主导出 / CHANGELOG 版本
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
客户端 bundle 在 **jsdom 里真实执行**(加载契约 → apply 接线 → 渲染 → tab 切换 → 指令采集与官方通道执行 → 窄窗口统一 dock 形态 → 卸载清理),不依赖人工点检。
|
|
149
|
+
|
|
150
|
+
真实引擎验证(dsh 0.2.1-rc.4 已做过一轮)——
|
|
151
|
+
|
|
152
|
+
- `GET /api/dsh-mobile-compose/capabilities` 返回正确 JSON(`platform.android:false`);
|
|
153
|
+
- 页面 roster 引用 `/plugins/@xiaopb/dsh-mobile-compose/client.js` 且内容完整(含全部新特性标记);
|
|
154
|
+
- 引擎日志无本插件相关报错。
|
|
155
|
+
|
|
156
|
+
## 已知风险与验证点
|
|
157
|
+
|
|
158
|
+
- **DOM 标识漂移**:隐藏规则与文件回流依赖官方输入框的 DOM 结构(`aria-haspopup="listbox"`、隐藏 file input 的相邻关系)。DSH 升级若改变结构,表现为「原生按钮复现 / 图片动作失效」,console 会有 `[dsh-mobile-compose] 未找到…` 警告——更新选择器即可;设置页 `hideNativeButtons=false` 可临时回退到两套入口并存。
|
|
159
|
+
- **React change 去重**:文件注入已置空 `_valueTracker` 规避同值去重(jsdom 测试覆盖注入派发路径)。
|
|
160
|
+
- **`:has()` 选择器**:需要 Chrome/WebView 105+;旧 WebView 上表现为原生按钮保留(功能不受影响)。
|
|
161
|
+
- 「调用指令」走程序化点击官方按钮(其菜单由官方包内部件管理,不从外部重建);若点击无响应,设置页回退方案同上。
|
|
162
|
+
|
|
163
|
+
## 许可
|
|
164
|
+
|
|
165
|
+
MIT。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @xiaopb/dsh-mobile-compose bundle patch: 插入 web profile roster 行,使本插件
|
|
2
|
+
# 的客户端 bundle(lib/client.js,__ModuleLoader__ closure factory)随 web GUI
|
|
3
|
+
# 加载。UI 全部通过官方插槽(conversation.input.left)与官方上传通道工作,
|
|
4
|
+
# 不禁用、不替换任何官方服务。
|
|
5
|
+
- insert:
|
|
6
|
+
- id: mobile-compose
|
|
7
|
+
name: '@xiaopb/dsh-mobile-compose'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* capabilities 路由族:GET /api/dsh-mobile-compose/capabilities
|
|
5
|
+
*
|
|
6
|
+
* 客户端装载时拉取一次,按响应决定:
|
|
7
|
+
* - 动作宫格里渲染哪些项(actions[].enabled);
|
|
8
|
+
* - 是否隐藏原生「添加文件 / 调用指令」按钮(client.hideNativeButtons)。
|
|
9
|
+
*
|
|
10
|
+
* 拉取失败(未启用 / headless profile / 旧版本引擎)时客户端走内置默认集,
|
|
11
|
+
* 所以本路由是**能力协商的增强路径,不是功能门槛**。
|
|
12
|
+
*
|
|
13
|
+
* platform.android:Node 在 Android App 内运行时仍报 linux,由 ANDROID_DATA /
|
|
14
|
+
* DSH_ANDROID_USER_DATA 环境变量(以及 /system/build.prop 兜底探测)显式区分
|
|
15
|
+
* ——与 dsh-hardssh 的 platform.ts 同一约定。安卓端将来把 take-photo 等动作
|
|
16
|
+
* 路由到原生 Intent 时,就是在这个响应里把 routed:'native' 声明出来,客户端
|
|
17
|
+
* 据此改走 App 桥而不是浏览器 input[capture]。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const http = require('node:http')
|
|
21
|
+
|
|
22
|
+
/** Android 运行时探测(与 dsh-hardssh/src/platform.ts 同语义)。 */
|
|
23
|
+
function isAndroidRuntime() {
|
|
24
|
+
if (process.env.ANDROID_DATA !== undefined) return true
|
|
25
|
+
if (process.env.DSH_ANDROID_USER_DATA !== undefined) return true
|
|
26
|
+
try {
|
|
27
|
+
return require('node:fs').existsSync('/system/build.prop')
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 动作注册表(宿主侧单一事实来源;客户端另有一份形状相同的内置默认)。 */
|
|
34
|
+
const ACTION_DEFS = [
|
|
35
|
+
{ id: 'pick-image', source: 'gallery', label: '图片' },
|
|
36
|
+
{ id: 'take-photo', source: 'camera', label: '拍照' },
|
|
37
|
+
{ id: 'pick-file', source: 'document', label: '文件' },
|
|
38
|
+
{ id: 'command', source: 'internal', label: '指令' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function buildCapabilitiesPayload(config) {
|
|
42
|
+
const cfg = config && typeof config === 'object' ? config : {}
|
|
43
|
+
const actions = typeof cfg.actions === 'object' && cfg.actions !== null ? cfg.actions : {}
|
|
44
|
+
const hidden = Array.isArray(cfg.hiddenCommands) ? cfg.hiddenCommands : []
|
|
45
|
+
const android = isAndroidRuntime()
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
plugin: 'dsh-mobile-compose',
|
|
49
|
+
platform: { android, web: true },
|
|
50
|
+
client: { hideNativeButtons: cfg.hideNativeButtons !== false },
|
|
51
|
+
// 与内置动作同义、默认不展示的指令(客户端可再叠加自己的默认集)。
|
|
52
|
+
commands: { hidden },
|
|
53
|
+
actions: ACTION_DEFS.map((def) => ({
|
|
54
|
+
...def,
|
|
55
|
+
enabled: actions[def.id] !== false,
|
|
56
|
+
// 安卓原生路由(相机 / 相册 Intent)尚未接入:桌面与安卓当前都走
|
|
57
|
+
// 浏览器 input[accept][capture] 方案,routed 一律 'web'。
|
|
58
|
+
routed: 'web',
|
|
59
|
+
})),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sendJson(res, status, obj) {
|
|
64
|
+
try {
|
|
65
|
+
const body = JSON.stringify(obj)
|
|
66
|
+
res.writeHead(status, {
|
|
67
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
68
|
+
'Content-Length': Buffer.byteLength(body),
|
|
69
|
+
'Cache-Control': 'no-store',
|
|
70
|
+
})
|
|
71
|
+
res.end(body)
|
|
72
|
+
} catch (_) {
|
|
73
|
+
try {
|
|
74
|
+
res.destroy()
|
|
75
|
+
} catch (_2) {
|
|
76
|
+
/* 静默降级 */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 构造 capabilities 路由(WebRoute 形状:{kind, path, handler})。
|
|
83
|
+
*
|
|
84
|
+
* @param configOrSource - 归一化配置对象,或返回该对象的 thunk(设置页实时
|
|
85
|
+
* 来源:用户在 DSH 设置页改动后,下一次请求即拿到新值,无需重载插件)。
|
|
86
|
+
*/
|
|
87
|
+
function createCapabilitiesRoute(configOrSource) {
|
|
88
|
+
const getConfig = typeof configOrSource === 'function' ? configOrSource : () => configOrSource
|
|
89
|
+
return {
|
|
90
|
+
kind: 'exact',
|
|
91
|
+
path: '/api/dsh-mobile-compose/capabilities',
|
|
92
|
+
handler: function handler(_req, res) {
|
|
93
|
+
let cfg
|
|
94
|
+
try {
|
|
95
|
+
cfg = getConfig()
|
|
96
|
+
} catch (_) {
|
|
97
|
+
cfg = {}
|
|
98
|
+
}
|
|
99
|
+
sendJson(res, 200, buildCapabilitiesPayload(cfg))
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 独立可用:node:test 与 smoke 直接调用 handler(伪 req/res)。 */
|
|
105
|
+
function createCapabilitiesHandler(configOrSource) {
|
|
106
|
+
return createCapabilitiesRoute(configOrSource).handler
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { createCapabilitiesRoute, createCapabilitiesHandler, buildCapabilitiesPayload, isAndroidRuntime, ACTION_DEFS, sendJson }
|