@yfwu2020/dsh-selection-explain 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,154 +1,183 @@
1
- # dsh-selection-explain · 划词解读
1
+ <div align="center">
2
2
 
3
- > 在 DSH 里**选中任意文字**,就地得到**专业翻译** + **它在当前上下文里到底是什么意思**;还能接着追问、把别处的文字带进来问、或者一键升格成正式会话。
3
+ # 划词解读
4
4
 
5
- [![release](https://img.shields.io/github/v/release/yfwu2020/dsh-selection-explain?label=release)](https://github.com/yfwu2020/dsh-selection-explain/releases)
6
- [![npm](https://img.shields.io/npm/v/@yfwu2020/dsh-selection-explain?label=npm)](https://www.npmjs.com/package/@yfwu2020/dsh-selection-explain)
7
- [![license](https://img.shields.io/badge/license-MIT-green)](./package.json)
8
- [![DSH plugin](https://img.shields.io/badge/DSH-plugin-8b5cf6)](https://github.com/yfwu2020/dsh-selection-explain)
5
+ **划一下,说一句。把不懂的地方,就地问明白。**
6
+
7
+ DSH 中的翻译、上下文解读与语音追问。
8
+
9
+ [快速安装](#install) · [语音输入](#voice) · [功能一览](#read) · [使用与设置](#settings)
10
+
11
+ `DSH 插件` · `v0.10.1` · `MIT`
12
+
13
+ </div>
9
14
 
10
15
  ---
11
16
 
12
- ## 语音:划完直接说,不用打字
17
+ <a id="voice"></a>
13
18
 
14
- 追问往往只有一两句("这词在这里是不是贬义?"),可打字这件事本身就要把手从鼠标挪到键盘——和"划词"是矛盾的。所以划完可以直接说:
19
+ ## 01 / 划完,直接说
15
20
 
16
- | 你现在的状态 | 按语音键说话 → 结果 |
17
- | --- | --- |
18
- | **小窗没开**,刚划了一段文字 | 选中的地方**就地弹出一张临时卡片**,说的话直接落进卡片;松手定稿后**自动进小窗**追问(跳过翻译与详解) |
19
- | **小窗开着**,刚划了一段文字 | 等于**替你点了一下 `❝ 引用`**:那段文字挂进小窗、光标进输入框,接着说的话直接落在小窗里,和下一条提问一起发出去 |
20
- | 想直接对输入框说话 | 点输入框右边的 🎤 —— 和主会话**同一套**语音输入:实时波形、边说边出字、`■` 结束、`Esc` 取消(一次最长 60 秒) |
21
+ 选中一句话,直接用语音问“它在这里是什么意思?”——问题留在选区旁,回答接着出现在小窗里。
21
22
 
22
- <img src="assets/voice.png" alt="录音中的小窗:工具行是「✕ 取消 · 实时波形 · ■ 停止」,输入框里已经跟着长出了两行文字" width="540">
23
+ ### 从划词到提问,一口气完成
23
24
 
24
- **侧边栏的网页里也能这么用**(新)。侧边栏预览的 HTML 页面跑在沙箱 iframe 里,父页面读不到它的选区;插件往被预览的文档里注入一小段"桥"脚本,由**帧内**读取选中的文字、把它前后的上下文(各 1500 字)和坐标报出来,再用主界面同一张卡片 / 同一个浮标呈现。**安全边界没有退让**:帧始终是不透明源,不给 `allow-same-origin`——网页读不到 GUI 的 DOM / Cookie / localStorage,桥也只往外报"选中了什么 + 周围那点文本"。
25
+ **小窗还没打开**:划选文字后启动输入法语音,选区旁会出现临时卡片,承接你说的话。结束语音、文字定稿后,默认自动发送,直接进入追问。
25
26
 
26
- - **收尾两个开关默认都开**(设置页「界面 · 语音输入」可关):说完**自动发送**;一个字都没说**自动收起**。
27
- - **不读输入法的转写结果**:开麦时插件只是把自己的输入框抢到焦点上,字是输入法自己"上屏"进来的——所以不需要任何输入法权限,也不经第三方。
28
- - **音频不落盘、不进会话**:只在这一次识别里用一下,转写完即丢。
29
- - 需要:macOS 14.4+(用来检测"输入法正在用麦克风",插件自带一个小探针)+ 一个输入法(实测豆包)。识别在 host 上做,本机模型 / 云端服务都能用。
27
+ ![选中文本与语音卡片之间的真实连线,问题可直接写入选区旁的输入框](assets/voice-card-light.png)
30
28
 
31
- ---
29
+ *选区通过青色下划线和虚线连接到临时输入卡片。*
30
+
31
+ **小窗已经打开**:划选新的文字,再启动输入法语音。这段文字会作为引用加入小窗,你说的话写入追问框。
32
+
33
+ ### 在小窗里,也能直接开口
34
+
35
+ 点击输入框旁的麦克风开始录音,查看实时波形与转写;点击停止完成输入,或按 `Esc` 取消本次录音。实时转写不可用时,会在停止后识别。
36
+
37
+ ![DSH 中实际运行的语音输入:小窗底部显示录音控制与波形](assets/voice.png)
38
+
39
+ *英文例句的解读小窗中,直接启动语音追问。*
40
+
41
+ 语音卡片默认开启**说完直接发送**和**空录音时收起**。在 **设置 → 界面 → 语音输入** 中可以分别关闭。
42
+
43
+ ![语音相关设置的真实截图:空录音时收起与说完直接发送](assets/voice-settings.png)
44
+
45
+ > 输入法联动需要 **macOS 14.4+** 与支持的语音输入法(已适配豆包)。小窗麦克风需要 DSH 启用语音识别服务。未配置语音时,文字解读与键盘追问仍可使用。
46
+
47
+ <details>
48
+ <summary>小窗麦克风提示不可用?</summary>
49
+
50
+ 请检查 DSH 宿主是否启用了 `@deepseek-ai/dsh-experimental-speech-to-text`,以及识别模型是否已就绪。首次使用时,如果小窗显示 **准备模型**,点击后按提示准备;下载与识别在宿主运行。
51
+
52
+ 这与输入法语音是两个独立入口:输入法联动使用输入法本身的识别能力。
53
+
54
+ </details>
55
+
56
+ <a id="read"></a>
57
+
58
+ ## 02 / 先读懂,再深入
59
+
60
+ 在 DSH 中选中文字,点击 **解读**。英文先给译文,中文按内容给解读,代码则生成注释;需要更多背景时,再点 **展开详解**。
61
+
62
+ ![选中英文例句后,真实小窗给出翻译并显示展开详解入口](assets/translation.png)
63
+
64
+ *选中 “The migration ran long, so we will ship on Wednesday”,先得到译文。*
65
+
66
+ 展开后,模型结合前后文与可用的会话背景解释术语、语气和承接关系。你可以直接在底部继续追问。
67
+
68
+ ![真实解读小窗中的上下文详解与后续追问](assets/detail.png)
69
+
70
+ *接着问“ran long 是说迁移失败了吗?”,把问题留在同一段讨论里。*
32
71
 
33
- ## 划词解读:翻译 + "在这里"的意思
72
+ <a id="quotes"></a>
34
73
 
35
- 读文档、看代码注释、翻聊天记录时,卡住的往往不是"这个词什么意思",而是**"它在这里什么意思"**。插件把**选中文字 + 它周围的内容 + 当前会话的对话背景**一起交给模型,所以答案能落到"这里"。
74
+ ## 03 / 把相关内容,一起带进来
36
75
 
37
- - **两段式,先快后深**:点开面板**先只出翻译**(上下文收窄,首字最快);翻译出来后才出现 `↓ 展开详解`,点了才用完整会话背景做深度解读。想快就停在第一段。
38
- - **翻译按内容自适应**:中文句子只译其中夹着的英文;中文词列义项;英文句给译文;英文单词带音标逐义项;其他语言译成中文。最后固定一条**结论条**:`> 在本句中:<它在这里的确切含义>`。
39
- - **详解讲透"在这里"**:先判断这段文字是真实内容还是示例 / 引用,再讲专业名词的两层含义(通用定义 + 此处的具体所指)与承接关系;拿不准的会**先联网查证**再解释,事实融进解释里。
40
- - **选中代码 → 出注释**:整篇回答是一个代码块,**注释与原代码交替**,每条语句前面一行该语言的注释符号,原代码逐字不变。
76
+ 小窗打开时,在主界面、小窗正文或支持的侧边栏网页预览中划选文字,点 **引用**,就能带着新材料继续问。一次最多加入 **4 段引用**,每段都可以单独移除。
41
77
 
42
- <img src="assets/stage2.png" alt="展开详解后:翻译与详解两节卡片,下方可继续追问" width="540">
78
+ ![真实小窗中,所选内容作为引用卡片出现在追问输入框上方](assets/quotes.png)
43
79
 
44
- ### 追问、引用、历史、升格
80
+ 不必每次从头来:
45
81
 
46
- | 想做的事 | 怎么操作 |
82
+ | 想做什么 | 入口 |
47
83
  | --- | --- |
48
- | **追问** | 面板底部输入框直接问(`Enter` 发送 / `Shift+Enter` 换行);答案流式出现,多轮上下文保留 |
49
- | **引用别处的文字一起问** | 小窗里、主界面上、**侧边栏网页里**划选 → 点 `❝ 引用`;鼠标悬停某条回答末尾的 `❝ 引用整条` 可引用整条(最多 4 段,卡片在输入框上方,可单条删) |
50
- | **回到刚才那次解读** | 点右下角悬浮胶囊,或 `🕘 最近` 里挑一条——原样回来,不重新请求 |
51
- | **把这次解读变成正式会话** | 点 `↗ 升格`(带上下文、引用与两节结论) |
52
- | **打断生成** | 生成中点 `停止`;关窗不会丢,胶囊点回来还是原样 |
84
+ | 回到刚才的解读 | 右下角悬浮胶囊,或小窗里的 **最近** |
85
+ | 把讨论继续做下去 | **升格**,将解读与追问带入正式会话 |
86
+ | 阅读代码 | 选中代码,查看注释与原代码交替呈现的解释 |
87
+ | 用图表理解复杂问题 | 将**输出偏好**切为网页,模型可生成 HTML 页面 |
53
88
 
54
- ---
89
+ <a id="install"></a>
55
90
 
56
- ## 安装
91
+ ## 快速安装
57
92
 
58
- **从 npm**(已发布):
93
+ 这是 DSH 环境中的插件。请先安装 DSH、配置一个可用模型,再运行:
59
94
 
60
95
  ```bash
61
96
  dsh plugin --profile web add @yfwu2020/dsh-selection-explain
62
97
  ```
63
98
 
64
- **从 Release**(附构建好的 `.tgz`,装完即用):
99
+ 安装后刷新 DSH 页面,选中一段文字,点击 **解读** 即可。
65
100
 
66
- ```bash
67
- VER=$(curl -sI https://github.com/yfwu2020/dsh-selection-explain/releases/latest | grep -i '^location:' | sed 's|.*/tag/v||' | tr -d '\r\n')
68
- curl -LO "https://github.com/yfwu2020/dsh-selection-explain/releases/download/v$VER/yfwu2020-dsh-selection-explain-$VER.tgz"
69
- mkdir -p ~/.dsh/plugins/dsh-selection-explain
70
- tar -xzf "yfwu2020-dsh-selection-explain-$VER.tgz" -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
71
- dsh plugin --profile web add ~/.dsh/plugins/dsh-selection-explain
72
- ```
101
+ 也可在 [GitHub Releases](https://github.com/yfwu2020/dsh-selection-explain/releases) 下载构建包,按下方说明安装。
102
+
103
+ <details>
104
+ <summary>安装 Release 构建包</summary>
73
105
 
74
- **从源码**:
106
+ 将下载的 `.tgz` 放到当前目录,以 v0.10.1 为例:
75
107
 
76
108
  ```bash
77
- git clone git@github.com:yfwu2020/dsh-selection-explain.git && cd dsh-selection-explain
78
- npm install && bash scripts/build.sh && dsh plugin --profile web add .
109
+ mkdir -p ~/.dsh/plugins/dsh-selection-explain
110
+ tar -xzf yfwu2020-dsh-selection-explain-0.10.1.tgz -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
111
+ dsh plugin --profile web add ~/.dsh/plugins/dsh-selection-explain
79
112
  ```
80
113
 
81
- 装好后**刷新页面**,选中文字试试(控制台会打印 `[dsh-selection-explain] 划词解读已就绪`)。卸载:`dsh plugin --profile web remove @yfwu2020/dsh-selection-explain`。
82
-
83
- ---
84
-
85
- ## 用法
114
+ 安装完成后刷新 DSH 页面。其他版本请替换文件名中的版本号。
86
115
 
87
- | 动作 | 结果 |
88
- | --- | --- |
89
- | 鼠标划选 / `Shift`+方向键 | 选区末端浮出 `✦ 解读`(输入框内的选择不触发;侧边栏的 HTML 预览里同样生效) |
90
- | 点 `✦ 解读` | 弹出面板:顶部是选中文字,正文是「翻译」卡片 |
91
- | `↓ 展开详解` | 用完整会话背景追加「详解」卡片 |
92
- | 小窗开着时划词 | 浮出的是 `❝ 引用`(要解读新的一段,先 `Esc` 收起小窗) |
93
- | 输入框右边 🎤 / 划完直接开麦 | 见上面「语音」一节 |
94
- | `✕` / `Esc` | 关面板。**语音进行中按 `Esc` 只取消这次语音**(已说出来的字留着),面板留着;再按一次才关窗 |
95
- | 右下角悬浮胶囊 | 点一下:回到最近一次小窗 / 收起 |
116
+ </details>
96
117
 
97
- 几个不别扭的细节:**点面板外不会关闭面板**(答案留在原地,只有 `✕`/`Esc`/胶囊能收);输入框**跟着内容长高**(最多约 7 行,再长内部滚动并自动停在最新一行);开窗**自动聚焦**输入框(只有你正在别处打字时才不抢)。与「完整视图」插件同装时,`Esc` 会按"最后操作的那个窗"逐层退出,一次只退一层。
118
+ <details>
119
+ <summary>从源码安装 / 卸载</summary>
98
120
 
99
- ---
100
-
101
- ## 设置
121
+ ```bash
122
+ git clone https://github.com/yfwu2020/dsh-selection-explain.git
123
+ cd dsh-selection-explain
124
+ npm install
125
+ npm run build
126
+ dsh plugin --profile web add .
127
+ ```
102
128
 
103
- 在面板里点 ⚙(或在设置页)改,改完**自动保存**;也能写 `cordis.patch.yml`(键名与默认值见 `src/settings.ts`)。常改的就几项:
129
+ 源码构建需要 package.json 中列出的 DSH 开发依赖可用。
104
130
 
105
- - **模型**:默认「跟随主会话」,也可单独指定(首轮 / 次轮 / 追问共用这一个模型)。
106
- - **思考强度**:三个阶段各自一格 —— `关 / 低 / 高 / 最高`。
107
- 各家的档位名字并不统一(`off>low>high>max`、`low>high>max`、`off>minimal>…>high`、只有 `max`…),所以插件**每次都按当前模型自己声明的档位现算**,把你这四格落到它真实拥有的档上(悬停格子能看到"该模型发 xxx",设置页那行也会实时写出四格的落点)。**小窗里的「推理等级(追问档)」是同一套四格、同一份数据**,两边改哪边都一样。模型不吃某一档时会自动换档重试并记住结论,下次不再撞。
108
- - **背景 / 工具 / 上限**:是否附带会话消息、引用是否带上文、能不能联网(`advanced_search`、`platform_search`)、超时与频率上限——都有默认值,不改也能用。
109
- - **界面 · 语音输入**:上面说的两个收尾开关、右下角胶囊、侧边栏网页划词桥(`bridgeSidebarPreview`,默认开)。
131
+ 卸载 npm 安装的插件:
110
132
 
111
- ---
133
+ ```bash
134
+ dsh plugin --profile web remove @yfwu2020/dsh-selection-explain
135
+ ```
112
136
 
113
- ## 隐私
137
+ </details>
114
138
 
115
- - **语音**:音频不落盘、不进会话;插件不读输入法的转写结果;不做边录边出的流式上传(说完一次给全,识别在 host 上)。
116
- - **会话背景**:可以关(`sessionContext: false`);开启时只带你选中文字所在的消息及其之前若干条,工具调用 / 思考 / 系统注入都会被滤掉。
117
- - **联网**:只在你启用的工具范围内查证;工具日志不进小窗,只给结论。
118
- - **本地数据**:解读历史 `~/.dsh/selection-explain/history.json`;"哪些模型不吃关档 / 关档会泄漏"的记忆 `leaky-off.json`。都是明文,随时可删。
139
+ <a id="settings"></a>
119
140
 
120
- ---
141
+ ## 使用与设置
121
142
 
122
- ## 常见问题
143
+ `Enter` 发送 · `Shift+Enter` 换行 · `Esc` 收起小窗。录音期间,第一次 `Esc` 只取消录音,再按一次才关闭小窗。
123
144
 
124
- **选中了没反应?** 输入框里的选择不触发(避免干扰打字);另外面板**不跟随页面滚动**(滚动即收起),符合"划完即看"。侧边栏的**源码视图**里没有 iframe,切回「预览」即可。
145
+ 点击小窗顶部的 **设置**,修改会自动保存:
125
146
 
126
- **结论可靠吗?** 来自你配置的模型。专业领域术语请以人工判断为准;拿不准时它会联网查证,但也会出错。
147
+ | 设置 | 可以调整什么 |
148
+ | --- | --- |
149
+ | 模型 | 跟随主会话,或为解读单独指定模型 |
150
+ | 思考强度 | 首轮、次轮与追问分别选择 **关 / 低 / 高 / 最高**;按模型实际能力映射 |
151
+ | 背景 | 是否附带会话消息,以及引用的上下文范围 |
152
+ | 联网 | 是否允许模型调用工具,以及允许的工具范围 |
153
+ | 界面 | 语音卡片收尾、侧边栏网页划词与悬浮胶囊 |
154
+ | 数据 | 保留多少条最近解读 |
127
155
 
128
- **为什么首轮只有翻译?** 故意的:先用最窄的上下文抢首字速度,想要深度再点 `↓ 展开详解`。
156
+ ## 数据与使用范围
129
157
 
130
- **一定要装语音相关的服务吗?** 不用。没装时只是 🎤 与语音卡明说不可用,划词解读一切照常。
158
+ 选中文字、必要的前后文,以及开启的会话背景会交给你配置的模型。可在设置中关闭会话背景或工具调用。
131
159
 
132
- ---
160
+ 插件不将录音保存为音频文件,也不把音频附到会话;小窗录音由 DSH 配置的识别服务处理,是否经过云端取决于该服务。输入法语音由输入法本身处理,插件接收输入框中写入的文字。
133
161
 
134
- ## 已知限制
162
+ 解读历史默认保留最近 **20 条**,以明文存储在 `~/.dsh/selection-explain/history.json`。可在 **设置 → 数据** 中调整,设为 `0` 停止保存新历史;已有历史不会因此清空。
135
163
 
136
- - 只覆盖 **DSH 界面内**(含侧边栏 HTML 预览)的选中文字;**内置浏览器标签页里的外部站点**(远端 URL 的 iframe,没有任何注入手段)与**源码视图**不触发。
137
- - 用**路由 URL**(而非 `srcdoc`/`blob`)的第三方预览器不接管(不动别人的资源改写与生命周期);小窗里自己生成的网页预览(iframe 内部)也不划词——想引用它就用末尾的 `❝ 引用整条`。
138
- - 语音卡依赖 `macOS 14.4+` 的麦克风探针;🎤 语音输入需要宿主装有语音识别服务(如本机 SenseVoice)。
139
- - 依赖 DSH 的 `webServer` 与 `llm` 服务;宿主过旧可能不兼容(见 `package.json` 的 peerDependencies)。
164
+ 支持 DSH 界面与可注入的侧边栏 HTML 预览。输入框、源码视图、外部站点浏览器标签页,以及部分第三方预览不触发划词(直接通过独立 URL 加载、无法注入划词桥的页面)。模型回答与联网查证仍可能出错。
140
165
 
141
- ---
166
+ <details>
167
+ <summary>开发与兼容性</summary>
142
168
 
143
- ## 开发者
169
+ 依赖 DSH 的 `webServer`、`llm` 等宿主服务;具体版本范围见 [package.json](https://github.com/yfwu2020/dsh-selection-explain/blob/main/package.json)。
144
170
 
145
171
  ```bash
146
- npm test # 单测 + 真浏览器冒烟(本机 Chrome;找不到自动跳过)
147
- bash scripts/build.sh # host 走 tsc,client 是手写 bundle 直接拷贝到 lib/
172
+ npm test
173
+ npm run typecheck
174
+ npm run build
148
175
  ```
149
176
 
150
- 浏览器控制台自带自检钩子:`__dshSelectionExplain.state()` 看状态、`voiceCard()` / `voice()` 看语音、`bridge()` 看侧边栏划词桥、`selection()` 看当前选区。HTTP 接口(`/selection-explain/api/*`)、实现细节与踩坑记录见 [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) 与源码注释 —— 那份文档随本机开发走,刻意不进发布包。
177
+ 配置项见 [src/settings.ts](https://github.com/yfwu2020/dsh-selection-explain/blob/main/src/settings.ts),默认值见 [src/index.ts](https://github.com/yfwu2020/dsh-selection-explain/blob/main/src/index.ts)。
178
+
179
+ </details>
151
180
 
152
- ## License
181
+ ---
153
182
 
154
- MIT
183
+ [MIT License](https://github.com/yfwu2020/dsh-selection-explain/blob/main/LICENSE) · [反馈问题](https://github.com/yfwu2020/dsh-selection-explain/issues)
Binary file
package/assets/quotes.png CHANGED
Binary file
Binary file
Binary file
Binary file
package/assets/voice.png CHANGED
Binary file
package/lib/client.js CHANGED
@@ -688,6 +688,9 @@ window.__ModuleLoader__.load({
688
688
  // flex-wrap 是给窄屏兜底的:这一行现在有 4 件东西(输出偏好 / 模型 / 麦克风 / 发送),
689
689
  // 窄面板下宁可折成两行,也不能把发送键挤出可视区(挤出去就点不到了)。
690
690
  '.dsh-sel-asktools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;row-gap:6px}',
691
+ // 录音行出现时**不许换行**:发送键必须留在同一行
692
+ //(用户报:一开麦,↑ 就被顶到下面单独一行去了)
693
+ '.dsh-sel-asktools[data-capturing="1"]{flex-wrap:nowrap}',
691
694
  '.dsh-sel-askspace{flex:1 1 auto}',
692
695
  // 页眉里的撑开占位(标题与右侧按钮之间)
693
696
  '.dsh-sel-headspace{flex:1 1 auto;min-width:8px}',
@@ -764,7 +767,11 @@ window.__ModuleLoader__.load({
764
767
  '.dsh-sel-mic[data-state="requesting"] svg{animation:dsh-sel-spin .9s linear infinite}',
765
768
  '.dsh-sel-mic:disabled{cursor:default;opacity:.35}',
766
769
  // 录音行:flex:1 让它吃掉中间那截(主会话也是 waveform flex:1)
767
- '.dsh-sel-capture{display:none;flex:1 1 auto;min-width:0;align-items:center;gap:8px}',
770
+ // flex-basis 必须给 **0%**(不是 auto):行内换行的判定用的是**收缩前**的假设宽度,
771
+ // 而波形那个 svg 的固有宽度约 300px —— 给 auto 时"这一行放不下"会被判成真,发送键于是
772
+ // 掉到下一行。给 0% 之后两者按可用宽度分;min-width:0 + 波形的 min-width:24px 保证
773
+ // 录音行照旧撑满、也不会塌成 0。
774
+ '.dsh-sel-capture{display:none;flex:1 1 0%;min-width:0;align-items:center;gap:8px}',
768
775
  '.dsh-sel-capture[data-show="1"]{display:flex}',
769
776
  // 圆键:和发送键同尺寸、同圆度;浅底 + hover 变深(主会话的 roundButton 也是这套)
770
777
  '.dsh-sel-round{flex:0 0 auto;width:26px;height:26px;padding:0;border:0;border-radius:13px;cursor:pointer;',
@@ -777,7 +784,7 @@ window.__ModuleLoader__.load({
777
784
  // 停止键:图标是实心方块(主会话的 IconStopFill),颜色取主文字色 —— 一眼是"停"
778
785
  '.dsh-sel-stop{color:var(--dsw-alias-label-primary,inherit)}',
779
786
  // 波形:80 根竖线、中间对齐、静音时是一条虚线(算法与主会话逐行一致,见 paintWave)
780
- '.dsh-sel-vwave{flex:1 1 auto;min-width:24px;height:24px;display:block;color:var(--dsw-alias-label-secondary,inherit)}',
787
+ '.dsh-sel-vwave{flex:1 1 0%;min-width:24px;height:24px;display:block;color:var(--dsw-alias-label-secondary,inherit)}',
781
788
  // 状态文案(请允许使用麦克风… / 识别中… / 没听清…):12px 次要色 + 可选呼吸点
782
789
  '.dsh-sel-vactivity{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:8px;font-size:12px;line-height:1.4;',
783
790
  'color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
@@ -4533,7 +4540,11 @@ window.__ModuleLoader__.load({
4533
4540
  vcard.setAttribute('data-voiced', on ? '1' : '0')
4534
4541
  if (!vcardBox) return
4535
4542
  if (String(vcardBox.value || '').trim()) return
4536
- vcardBox.placeholder = on ? '正在听…' : '直接打字也行…'
4543
+ // 松手之后、框还空着时显示什么:
4544
+ // · 「空录音时收起」开着(默认)→ 「正在关闭…」:这张卡马上要被收掉,
4545
+ // 如实说它正在走(而不是"直接打字也行…"那种假邀请);
4546
+ // · 两个收尾开关都关着(卡会留着)→ 才给"直接打字也行…"这句邀请。
4547
+ vcardBox.placeholder = on ? '正在听…' : settingsLive.voiceCancelOnSilence !== false ? '正在关闭…' : '直接打字也行…'
4537
4548
  }
4538
4549
 
4539
4550
  function focusVCard() {
@@ -4924,6 +4935,9 @@ window.__ModuleLoader__.load({
4924
4935
  function closeVCard(animate) {
4925
4936
  if (!vcardState.open) return
4926
4937
  vcardState.open = false
4938
+ // 取消的那一刻就把空框的占位换成「正在关闭…」:卡正在塌缩,空框里挂着"直接打字也行…"
4939
+ // 是句假话。已经打出来的字**不动** —— 它就是那个塌缩动画要收掉的东西。
4940
+ if (vcardBox && !String(vcardBox.value || '').trim()) vcardBox.placeholder = '正在关闭…'
4927
4941
  vcardState.range = null
4928
4942
  vcardState.anchorRect = null
4929
4943
  // ⚠️ 必须**显式 blur**,不能只把卡片 display:none 掉。
@@ -10379,6 +10393,8 @@ window.__ModuleLoader__.load({
10379
10393
  var capturing = phase !== 'idle'
10380
10394
  captureRow.setAttribute('data-show', capturing ? '1' : '0')
10381
10395
  captureRow.style.display = capturing ? 'flex' : 'none'
10396
+ // 工具行也标记一下:录音时 flex-wrap 锁死(发送键必须还在这一行)
10397
+ askTools.setAttribute('data-capturing', capturing ? '1' : '0')
10382
10398
  // 录音行出现时,工具行左边那三样(输出偏好 / 模型 / 🎤)让位——和主会话一样,
10383
10399
  // 录音时那行只讲一件事:怎么结束、结束之后会得到什么。
10384
10400
  webMode.style.display = capturing ? 'none' : ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yfwu2020/dsh-selection-explain",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "DSH 划词解读:选中文字浮出按钮,点击弹窗流式显示专业中英翻译 + 该文字在当前上下文中的含义详解",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -337,6 +337,50 @@ async function main() {
337
337
  // 因为 CSS 声明块漏了收尾花括号被整条丢弃(属性对、规则不生效,单测照样全绿);
338
338
  // 修好闭合后它又暴露了"追问一下宽度就从 440 跳到 540"的问题 —— 两件事都只有真浏览器量得出来。
339
339
  // 现在宽度**不随阶段变**(恒 540),下面同时钉住"首轮就是 540"和"追问之后仍是 540"。
340
+ // ── 录音行不许把发送键顶到下一行(用户报:一开麦 ↑ 就掉到下面单独一行)──
341
+ // 真浏览器量布局:按录音态把 DOM 摆成插件摆的样子(亮出录音行、让位那几样工具),
342
+ // 再量发送键是否还与录音行同处一行。CSS 里那两条 flex-basis:0% + nowrap 就是为它服务的。
343
+ {
344
+ var toolsEl = document.querySelector('.dsh-sel-asktools')
345
+ var captureEl = document.querySelector('.dsh-sel-capture')
346
+ var sendEl = document.querySelector('.dsh-sel-asksend')
347
+ var waveEl = document.querySelector('.dsh-sel-vwave')
348
+ if (toolsEl && captureEl && sendEl) {
349
+ var yielding = ['.dsh-sel-pref', '.dsh-sel-askspace', '.dsh-sel-picker', '.dsh-sel-mic']
350
+ .map(function (sel) { return document.querySelector(sel) })
351
+ .filter(Boolean)
352
+ var restore = yielding.map(function (n) { return [n, n.style.display] })
353
+ // 必须**窄一点**量:波形那个 svg 的固有宽度是 384px(viewBox 640x40 按 24px 高推出来的),
354
+ // 540px 的面板刚好放得下、量不出问题;用户那个宽度才会露出来。面板本来就可调窄,这是真实场景。
355
+ var panelWide = document.querySelector('.dsh-sel-panel')
356
+ var widthBefore = panelWide ? panelWide.style.width : ''
357
+ if (panelWide) panelWide.style.width = '420px'
358
+ toolsEl.setAttribute('data-capturing', '1')
359
+ captureEl.setAttribute('data-show', '1')
360
+ captureEl.style.display = 'flex'
361
+ if (waveEl) waveEl.style.display = ''
362
+ yielding.forEach(function (n) { n.style.display = 'none' })
363
+ var captureRect = captureEl.getBoundingClientRect()
364
+ var sendRect = sendEl.getBoundingClientRect()
365
+ check(
366
+ '录音行出现时发送键仍在同一行(不被顶到下面)',
367
+ sendRect.height > 0 && sendRect.top < captureRect.bottom && Math.abs(sendRect.top - captureRect.top) <= 8,
368
+ JSON.stringify({
369
+ capture: [Math.round(captureRect.top), Math.round(captureRect.bottom)],
370
+ send: [Math.round(sendRect.top), Math.round(sendRect.bottom)],
371
+ }),
372
+ )
373
+ restore.forEach(function (pair) { pair[0].style.display = pair[1] })
374
+ if (panelWide) panelWide.style.width = widthBefore
375
+ captureEl.style.display = 'none'
376
+ captureEl.setAttribute('data-show', '0')
377
+ toolsEl.setAttribute('data-capturing', '0')
378
+ if (waveEl) waveEl.style.display = 'none'
379
+ } else {
380
+ check('录音行出现时发送键仍在同一行(不被顶到下面)', false, '没找到工具行/录音行/发送键')
381
+ }
382
+ }
383
+
340
384
  var WIDE_WIDTH = 540
341
385
  {
342
386
  var panelEl = document.querySelector('.dsh-sel-panel')
@@ -3646,6 +3646,16 @@ assert('点回来看到"已停止"的状态与可重试入口', hook.state().pha
3646
3646
 
3647
3647
  assert('CSS 契约:模型胶囊紧挨发送键、菜单向上弹', /\.dsh-sel-picker\{display:inline-flex/.test(css) && /\.dsh-sel-pickermenu\{position:absolute;right:10px;bottom:calc\(100% \+ 8px\)/.test(css), '')
3648
3648
  assert('CSS 契约:档位选中态为主色实心', /\.dsh-sel-tierbtn\[data-on="1"\]\{background:var\(--sel-fill/.test(css), '')
3649
+ // 用户报:一开麦,输入框右下角那个 ↑ 就被顶到下面单独一行去了。
3650
+ // 行内换行的判定用的是**收缩前**的假设宽度,而波形 svg 的固有宽度约 300px ——
3651
+ // 所以录音行/波形必须 flex-basis:0%(不是 auto),并且录音时工具行锁死 nowrap。
3652
+ assert(
3653
+ 'CSS 契约:录音时工具行不换行 + 录音行与波形按可用宽度分(flex-basis 0%)',
3654
+ /\.dsh-sel-asktools\[data-capturing="1"\]\{flex-wrap:nowrap\}/.test(css) &&
3655
+ /\.dsh-sel-capture\{display:none;flex:1 1 0%;min-width:0/.test(css) &&
3656
+ /\.dsh-sel-vwave\{flex:1 1 0%;min-width:24px/.test(css),
3657
+ '',
3658
+ )
3649
3659
 
3650
3660
  const nodes = hook.modelNodes()
3651
3661
  // 桩里没有 querySelectorAll(客户端也不用它),用 walk 数节点
@@ -5681,6 +5691,13 @@ assert('Esc/关闭后隐藏', panel.style.display === 'none')
5681
5691
  hook.voiceCard().open === true,
5682
5692
  JSON.stringify(hook.voiceCard()),
5683
5693
  )
5694
+ // 开着「空录音时收起」时,这段"等定稿"的窗口里框里显示「正在关闭…」:
5695
+ // 卡片马上要被收掉,写"直接打字也行…"是句假话(用户明确要求取消时不要显示任何文字)
5696
+ assert(
5697
+ '(开着「空录音时收起」)空框里如实显示「正在关闭…」(不是"直接打字也行…"那种假邀请)',
5698
+ !!box && String(box.placeholder) === '正在关闭…' && String(box.value) === '',
5699
+ JSON.stringify({ placeholder: box ? String(box.placeholder) : '(无卡片)', value: box ? String(box.value) : '' }),
5700
+ )
5684
5701
  await sleep(900)
5685
5702
  const cardSilent = find('dsh-sel-vcard')
5686
5703
  assert('定稿后才取消(空录音)', hook.voiceCard().open === false, JSON.stringify(hook.voiceCard()))
@@ -5761,6 +5778,38 @@ assert('Esc/关闭后隐藏', panel.style.display === 'none')
5761
5778
  JSON.stringify(hook.voiceCard()),
5762
5779
  )
5763
5780
 
5781
+ // 关掉「空录音时收起」(卡会留着)→ 才给"直接打字也行…"那句邀请
5782
+ settingsStub.value = { voiceCancelOnSilence: false, voiceAutoSend: false }
5783
+ hook.reloadSettings()
5784
+ await sleep(320)
5785
+ hook.close()
5786
+ await sleep(20)
5787
+ micStub.value = recording
5788
+ // 走真实路径把卡开出来(与上面几段同一套:选区还在 → mouseup → 浮标 → 这一拍轮询发现开麦)
5789
+ documentStub.dispatch('mouseup', { target: body })
5790
+ await sleep(320)
5791
+ if (box) {
5792
+ box.value = ''
5793
+ box.dispatch('input', { target: box, preventDefault() {}, stopPropagation() {} })
5794
+ }
5795
+ await sleep(80)
5796
+ micStub.value = stopped
5797
+ await sleep(400)
5798
+ assert(
5799
+ '(关掉「空录音时收起」)卡片要留着,这时才显示"直接打字也行…"',
5800
+ hook.voiceCard().open === true && !!box && String(box.placeholder).indexOf('直接打字') >= 0,
5801
+ JSON.stringify({ open: hook.voiceCard().open, placeholder: box ? String(box.placeholder) : '(无)' }),
5802
+ )
5803
+ // Esc 取消:那一瞬间空框里就该是空的(不留占位)
5804
+ documentStub.dispatch('keydown', { key: 'Escape', target: body, preventDefault() {}, stopPropagation() {} })
5805
+ await sleep(20)
5806
+ assert(
5807
+ 'Esc 取消录音:那一刻空框就换成「正在关闭…」(不等动画结束)',
5808
+ !!box && String(box.placeholder) === '正在关闭…' && String(box.value) === '',
5809
+ JSON.stringify({ placeholder: box ? String(box.placeholder) : '(无)', value: box ? String(box.value) : '' }),
5810
+ )
5811
+ await sleep(800) // 等关机动画收尾
5812
+
5764
5813
  // 收尾:设置清回默认,别影响后面的用例
5765
5814
  settingsStub.value = {}
5766
5815
  hook.reloadSettings()
@@ -688,6 +688,9 @@ window.__ModuleLoader__.load({
688
688
  // flex-wrap 是给窄屏兜底的:这一行现在有 4 件东西(输出偏好 / 模型 / 麦克风 / 发送),
689
689
  // 窄面板下宁可折成两行,也不能把发送键挤出可视区(挤出去就点不到了)。
690
690
  '.dsh-sel-asktools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;row-gap:6px}',
691
+ // 录音行出现时**不许换行**:发送键必须留在同一行
692
+ //(用户报:一开麦,↑ 就被顶到下面单独一行去了)
693
+ '.dsh-sel-asktools[data-capturing="1"]{flex-wrap:nowrap}',
691
694
  '.dsh-sel-askspace{flex:1 1 auto}',
692
695
  // 页眉里的撑开占位(标题与右侧按钮之间)
693
696
  '.dsh-sel-headspace{flex:1 1 auto;min-width:8px}',
@@ -764,7 +767,11 @@ window.__ModuleLoader__.load({
764
767
  '.dsh-sel-mic[data-state="requesting"] svg{animation:dsh-sel-spin .9s linear infinite}',
765
768
  '.dsh-sel-mic:disabled{cursor:default;opacity:.35}',
766
769
  // 录音行:flex:1 让它吃掉中间那截(主会话也是 waveform flex:1)
767
- '.dsh-sel-capture{display:none;flex:1 1 auto;min-width:0;align-items:center;gap:8px}',
770
+ // flex-basis 必须给 **0%**(不是 auto):行内换行的判定用的是**收缩前**的假设宽度,
771
+ // 而波形那个 svg 的固有宽度约 300px —— 给 auto 时"这一行放不下"会被判成真,发送键于是
772
+ // 掉到下一行。给 0% 之后两者按可用宽度分;min-width:0 + 波形的 min-width:24px 保证
773
+ // 录音行照旧撑满、也不会塌成 0。
774
+ '.dsh-sel-capture{display:none;flex:1 1 0%;min-width:0;align-items:center;gap:8px}',
768
775
  '.dsh-sel-capture[data-show="1"]{display:flex}',
769
776
  // 圆键:和发送键同尺寸、同圆度;浅底 + hover 变深(主会话的 roundButton 也是这套)
770
777
  '.dsh-sel-round{flex:0 0 auto;width:26px;height:26px;padding:0;border:0;border-radius:13px;cursor:pointer;',
@@ -777,7 +784,7 @@ window.__ModuleLoader__.load({
777
784
  // 停止键:图标是实心方块(主会话的 IconStopFill),颜色取主文字色 —— 一眼是"停"
778
785
  '.dsh-sel-stop{color:var(--dsw-alias-label-primary,inherit)}',
779
786
  // 波形:80 根竖线、中间对齐、静音时是一条虚线(算法与主会话逐行一致,见 paintWave)
780
- '.dsh-sel-vwave{flex:1 1 auto;min-width:24px;height:24px;display:block;color:var(--dsw-alias-label-secondary,inherit)}',
787
+ '.dsh-sel-vwave{flex:1 1 0%;min-width:24px;height:24px;display:block;color:var(--dsw-alias-label-secondary,inherit)}',
781
788
  // 状态文案(请允许使用麦克风… / 识别中… / 没听清…):12px 次要色 + 可选呼吸点
782
789
  '.dsh-sel-vactivity{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:8px;font-size:12px;line-height:1.4;',
783
790
  'color:var(--dsw-alias-label-secondary,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
@@ -4533,7 +4540,11 @@ window.__ModuleLoader__.load({
4533
4540
  vcard.setAttribute('data-voiced', on ? '1' : '0')
4534
4541
  if (!vcardBox) return
4535
4542
  if (String(vcardBox.value || '').trim()) return
4536
- vcardBox.placeholder = on ? '正在听…' : '直接打字也行…'
4543
+ // 松手之后、框还空着时显示什么:
4544
+ // · 「空录音时收起」开着(默认)→ 「正在关闭…」:这张卡马上要被收掉,
4545
+ // 如实说它正在走(而不是"直接打字也行…"那种假邀请);
4546
+ // · 两个收尾开关都关着(卡会留着)→ 才给"直接打字也行…"这句邀请。
4547
+ vcardBox.placeholder = on ? '正在听…' : settingsLive.voiceCancelOnSilence !== false ? '正在关闭…' : '直接打字也行…'
4537
4548
  }
4538
4549
 
4539
4550
  function focusVCard() {
@@ -4924,6 +4935,9 @@ window.__ModuleLoader__.load({
4924
4935
  function closeVCard(animate) {
4925
4936
  if (!vcardState.open) return
4926
4937
  vcardState.open = false
4938
+ // 取消的那一刻就把空框的占位换成「正在关闭…」:卡正在塌缩,空框里挂着"直接打字也行…"
4939
+ // 是句假话。已经打出来的字**不动** —— 它就是那个塌缩动画要收掉的东西。
4940
+ if (vcardBox && !String(vcardBox.value || '').trim()) vcardBox.placeholder = '正在关闭…'
4927
4941
  vcardState.range = null
4928
4942
  vcardState.anchorRect = null
4929
4943
  // ⚠️ 必须**显式 blur**,不能只把卡片 display:none 掉。
@@ -10379,6 +10393,8 @@ window.__ModuleLoader__.load({
10379
10393
  var capturing = phase !== 'idle'
10380
10394
  captureRow.setAttribute('data-show', capturing ? '1' : '0')
10381
10395
  captureRow.style.display = capturing ? 'flex' : 'none'
10396
+ // 工具行也标记一下:录音时 flex-wrap 锁死(发送键必须还在这一行)
10397
+ askTools.setAttribute('data-capturing', capturing ? '1' : '0')
10382
10398
  // 录音行出现时,工具行左边那三样(输出偏好 / 模型 / 🎤)让位——和主会话一样,
10383
10399
  // 录音时那行只讲一件事:怎么结束、结束之后会得到什么。
10384
10400
  webMode.style.display = capturing ? 'none' : ''