@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 +126 -97
- package/assets/detail.png +0 -0
- package/assets/quotes.png +0 -0
- package/assets/translation.png +0 -0
- package/assets/voice-card-light.png +0 -0
- package/assets/voice-settings.png +0 -0
- package/assets/voice.png +0 -0
- package/lib/client.js +19 -3
- package/package.json +1 -1
- package/scripts/smoke-panel.mjs +44 -0
- package/scripts/test-client.mjs +49 -0
- package/src/client/index.js +19 -3
package/README.md
CHANGED
|
@@ -1,154 +1,183 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# 划词解读
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
23
|
+
### 从划词到提问,一口气完成
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
**小窗还没打开**:划选文字后启动输入法语音,选区旁会出现临时卡片,承接你说的话。结束语音、文字定稿后,默认自动发送,直接进入追问。
|
|
25
26
|
|
|
26
|
-
-
|
|
27
|
-
- **不读输入法的转写结果**:开麦时插件只是把自己的输入框抢到焦点上,字是输入法自己"上屏"进来的——所以不需要任何输入法权限,也不经第三方。
|
|
28
|
-
- **音频不落盘、不进会话**:只在这一次识别里用一下,转写完即丢。
|
|
29
|
-
- 需要:macOS 14.4+(用来检测"输入法正在用麦克风",插件自带一个小探针)+ 一个输入法(实测豆包)。识别在 host 上做,本机模型 / 云端服务都能用。
|
|
27
|
+

|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
*选区通过青色下划线和虚线连接到临时输入卡片。*
|
|
30
|
+
|
|
31
|
+
**小窗已经打开**:划选新的文字,再启动输入法语音。这段文字会作为引用加入小窗,你说的话写入追问框。
|
|
32
|
+
|
|
33
|
+
### 在小窗里,也能直接开口
|
|
34
|
+
|
|
35
|
+
点击输入框旁的麦克风开始录音,查看实时波形与转写;点击停止完成输入,或按 `Esc` 取消本次录音。实时转写不可用时,会在停止后识别。
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
*英文例句的解读小窗中,直接启动语音追问。*
|
|
40
|
+
|
|
41
|
+
语音卡片默认开启**说完直接发送**和**空录音时收起**。在 **设置 → 界面 → 语音输入** 中可以分别关闭。
|
|
42
|
+
|
|
43
|
+

|
|
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
|
+

|
|
63
|
+
|
|
64
|
+
*选中 “The migration ran long, so we will ship on Wednesday”,先得到译文。*
|
|
65
|
+
|
|
66
|
+
展开后,模型结合前后文与可用的会话背景解释术语、语气和承接关系。你可以直接在底部继续追问。
|
|
67
|
+
|
|
68
|
+

|
|
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
|
-
|
|
78
|
+

|
|
43
79
|
|
|
44
|
-
|
|
80
|
+
不必每次从头来:
|
|
45
81
|
|
|
46
|
-
|
|
|
82
|
+
| 想做什么 | 入口 |
|
|
47
83
|
| --- | --- |
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
| **打断生成** | 生成中点 `停止`;关窗不会丢,胶囊点回来还是原样 |
|
|
84
|
+
| 回到刚才的解读 | 右下角悬浮胶囊,或小窗里的 **最近** |
|
|
85
|
+
| 把讨论继续做下去 | **升格**,将解读与追问带入正式会话 |
|
|
86
|
+
| 阅读代码 | 选中代码,查看注释与原代码交替呈现的解释 |
|
|
87
|
+
| 用图表理解复杂问题 | 将**输出偏好**切为网页,模型可生成 HTML 页面 |
|
|
53
88
|
|
|
54
|
-
|
|
89
|
+
<a id="install"></a>
|
|
55
90
|
|
|
56
|
-
##
|
|
91
|
+
## 快速安装
|
|
57
92
|
|
|
58
|
-
|
|
93
|
+
这是 DSH 环境中的插件。请先安装 DSH、配置一个可用模型,再运行:
|
|
59
94
|
|
|
60
95
|
```bash
|
|
61
96
|
dsh plugin --profile web add @yfwu2020/dsh-selection-explain
|
|
62
97
|
```
|
|
63
98
|
|
|
64
|
-
|
|
99
|
+
安装后刷新 DSH 页面,选中一段文字,点击 **解读** 即可。
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
147
|
-
|
|
172
|
+
npm test
|
|
173
|
+
npm run typecheck
|
|
174
|
+
npm run build
|
|
148
175
|
```
|
|
149
176
|
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
package/scripts/smoke-panel.mjs
CHANGED
|
@@ -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')
|
package/scripts/test-client.mjs
CHANGED
|
@@ -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()
|
package/src/client/index.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
|
-
|
|
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
|
|
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
|
-
|
|
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' : ''
|