@yfwu2020/dsh-selection-explain 0.9.0 → 0.10.0

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,6 +1,6 @@
1
1
  # dsh-selection-explain · 划词解读
2
2
 
3
- > 在 [DSH](https://github.com/deepseek-ai) Web 界面里**选中任意文字**,就地得到**专业翻译** + **这段文字在当前上下文里到底是什么意思**,还能接着追问、或一键升格成正式会话。
3
+ > 在 DSH 里**选中任意文字**,就地得到**专业翻译** + **它在当前上下文里到底是什么意思**;还能接着追问、把别处的文字带进来问、或者一键升格成正式会话。
4
4
 
5
5
  [![release](https://img.shields.io/github/v/release/yfwu2020/dsh-selection-explain?label=release)](https://github.com/yfwu2020/dsh-selection-explain/releases)
6
6
  [![npm](https://img.shields.io/npm/v/@yfwu2020/dsh-selection-explain?label=npm)](https://www.npmjs.com/package/@yfwu2020/dsh-selection-explain)
@@ -9,266 +9,76 @@
9
9
 
10
10
  ---
11
11
 
12
- ## 它解决什么问题
12
+ ## 语音:划完直接说,不用打字
13
13
 
14
- 读英文文档、看代码注释、翻聊天记录时,卡住的往往不是"这个词什么意思",而是**"它在这里什么意思"**:
14
+ 追问往往只有一两句("这词在这里是不是贬义?"),可打字这件事本身就要把手从鼠标挪到键盘——和"划词"是矛盾的。所以划完可以直接说:
15
15
 
16
- - `commit` 是"提交"还是"承诺"?`stale closure` 在这个 bug 报告里指哪种闭包?
17
- - 同事写的 "let's circle back on the migration" 到底在安排什么?
18
- - 这段报错里的 `EADDRINUSE`,在当前这个项目里是端口冲突还是残留进程?
19
-
20
- 普通翻译工具只给词义,**不给语境**。这个插件把**选中文字 + 它周围的内容 + 当前会话的对话背景**一起喂给模型,所以答案能落到"这里"——并且它知道你是在读文档、读代码,还是在跟人对话。
21
-
22
- ## 功能
23
-
24
- ### 两段式:先快后深
25
-
26
- 点开面板**先只出翻译**(上下文窗口收窄到 8 条消息,首字最快);翻译出来后才出现 **`↓ 展开详解`**,点了才用完整会话背景(24 条消息)做深度解读。想要快就停在第一段,想要透就展开——**不强迫你为深度等首字**。
27
-
28
- <img src="assets/stage1.png" alt="首轮:只有翻译一节 + 展开详解按钮" width="540">
29
-
30
- ### 翻译:按选中内容的语言和形态自适应
31
-
32
- 不是无脑翻译,而是先判断这段文字是什么,只给对应的那一种:
33
-
34
- | 选中内容 | 输出 |
16
+ | 你现在的状态 | 按语音键说话 → 结果 |
35
17
  | --- | --- |
36
- | 中文句子(夹英文) | 中文部分**不翻译**,只把句中夹着的英文片段逐条译成中文 |
37
- | 中文词 / 词组 | 列常用义项(每行一条) |
38
- | 英文句子 / 词组 | 直接给中文译文 |
39
- | 英文单词 | 逐个义项一行,**带音标** |
40
- | 日 / 韩 / 法 / 德等其他语言 | 译成中文 |
41
-
42
- 最后固定给一条**结论条**:`> 在本句中:<它在这里的确切含义>`,面板里渲染成高亮条,一眼看到答案。
18
+ | **小窗没开**,刚划了一段文字 | 选中的地方**就地弹出一张临时卡片**,说的话直接落进卡片;松手定稿后**自动进小窗**追问(跳过翻译与详解) |
19
+ | **小窗开着**,刚划了一段文字 | 等于**替你点了一下 `❝ 引用`**:那段文字挂进小窗、光标进输入框,接着说的话直接落在小窗里,和下一条提问一起发出去 |
20
+ | 想直接对输入框说话 | 点输入框右边的 🎤 —— 和主会话**同一套**语音输入:实时波形、边说边出字、`■` 结束、`Esc` 取消(一次最长 60 秒) |
43
21
 
44
- ### 详解:讲透"在这里"的意思
22
+ <img src="assets/voice.png" alt="录音中的小窗:工具行是「✕ 取消 · 实时波形 · ■ 停止」,输入框里已经跟着长出了两行文字" width="540">
45
23
 
46
- - **先判断来源**:这段文字是真实内容,还是示例 / 测试用例 / 演示输出 / 引用别人的话?是后者就先点明(否则示例句会被当成真实陈述去解释)。
47
- - **专业名词两层讲**:先给通用含义(本领域标准定义或全称),再说它在这段上下文里具体指什么。
48
- - **讲透承接关系**:关键说法各指什么、承接了什么、能推出什么。
49
- - **可以联网**:拿不准或是新出现的名词、缩写、产品名,会先搜索确认再解释,事实融进解释里,不罗列搜索过程。
24
+ **侧边栏的网页里也能这么用**(新)。侧边栏预览的 HTML 页面跑在沙箱 iframe 里,父页面读不到它的选区;插件往被预览的文档里注入一小段"桥"脚本,由**帧内**读取选中的文字、把它前后的上下文(各 1500 字)和坐标报出来,再用主界面同一张卡片 / 同一个浮标呈现。**安全边界没有退让**:帧始终是不透明源,不给 `allow-same-origin`——网页读不到 GUI 的 DOM / Cookie / localStorage,桥也只往外报"选中了什么 + 周围那点文本"。
50
25
 
51
- ### 选中代码 → 出注释
26
+ - **收尾两个开关默认都开**(设置页「界面 · 语音输入」可关):说完**自动发送**;一个字都没说**自动收起**。
27
+ - **不读输入法的转写结果**:开麦时插件只是把自己的输入框抢到焦点上,字是输入法自己"上屏"进来的——所以不需要任何输入法权限,也不经第三方。
28
+ - **音频不落盘、不进会话**:只在这一次识别里用一下,转写完即丢。
29
+ - 需要:macOS 14.4+(用来检测"输入法正在用麦克风",插件自带一个小探针)+ 一个输入法(实测豆包)。识别在 host 上做,本机模型 / 云端服务都能用。
52
30
 
53
- 如果选中的是代码(在 `<pre>/<code>` 里,或文本特征像代码),首轮标题变成「**注释**」:整个回答是一个代码块,里面是**注释 + 原代码交替**的批注清单——每条语句**前面**一行该语言的注释符号(`//`、`#`、`--`、`<!--`…),原代码逐字不变。注释行弱化显示,一眼分得清哪行是代码、哪行是批注。缩进按原行起始列对齐,折行也不会跑到注释左边。
54
-
55
- <img src="assets/code.png" alt="选中代码:注释与原代码交替的批注清单" width="540">
31
+ ---
56
32
 
57
- ### 临时对话小窗
33
+ ## 划词解读:翻译 + "在这里"的意思
58
34
 
59
- 翻译一出来就能直接追问,不必先展开详解:
35
+ 读文档、看代码注释、翻聊天记录时,卡住的往往不是"这个词什么意思",而是**"它在这里什么意思"**。插件把**选中文字 + 它周围的内容 + 当前会话的对话背景**一起交给模型,所以答案能落到"这里"。
60
36
 
61
- - 输入框 `Enter` 发送、`Shift+Enter` 换行(中文输入法确认候选词的 Enter 不会误发)。
62
- - 回答流式出现,多轮上下文保留。
63
- - 追问可以**联网查证**,但工具日志不进小窗——只给你模型消化后的结论。
64
- - 生成中可以点**停止**打断;没有可用结果时给「重新生成」。
65
- - 关掉面板**不会丢**:点右下角胶囊原样回来,不重新请求。
37
+ - **两段式,先快后深**:点开面板**先只出翻译**(上下文收窄,首字最快);翻译出来后才出现 `↓ 展开详解`,点了才用完整会话背景做深度解读。想快就停在第一段。
38
+ - **翻译按内容自适应**:中文句子只译其中夹着的英文;中文词列义项;英文句给译文;英文单词带音标逐义项;其他语言译成中文。最后固定一条**结论条**:`> 在本句中:<它在这里的确切含义>`。
39
+ - **详解讲透"在这里"**:先判断这段文字是真实内容还是示例 / 引用,再讲专业名词的两层含义(通用定义 + 此处的具体所指)与承接关系;拿不准的会**先联网查证**再解释,事实融进解释里。
40
+ - **选中代码 → 出注释**:整篇回答是一个代码块,**注释与原代码交替**,每条语句前面一行该语言的注释符号,原代码逐字不变。
66
41
 
67
42
  <img src="assets/stage2.png" alt="展开详解后:翻译与详解两节卡片,下方可继续追问" width="540">
68
43
 
69
- ### 语音输入:不想打字就说话
44
+ ### 追问、引用、历史、升格
70
45
 
71
- 追问往往只有一两句("这词在这里是不是贬义?"),可**打字**这件事本身就要把手从鼠标挪到键盘——和"划词"这个动作是矛盾的。输入框右下角因此多了一个麦克风,**交互与主会话的语音输入完全一致**(同一套录音行、同一个波形):
72
-
73
- | 操作 | 结果 |
46
+ | 想做的事 | 怎么操作 |
74
47
  | --- | --- |
75
- | 点一下 🎤 | 工具行换成**录音行**:`✕` 取消 · **实时波形**(跟着说话强弱) · `■` 停止 |
76
- | **说话的时候** | **字就跟着往输入框里长**:半句预览(说了 1 秒多就出字,之后每 ~1.3 秒刷新)+ 停顿定稿(停 0.6 秒算这句说完,定稿的字不再变) |
77
- | 点 `■` / 到了 60 秒 | 结束录音 → 整段再识别一次做收口(这段时间显示「识别中…」+ 呼吸点) |
78
- | 点 `✕` / 录音中按 `Esc` | 取消这次语音:**松开麦克风**、实时字幕停下;**已经说出来的字留在输入框**(不删字)。`Esc` 只取消这一次语音,**面板留着**(再按一次才关窗) |
79
- | 收起小窗 / 切走这个窗口 | 停录、**松开麦克风**(不会留下"正在使用麦克风"的标识),屏幕上已有的字留着 |
80
- | 录音中把这条消息发出去了 | 停止录音(已经写进输入框的字跟着这条消息发出去) |
81
-
82
- <img src="assets/voice.png" alt="录音中的小窗:工具行是「✕ 取消 · 实时波形 · ■ 停止」,输入框里已经跟着长出了两行文字(框自己撑开了)" width="540">
83
-
84
- - **插进去,不是发出去**:识别结果落在光标处,可以和已经写了一半的话接着拼、改错字、再补第二段。识别有错字是常态,能改才有用。
85
- - **框会跟着字长**:边说边落字时输入框自己往上撑(最多约 7 行,再多就内部滚动并停在最新一行)——字是"自己长出来"的,别指望你会去拖那个框。
86
- - **实时字幕是"预览 + 定稿"两层**:说的过程中每 ~1.3 秒把没说完的半句送去识别一次,半句预览**整段替换**上一拍(窗口固定 ≤10 秒,所以说到第 40 秒也不会变慢);停 0.6 秒就算这句说完,送一次识别后**定稿**,之后不再变。点 `■` 结束时用整段录音再识别一次收口——**整段结果以已定稿的文字开头就只补后半句,定稿的字一个不动**(对不上才整块替换)。
87
- - **实时字幕只在本地识别器上默认开**:每一拍都是一次识别调用,本机模型不花钱、只占点 CPU(实测 8 秒窗口约 0.2 秒);云端识别器按次计费,不做预览。拿不到麦克风原始采样(老浏览器)、或你手动改了预览那半句,就自动退回"停止后出字"。
88
- - **音频不落盘、不进会话**:录到的音频只在这一次识别里用一下,host 转写完即丢——不会写进 `~/.dsh`,也不会出现在会话记录里。
89
- - **识别在 host 上做**(装了本地 SenseVoice 就是**在你机器上**):音频不出机器;识别器走 DSH 的语音识别服务,本机模型 / 云端服务都能用。
90
- - **出错留在录音行里说清是哪一种**:没授权 / 没麦克风设备 / 浏览器不支持 / 没启用识别服务 / 录音太长 / 没听清——各给一句原因 + 一个行内动作(🎤 重录,或模型没准备好时的 **`准备模型`** 按钮,下载在 host 上跑、关掉面板也继续)。按 `✕` 就收起、工具行原样还回来。
91
- - 一次最长 **60 秒**;需要 DSH 装着语音识别服务(`@deepseek-ai/dsh-experimental-speech-to-text`)。没有那个服务时,点麦克风会明说不可用,不会装作在录。
92
-
93
- <img src="assets/voice-inserted.png" alt="识别完成后:转写文字插进了输入框,录音行收起、🎤 回位" width="540">
94
-
95
- ### 引用:把别处的文字带进追问
96
-
97
- 追问时经常要指着一段东西问——"这段和上面那段矛盾吗"、"按这个格式再来一版"。**引用**就是把那段文字挂到输入框上,跟着这条提问一起发给模型:
98
-
99
- | 想引用什么 | 怎么操作 |
100
- | --- | --- |
101
- | **小窗里的内容**(翻译 / 详解 / 某条回答里的几句) | 直接在小窗里划选 → 浮出 `❝ 引用` → 点一下 |
102
- | **主界面上选中的文字**(小窗开着的时候) | 在页面里划选 → 同一个 `❝ 引用` → 点一下(侧边栏网页里划词也一样) |
103
- | **整条回答** | 鼠标移到那条回答上,末尾浮出 `❝ 引用整条`(网页回答会先折成 Markdown,不会引用一整页 HTML 源码) |
104
-
105
- <img src="assets/quotes.png" alt="引用:输入框上方的引用卡片、气泡里的引用块、回答末尾的「引用整条」" width="540">
106
-
107
- - **引用卡片贴在输入框上方**:来源(小窗第 2 轮回答 / 小窗「翻译」节 / 主界面选中 / 侧边栏网页…)+ 内容摘要,右侧 `✕` 单条删除。最多 4 段,重复的不会重复加。
108
- - **引用只带"模型看不到的东西"**:每段引用都写清**出自哪里**;只有材料本身不在模型上下文里时,才额外附上它当时所在的那一段对话。
109
- - **小窗里的引用**(某条回答 / 翻译节 / 详解节 / 顶部选中文字条)→ **只写出处**(`小窗第 2 轮回答`、`小窗「翻译」节`……):小窗这几轮对话本来就会随历史进模型,再贴一遍是重复;
110
- - **主界面(会话)里的引用** → 附上 `【引用处上下文】`:引用所在的**那一组对话 ± 一组**(一组 = 一条用户消息 + 它的回答,被引用的部分用 `【】` 标出),由 host 从**会话记录**里取,工具调用/思考/系统注入全部滤掉;
111
- - **侧边栏网页里的引用** → 附上网页里选区前后各 1500 字的窗口。
112
- - **发送时才拼进消息**:`【引用 1】(来自小窗第 2 轮回答)` +(需要时)`【引用处上下文】…` + `【我的问题】…` —— 出处/上下文在前、问题在后,用户气泡里也照这个顺序显示,回头翻记录知道当时在问什么。
113
- - **只挂引用、一个字不写也能发**:这时用一句兜底提问("就上面引用的文字,说说它在这里是什么意思。"),不会空发。
114
- - **只属于下一条提问**:发出即清空;换一段新的选中文字、或从「最近」回放另一段对话时也会清掉(那些引用属于上一次对话)。
115
- - **小窗开着时浮标就是「引用」**:要解读新的一段,先按 `Esc` 收起小窗再划词——避免两个小窗抢同一处选区。
116
- - **点完「✦ 解读」那行高亮会立刻收掉**(`Esc`、重开也一样):小窗顶部本来就写着选中文字,留着高亮反而碍事——
117
- 小窗亮着时它是「活的选区」,在输入框里按一下方向键就会又冒出一个「❝ 引用」,指的却是刚才解读的那段。
118
- 要引用它,重新划一次即可(那时浮标才是「❝ 引用」)。
119
- - 引用会**进历史、也进升格**:回放这段对话时引用块跟着回来(以拼好的原文形式),升格出来的正式会话里同样带着。
120
-
121
- ### 输出偏好:Markdown ↔ 网页
122
-
123
- 输入区左下角那个两格开关(`M↓` = Markdown / `▭` = 网页)决定**追问的回答用什么形式**。默认是 **Markdown**,选择记在浏览器本地,下次打开还是它。
124
-
125
- | 档位 | 行为 |
126
- | --- | --- |
127
- | **Markdown**(默认) | 普通文字 / Markdown 回答——标题、列表、表格、代码块都可以。**并不禁止 HTML**:如果某个问题用一页网页明显讲得更清楚,照样会给网页;只是不会为了"好看"把简单问题做成页面 |
128
- | **网页** | **网页优先**(不是"一律网页"):需要对比、流程、层级、图表,或文字答案会长到不好读 → 直接给**完整的单文件 HTML**,页面本身就是答案;一两段话能说清的简单问题**仍然用文字回答** |
129
-
130
- <img src="assets/web.png" alt="网页模式:回答里嵌一张可交互的页面,带「预览 / 源码」切换" width="540">
131
-
132
- **网页输出的优点**(也是它值得单独一档的原因):
133
-
134
- - **复杂信息能"看"而不是"读"**:三种方案的取舍做成对照表 + 条形图,一眼看出差别;文字版要读三段才建立同样的印象。
135
- - **页面就是答案,不是附件**:生成的是**完整单文件 HTML**(样式与脚本全内联),小窗当场渲染成可交互页面——能悬停、能点、能滚,不用保存成文件再打开。
136
- - **两个视图随时切**:回答上方有 `预览 / 源码` 两个页签,切到源码就能把 HTML 拿走。
137
- - **默认就整页展示**:预览高度**由页面内容决定**(内部不滚动),所以一页对比表能完整看下来;想收起来点右侧 **`⤡ 还原`**,回到固定高度(320px、内部滚动)。
138
- - **有设计规范兜底**:开网页模式时,插件会把自己带的 `skills/web-design/SKILL.md` 注入提示词——按小窗**约 500px 宽的窄容器**写的单列布局规范,避免生成那种"三栏卡片 + 渐变大标题"的通用 AI 模板感。(该文件读不到时有一份内置兜底,不会因此失败。)
139
- - **回答仍然可追问**:网页结果会作为前情带回下一轮,可以继续说"把第二列改成时间线"。
140
-
141
- **什么时候别用网页**:只想要一段解释、或要的是可以直接复制的文本时,Markdown 更快也更省 token——网页要生成一整份 HTML,等待时间和输出长度都更大。
142
-
143
- ### 「最近聊过的」历史
144
-
145
- 面板头部 `🕘 最近` 打开独立浮层列表(贴面板侧边,空间不够自动翻到另一侧)。重划同一个词**直接回放上次的对话,一次模型都不调**。默认保留最近 20 个划词条目(每个条目含它自己的全部追问),LRU 淘汰,**升格过的条目不会被淘汰**。
146
-
147
- 历史落在 `~/.dsh/selection-explain/history.json`,**不建会话**——不污染你的会话列表。
148
-
149
- ### 升格为正式会话
150
-
151
- 面板头部 `↗ 升格` 把这次解读(选中文字 + 翻译 / 详解 + 全部追问)变成一个正式会话,建在**来源会话同一个项目下**。临时小窗聊出价值了,就把它变成正式的。
152
-
153
- ### 悬浮状态胶囊
154
-
155
- 右下角一枚胶囊显示"最近一次划词现在什么状态"(进行中报阶段和秒数、其余报结果),点一下回到那个小窗。它会自动贴到费用胶囊上方,宽度也跟着它走(同宽同位置);**没装**费用胶囊(dsh-spend)时,宽度收到 200px —— 独处右下角时不至于横着一条。宽度不够时先省"选中的是哪个词"(省略号收尾),状态和箭头永远完整。
156
-
157
- 还能**拖动**:按住拖到别处就留在那儿(**位置记在 host 上**,刷新/重载后回到那儿;拖回右下角就清掉)(贴角那套定位不再动它,窗口缩小时也会夹回视口内);收球/展开一律以**右边缘**为锚 —— 往左收成球、往右展开回原样,右边缘一个像素都不动;拖到右下角附近会**自动吸附**回去 —— 有费用胶囊就吸附到"跟着它"的位置(在它上方、同宽),没有就吸附回右下角,吸附是滑回去的动画而不是瞬移。点一下仍然是开关小窗(拖完那一下不会误触发)。
158
-
159
- **静置收球**(只在没装费用胶囊时):超过 10 秒没有点击、也没有小窗活动,胶囊会收成一枚 32px 的小球,球心里的圆点会**长成一颗星芒**(和「✦ 解读」浮标那颗星逐点同形,缩小到 12px —— 和胶囊里文字一样高;状态颜色照旧),鼠标一放上去或点一下就展开回来;收和放是**严格互逆**的同一条动画(同一条时长 + 时间对称的曲线,把收球倒过来放就是展开,球心那个图形的长/收也在同一条曲线上)。有费用胶囊时它要和人家并排,所以不收;小窗开着、正在翻译/追问、鼠标停在上面时也都不收。
160
-
161
- ### 侧边栏网页里也能划词
162
-
163
- 侧边栏里显示的**网页**里选中文字,同样会浮出 `✦ 解读` 按钮:解释用**那个网页自己的上下文**(选区前后各 1500 字,`【】`标出选中部分),追问、历史、升格都照常。覆盖两类:
164
-
165
- | 谁渲染的网页 | 例子 |
166
- | --- | --- |
167
- | 内置「文档预览」的 HTML 文件预览 | 在侧边栏点开一个 `.html` 文件(预览模式) |
168
- | 其它插件在侧边栏渲染的生成网页 | **「图解」**(`dsh-reply-visual`:模型产出的单文件 HTML 在侧边栏渲染)等 |
169
-
170
- 为什么需要专门做一层:这些网页都渲染在不透明源沙箱 iframe 里(`sandbox="allow-scripts"`,刻意**没有** `allow-same-origin`),父页面拿不到它的文档,顶层 `window.getSelection()` 永远是空的——划词在那里天然是瞎的。所以插件往被渲染的文档里注入一段**桥脚本**:它在帧内读选区、就地采上下文,再 `postMessage` 报出来;父页面把「帧内坐标 + iframe 的位置」换算成视口坐标,复用同一套浮标与面板。
171
-
172
- | 网页形态 | 桥怎么进去 |
173
- | --- | --- |
174
- | **基础预览**(默认,`srcdoc` + `sandbox=""`,宿主已用 DOMPurify 清洗过:无脚本、无外链) | 重写 `srcdoc`:插入桥脚本,把 CSP 的 `script-src 'none'` 放宽成 `'unsafe-inline'`(`default-src 'none'` / `connect-src 'none'` 等其余限制一条不动),沙箱补 `allow-scripts` |
175
- | **交互式预览**(开发者工具打开后:外层是 `blob:` bootstrap + `document.write`) | 取回那份 blob → 插入桥脚本 → 换成一个新的 blob 装上去;桥的监听挂在 `window` 上并用轮询兜底,`document.write` 冲不掉它 |
176
- | **其它插件的生成网页**(如「图解」的 `blob:` 帧) | 同上(取回 blob → 注入 → 换新 blob),但**只桥本来就允许脚本的帧**——不给别人的沙箱加权限 |
177
-
178
- **浮现与消失的时机都与主会话一致**:
179
-
180
- | 动作 | 结果 |
181
- | --- | --- |
182
- | 拖拽划词(按住鼠标) | **期间不弹**;松手后才浮出,位置是**最终**选区末端 |
183
- | 键盘扩选(`Shift`+方向键) | 松键即弹 |
184
- | 在网页里点一下(取消选区 / 点别处) | **按下就收**(不等松手),选区真没了再补一条清空 |
185
- | 点另一个网页 | 也收(跨帧点击同样算"点了别处") |
186
- | 点主界面别处 / 滚主界面 / 改窗口大小 | 收(与主会话同一套规则) |
187
- | 页面重绘、动画、字体回流导致的**瞬时**读不到选区 | **不**收(连续 6 拍 ≈1.5s 都读不到才算真没了) |
188
- | 选区滚出网页可视区 | 收(滚回来会重新浮出) |
189
-
190
- **安全边界没有退让**:帧始终是不透明源,**不会**加 `allow-same-origin`——网页读不到 GUI 的 DOM / Cookie / localStorage,也不能导航顶层窗口。桥只往外报「选中了什么 + 周围那点文本」。给宿主预览补 `allow-scripts` 是因为那份 HTML 已被清洗成"无脚本",放宽 `script-src` 之后真正会跑的只有桥自己;别人插件的沙箱一概不碰(只桥它们自己已经开了脚本的帧)。
191
-
192
- 不想要这个行为就关掉:`bridgeSidebarPreview: false`(见下面的配置表)。
193
-
194
- > 不在覆盖范围内:**内置浏览器标签页里的外部站点**(那是远端 URL 的 iframe,插件没有任何注入手段)、以及宿主的**源码视图**(没有 iframe,切回「预览」即可)。用**路由 URL**(而非 `srcdoc`/`blob`)的第三方预览器(如 better-sidebar 自己的 HTML 预览)也不动它——那些的资源配置归它们自己管。
195
-
196
- ### 零依赖渲染
197
-
198
- 面板自带 Markdown 渲染器(段落、多级列表、小标题、表格、引用、代码块)和**语法高亮 tokenizer**(自研,零依赖):注释 / 字符串 / 数字 / 关键字 / 函数名 / 类型 / 标签 / 属性 / 变量 / 运算符分色,按围栏语言标记选规则。配色跟随**面板实际底色**判定,不是看系统偏好——所以 App 深色 + 系统浅色也不会出现深底配深字。文字对比度实测全部 ≥ WCAG AA 4.5。
199
-
200
- <!--
201
- 上面几张演示图的来源(要换图看这里):
202
- · 图由 scripts/build-demo-pages.mjs 生成演示页 —— 样式直接从 lib/client.js 抽取真实 CSS,
203
- DOM 结构与文案照 src/client/index.js 的渲染器写,所以和真实面板一致(非手绘示意图)。
204
- · 截图:Chrome 无头(--headless --screenshot --force-device-scale-factor=2)
205
- · 裁边:node scripts/crop-png.mjs <in.png> assets/<name>.png 24
206
- · 想换成真实环境截图,直接用同名文件覆盖 assets/ 下的图片即可。
207
- -->
48
+ | **追问** | 面板底部输入框直接问(`Enter` 发送 / `Shift+Enter` 换行);答案流式出现,多轮上下文保留 |
49
+ | **引用别处的文字一起问** | 小窗里、主界面上、**侧边栏网页里**划选 → 点 `❝ 引用`;鼠标悬停某条回答末尾的 `❝ 引用整条` 可引用整条(最多 4 段,卡片在输入框上方,可单条删) |
50
+ | **回到刚才那次解读** | 点右下角悬浮胶囊,或 `🕘 最近` 里挑一条——原样回来,不重新请求 |
51
+ | **把这次解读变成正式会话** | 点 `↗ 升格`(带上下文、引用与两节结论) |
52
+ | **打断生成** | 生成中点 `停止`;关窗不会丢,胶囊点回来还是原样 |
208
53
 
209
54
  ---
210
55
 
211
56
  ## 安装
212
57
 
213
- ### 方式一:从 npm 安装
214
-
215
- 已发布到 npm registry:
58
+ **从 npm**(已发布):
216
59
 
217
60
  ```bash
218
- npm install @yfwu2020/dsh-selection-explain
219
- # 或直接装配进 profile(npm 包名可直接用)
220
61
  dsh plugin --profile web add @yfwu2020/dsh-selection-explain
221
62
  ```
222
63
 
223
- ### 方式二:从 Release 安装
224
-
225
- 每个 Release 都附了**构建好的 `.tgz`**(含 `lib/` 产物,装完即可用,无需 clone 源码)。
226
- 用 `latest` 路径可以不写版本号、永远取最新:
64
+ **从 Release**(附构建好的 `.tgz`,装完即用):
227
65
 
228
66
  ```bash
229
- # 下载最新 release 的安装包(无需知道版本号)
230
67
  VER=$(curl -sI https://github.com/yfwu2020/dsh-selection-explain/releases/latest | grep -i '^location:' | sed 's|.*/tag/v||' | tr -d '\r\n')
231
68
  curl -LO "https://github.com/yfwu2020/dsh-selection-explain/releases/download/v$VER/yfwu2020-dsh-selection-explain-$VER.tgz"
232
-
233
- # 解包到插件目录
234
69
  mkdir -p ~/.dsh/plugins/dsh-selection-explain
235
- tar -xzf "yfwu2020-dsh-selection-explain-$VER.tgz" \
236
- -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
237
-
238
- # 装配到 profile(重启后由官方接管)
70
+ tar -xzf "yfwu2020-dsh-selection-explain-$VER.tgz" -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
239
71
  dsh plugin --profile web add ~/.dsh/plugins/dsh-selection-explain
240
72
  ```
241
73
 
242
- > 也可以直接下某个固定版本,把上面的 `$VER` 换成版本号即可,例如
243
- > `.../releases/download/v0.1.2/yfwu2020-dsh-selection-explain-0.1.2.tgz`。
244
-
245
- ### 方式三:从源码安装
74
+ **从源码**:
246
75
 
247
76
  ```bash
248
- git clone git@github.com:yfwu2020/dsh-selection-explain.git
249
- cd dsh-selection-explain
250
-
251
- # 装依赖(编译用的 @deepseek-ai/* 类型与 tsc 都在 devDependencies 里)
252
- npm install
253
-
254
- # 构建(自动探测 DSH 运行时;host 走 tsc,client 是手写 bundle 直接拷贝)
255
- bash scripts/build.sh
256
-
257
- # 装配
258
- dsh plugin --profile web add .
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 .
259
79
  ```
260
80
 
261
- > `scripts/build.sh` 会按顺序探测 DSH 运行时:**项目自己的 `node_modules`**(CI/`npm install` 走这条)
262
- > → `DSH_CHECKOUT` → PATH 上的 `dsh` → 常见 checkout → npx 缓存。
263
- > 探测不到时可以显式指定:`DSH_CHECKOUT=/path/to/dsh-harness bash scripts/build.sh`。
264
-
265
- 装好后**刷新页面**,选中文字试试。浏览器控制台会打印 `[dsh-selection-explain] 划词解读已就绪(选中文字试试)`。
266
-
267
- ### 卸载
268
-
269
- ```bash
270
- dsh plugin --profile web remove @yfwu2020/dsh-selection-explain
271
- ```
81
+ 装好后**刷新页面**,选中文字试试(控制台会打印 `[dsh-selection-explain] 划词解读已就绪`)。卸载:`dsh plugin --profile web remove @yfwu2020/dsh-selection-explain`。
272
82
 
273
83
  ---
274
84
 
@@ -276,360 +86,68 @@ dsh plugin --profile web remove @yfwu2020/dsh-selection-explain
276
86
 
277
87
  | 动作 | 结果 |
278
88
  | --- | --- |
279
- | **鼠标划选** / `Shift` + 方向键 | 选区末端浮出 `✦ 解读` 药丸按钮(✦ 是图标,按钮文字是「解读」;输入框内的选择不触发,避免干扰打字)。侧边栏的 HTML 文件预览里同样生效 |
280
- | 点击按钮 | 弹出面板:顶部是选中文字,正文是「翻译」卡片 |
281
- | 点 `↓ 展开详解` | 加载完整会话背景,追加「详解」卡片 |
282
- | 面板底部输入框 | 就这段文字继续追问(`Enter` 发送 / `Shift+Enter` 换行) |
283
- | 输入框右边 🎤 | **语音输入**(与主会话同一套 UI):点一下开始说,`■` 结束并把文字插进输入框、`✕` 取消(`Esc` 同样取消,一次最长 60 秒) |
284
- | 小窗开着时划词 | 浮出的是 `❝ 引用`(不是 `✦ 解读`):把这段文字挂进输入框的引用区,跟着下一条提问发给模型 |
285
- | 鼠标移到某条回答上 | 末尾浮出 `❝ 引用整条`:把整条回答挂进引用区(网页回答先折成 Markdown) |
286
- | `🕘 最近` | 打开最近划过的列表,点一条调回那段对话 |
287
- | `↗ 升格` | 把这次解读变成正式会话 |
288
- | `✕` / `Esc` | 关闭面板。**语音进行中按 `Esc` 只取消这次语音(等价于点 `✕`,已说出来的字留着),面板留着**;再按一次才关窗 |
289
- | 右下角悬浮胶囊 | 点一下:关着就回到最近一次小窗,开着就收起(来回切换,不重新请求) |
290
-
291
- **几个不别扭的细节:**
292
-
293
- - **点面板外不会关闭面板**。答案留在原地,只有 `✕` / `Esc` / 右下角胶囊能收起——不会因为手滑点一下就得重新问。
294
- - **输入框跟着内容长高**:写(或说)到第二行就自己撑开,最多约 7 行(132px);再长就内部滚动并**自动停在最新一行**——不会像以前那样第二行以下的字被裁掉。
295
- - **开窗即能打字**:小窗打开、输入框出现后会自动获得焦点(只有"你正在别处打字"时才不抢)。`Tab` 也能摸到输入框和 🎤。
296
- - **说话时看得见光标在跳**:录音一开始光标就落在输入框里,预览每落一句、光标跟到最新文字末尾——像个正在打字的人,而不是一坨自己冒出来的字。
297
- - 关闭时**会中止正在飞的请求**,并记成「已停止」(不是失败);内容都留着,点胶囊回来还是原样。
298
- - 拖动面板:抓头部即可拖动。
299
- - 追问时消息区**贴底跟随**;你主动往上翻就不会被拽回去。
300
- - `🕘 最近` 的列表点别处就收(不连带关面板),但 `Esc` 会把面板和它一起收。
301
- - **引用是"下一条提问"的**:发出去就清空;引用卡片上的 `✕` 可以单条撤掉;最多 4 段。
302
-
303
- ### 调试钩子
304
-
305
- 浏览器控制台里可以直接驱动,方便验证渲染链路:
306
-
307
- ```js
308
- window.__dshSelectionExplain.open('the migration ran long', 'Dev: the migration ran long, so we ship Wednesday.')
309
- window.__dshSelectionExplain.state() // { phase, chars, model, sections }
310
- window.__dshSelectionExplain.ping() // 当前模型路由与限制
311
- window.__dshSelectionExplain.bridge() // 侧边栏网页划词桥:{ on, frames:[{connected,own,srcdoc,sandbox}], active, selection }
312
- window.__dshSelectionExplain.selection() // 当前选区:{ text, source:'document'|'iframe', label, context }
313
- window.__dshSelectionExplain.quotes() // 待发送的引用:[{ id, label, text, context, contextChars, session }]
314
- window.__dshSelectionExplain.quote('某段文字', '主界面选中') // 手工加一段引用(等价于点浮标)
315
- window.__dshSelectionExplain.compose() // 引用 + 提问拼出来的那条消息原文(发给模型的就是它)
316
- window.__dshSelectionExplain.quoteState() // 引用浮标:{ visible, selection:{text,label,source}, panelOpen }
317
- window.__dshSelectionExplain.voice() // 语音输入:{ phase, capture, activity, dot, waveform, stop, action, bars, lastText, catalog, live:{ active, committed, preview, passes, rewritten, tap } }
318
- window.__dshSelectionExplain.voiceTick() // 手动催一拍实时字幕(测试用,不用等 1.3 秒节流)
319
- window.__dshSelectionExplain.voiceNode() // 🎤 触发键节点(无头环境里可能挂了两棵树,用它拿真正带监听的那个)
320
- window.__dshSelectionExplain.voiceNodes() // 录音行的各部件:{ row, cancel, wave, activity, action, stop }
321
- window.__dshSelectionExplain.askValue() // 读追问输入框(传参即写入),用来验"转写文字插进去了"
322
- ```
89
+ | 鼠标划选 / `Shift`+方向键 | 选区末端浮出 `✦ 解读`(输入框内的选择不触发;侧边栏的 HTML 预览里同样生效) |
90
+ | 点 `✦ 解读` | 弹出面板:顶部是选中文字,正文是「翻译」卡片 |
91
+ | `↓ 展开详解` | 用完整会话背景追加「详解」卡片 |
92
+ | 小窗开着时划词 | 浮出的是 `❝ 引用`(要解读新的一段,先 `Esc` 收起小窗) |
93
+ | 输入框右边 🎤 / 划完直接开麦 | 见上面「语音」一节 |
94
+ | `✕` / `Esc` | 关面板。**语音进行中按 `Esc` 只取消这次语音**(已说出来的字留着),面板留着;再按一次才关窗 |
95
+ | 右下角悬浮胶囊 | 点一下:回到最近一次小窗 / 收起 |
323
96
 
324
- ### HTTP 接口
325
-
326
- host 半注册了同源路由,可以单独调用:
327
-
328
- | 路由 | 说明 |
329
- | --- | --- |
330
- | `POST /selection-explain/api/analyze` | 选中文字 + 上下文 → **SSE 流式**返回翻译与详解 |
331
- | `GET /selection-explain/api/ping` | 健康检查:当前模型路由、各阶段推理档位、限制 |
332
- | `GET /selection-explain/api/models` | 可选模型列表(面板里的模型选择器用;带 `sessionId` 时 `current` 按该会话解析) |
333
- | `GET/POST /selection-explain/api/settings` | 小窗设置页:`GET` 下发 `{ groups, values, defaults }`(字段声明即校验边界),`POST { values }` 合并写入、`{ reset: true }` 恢复默认 |
334
- | `GET /selection-explain/api/route` | 极轻的"这一轮用哪个模型":`?sessionId=…` → `{ provider, model }`(设置页与模型胶囊按秒轮询它;带会话级短缓存,见下面「配置」一节) |
335
- | `GET/POST /selection-explain/api/history` | 小窗对话历史的读 / 写 / 删除 |
336
- | `POST /selection-explain/api/quote-context` | 引用上下文:`{ sessionId, text }` → 该引用所在的**一组对话 ± 一组**(`{ matched, context, rounds }`;定位不到时 `matched:false`,客户端退回自己采的局部上下文) |
337
- | `POST /selection-explain/api/promote` | 升格为正式会话 |
338
- | `GET /selection-explain/api/speech` | 语音输入:识别器目录与就绪状态(`{ available, providers:[{id,phase}], limits:{maxSeconds,maxBytes} }`) |
339
- | `POST /selection-explain/api/speech/transcribe` | 一段 **16kHz 单声道 PCM16 WAV**(base64)→ 文字:`{ audioBase64, language?, providerId? }` |
340
- | `POST /selection-explain/api/speech/prepare` | 显式准备本地识别模型(首次要下载,只在用户点了提示里的「准备模型」时调用) |
341
-
342
- 转写示例(`SPEECH_WAV` 是一段规范 WAV):
343
-
344
- ```bash
345
- node -e 'const fs=require("fs");const b=fs.readFileSync(process.argv[1]);process.stdout.write(JSON.stringify({audioBase64:b.toString("base64")}))' /tmp/v.wav > /tmp/v.json
346
- curl -s -X POST http://127.0.0.1:3080/selection-explain/api/speech/transcribe \
347
- -H 'content-type: application/json' --data-binary @/tmp/v.json
348
- # → {"ok":true,"text":"这是一段语音输入的测试…","providerId":"sensevoice-local",...}
349
- ```
350
-
351
- 健康检查示例:
352
-
353
- ```bash
354
- curl -s http://127.0.0.1:3080/selection-explain/api/ping
355
- ```
97
+ 几个不别扭的细节:**点面板外不会关闭面板**(答案留在原地,只有 `✕`/`Esc`/胶囊能收);输入框**跟着内容长高**(最多约 7 行,再长内部滚动并自动停在最新一行);开窗**自动聚焦**输入框(只有你正在别处打字时才不抢)。与「完整视图」插件同装时,`Esc` 会按"最后操作的那个窗"逐层退出,一次只退一层。
356
98
 
357
99
  ---
358
100
 
359
- ## 档位兜底:「关」档不被支持时自动降级
101
+ ## 设置
360
102
 
361
- 有些模型**不接受关闭推理**(上游直接 `400 invalid_request_error`)。首轮默认就是 `off`,
362
- 撞上这种模型时整个划词解读会全挂。
103
+ 在面板里点 ⚙(或在设置页)改,改完**自动保存**;也能写 `cordis.patch.yml`(键名与默认值见 `src/settings.ts`)。常改的就几项:
363
104
 
364
- 处理方式:
105
+ - **模型**:默认「跟随主会话」,也可单独指定(首轮 / 次轮 / 追问共用这一个模型)。
106
+ - **思考强度**:三个阶段各自一格 —— `关 / 低 / 高 / 最高`。
107
+ 各家的档位名字并不统一(`off>low>high>max`、`low>high>max`、`off>minimal>…>high`、只有 `max`…),所以插件**每次都按当前模型自己声明的档位现算**,把你这四格落到它真实拥有的档上(悬停格子能看到"该模型发 xxx",设置页那行也会实时写出四格的落点)。**小窗里的「推理等级(追问档)」是同一套四格、同一份数据**,两边改哪边都一样。模型不吃某一档时会自动换档重试并记住结论,下次不再撞。
108
+ - **背景 / 工具 / 上限**:是否附带会话消息、引用是否带上文、能不能联网(`advanced_search`、`platform_search`)、超时与频率上限——都有默认值,不改也能用。
109
+ - **界面 · 语音输入**:上面说的两个收尾开关、右下角胶囊、侧边栏网页划词桥(`bridgeSidebarPreview`,默认开)。
365
110
 
366
- - 撞到一次就**记住这个 provider/model**(`${DSH_HOME:-~/.dsh}/selection-explain/leaky-off.json`
367
- 的 `noOff` 字段),改用 **`low`**(最接近"关"的可接受档位)重试同一轮,并推一条 notice 说明。
368
- - **下次不再尝试 `off`** —— 直接发 `low`,不再先失败一轮(省一个来回的时延,也不闪报错)。
369
- - 重试只在"这一轮一个字都还没吐出来"时进行:已经出正文的失败不重试,避免把半截答案丢掉。
370
- - 兜底覆盖**三种轮次**(首轮 / 详解 / 追问)。以前只对追问生效、而且要客户端显式传了档位才触发,
371
- 所以首轮撞上这种模型时无人兜底。
372
-
373
- ## 拖动与改大小
374
-
375
- 小窗可以拖标题栏挪位置,拖右下角(或右/下边缘)改大小。
111
+ ---
376
112
 
377
- **位置和尺寸是两回事,只有尺寸会记住:**
113
+ ## 隐私
378
114
 
379
- - **位置不记**。每次开窗都按选区重新锚定:落在选区下方,下方放不下就翻到选区上方,
380
- 再夹进视口。拖动只对这一次有效,关掉重开就回到划词的地方 —— 这是刻意的设计。
381
- - **尺寸记**。拖角调过的大小存在 host 上(`${DSH_HOME:-~/.dsh}/selection-explain/panel.json`,
382
- 接口 `GET/POST /selection-explain/api/panel`,载荷只有 `{w,h}`),
383
- 刷新、换标签页、换浏览器 profile 都回到那个大小。
384
- - **双击右下角角标**恢复默认尺寸(清掉存档)。
115
+ - **语音**:音频不落盘、不进会话;插件不读输入法的转写结果;不做边录边出的流式上传(说完一次给全,识别在 host 上)。
116
+ - **会话背景**:可以关(`sessionContext: false`);开启时只带你选中文字所在的消息及其之前若干条,工具调用 / 思考 / 系统注入都会被滤掉。
117
+ - **联网**:只在你启用的工具范围内查证;工具日志不进小窗,只给结论。
118
+ - **本地数据**:解读历史 `~/.dsh/selection-explain/history.json`;"哪些模型不吃关档 / 关档会泄漏"的记忆 `leaky-off.json`。都是明文,随时可删。
385
119
 
386
- 其余约定:
120
+ ---
387
121
 
388
- - 尺寸下限 320×240,且始终夹在视口内(窗口缩小也不会超出)。上限是视口大小。
389
- - 用户拉过大小后,设置页那条约 560px 的高度下限就**不再插手**(`data-resized` 标记)。
390
- - 改动过程中不写盘,松手后防抖 300ms 再存 —— 不然每帧一个请求。
391
- - 改大小用 **pointer 事件**(不是 mouse):触控板/触屏一样能拖,并且 `setPointerCapture`
392
- 保证鼠标划出窗口也不会丢掉这一轮。
393
- - 只有**真的拖动过**才会触发存档逻辑(点一下角标不算改了大小)。
122
+ ## 常见问题
394
123
 
395
- ## 配置
124
+ **选中了没反应?** 输入框里的选择不触发(避免干扰打字);另外面板**不跟随页面滚动**(滚动即收起),符合"划完即看"。侧边栏的**源码视图**里没有 iframe,切回「预览」即可。
396
125
 
397
- ### 方式一:在小窗里改(推荐)
126
+ **结论可靠吗?** 来自你配置的模型。专业领域术语请以人工判断为准;拿不准时它会联网查证,但也会出错。
398
127
 
399
- 小窗头部有一枚 **⚙ 设置**(和「最近」「升格」同款的文字键),点开是一个从右侧滑入的抽屉,
400
- 六个分类按任务分组:
128
+ **为什么首轮只有翻译?** 故意的:先用最窄的上下文抢首字速度,想要深度再点 `↓ 展开详解`。
401
129
 
402
- | 分类 | 管什么 |
403
- | --- | --- |
404
- | 模型 | 一个模型选择,首轮、次轮与追问各自的思考强度 |
405
- | 解读 | 选中文字与上下文的长度上限、限流、超时(秒) |
406
- | 背景 | 解读附带的会话消息数、引用上下文的轮数与字符上限 |
407
- | 联网 | 联网开关、检索轮数、工具白名单、文件读取边界 |
408
- | 界面 | 侧边栏划词桥、右下角悬浮胶囊、静置收球时长(秒) |
409
- | 数据 | 保留最近对话数量,每条包含所选文字、模型回答及后续追问 |
410
-
411
- - **改完即时生效,无需重启。** 改动会**自动保存**(防抖 400ms),底部那行只有脏了才报
412
- 「未保存 · N 项」,不弹 toast、不打断。
413
- - **模型是一个下拉**,不用自己拼 provider 与 model:选项来自 host 已有的模型清单
414
- (`GET /selection-explain/api/models?sessionId=…`,含每个 provider 下的全部模型)。
415
- 写配置时展开成 `provider` + `model` 两个字段。
416
- - **「跟随主会话」是名副其实的**:不选具体模型时,插件按**小窗所属会话**的模型解析路由 ——
417
- 从会话原始事件里取最后一条 `model/selection`(`sessionModelOf`),
418
- 所以两个会话用不同模型时各用各的,下拉里「(当前:…)」报的也是本会话那一个。
419
- 会话没记过模型选择时落回 DSH 的全局默认模型。优先级:
420
- 小窗与设置页的统一选择 → **本会话的模型** → 全局默认 → 第一个可用。
421
- - **「小窗所属会话」= 划词那一刻的会话**(`panelSessionId`),不是"当前正在看的会话"。
422
- 新版 DSH 从 `uiSession.adapter.current` 的主视图绑定读取会话 ID;会话目录不再提供 `current`。
423
- 小窗开着时切走会话,解读 / 追问 / 升格与模型解析仍按原来那个会话算 ——
424
- 否则背景会取错会话,模型也会跟着变。
425
- 模型清单请求使用这个会话 ID,并保留各自的响应快照,避免切换时较慢的旧请求覆盖新会话。
426
- - **模型名是实时的(小窗开着就在跟)**:**小窗或设置抽屉任意一个开着**时按 2 秒轮询那条轻量接口
427
- `GET /selection-explain/api/route?sessionId=…`,主会话里换了模型,设置页括号里和**小窗输入框那枚
428
- 模型胶囊**自己就跟着变,不用关掉重开(`/models` 要列全部模型、`/ping` 要枚举工具表,都不适合按秒轮询)。
429
- 两个都收起来即停。host 侧这条接口带**会话级短缓存**:live 会话的事件数(`seq`)没涨就直接回上次的结论,
430
- 涨了也至少隔 3 秒才重读一次 —— 否则"小窗一直开着"会变成每 2 秒把整份会话日志重放一遍
431
- (实测 1.5MB 的会话一次 80~125ms)。用户动作路径(解读 / 追问 / 升格 / 模型清单 / ping)不吃这份缓存,
432
- 拿到的永远是此刻那一份。
433
- - **胶囊上显示的是模型名,不是 id**。跟随态也一样:开窗时顺手拉一次模型清单(60 秒缓存),
434
- 按 **provider + model 两个键**换成清单里的显示名 —— 同一个 model id 在多个 provider 下是不同模型、
435
- 不同名字(实测 `deepseek-flash` 在 opencode-go 叫「DeepSeek Flash」、在 deepseek-official /
436
- deepseek-account 叫「DeepSeek-V41-Flash」),只按 id 查会张冠李戴。清单还没回来时先显示 id,
437
- 回来后再画一次;provider 全名在胶囊的 hover 提示里。
438
- - **输入框那枚胶囊点开的模型列表里,当前在用的那个会打勾**(跟随态也一样,并在名字后面带一枚
439
- 「跟随」小标 —— 说明这个勾来自主会话、不是你选的);下面「推理等级」列的是**在用模型**声明的
440
- 档位。名字过长时收窄名字显示省略号,保证一行里放得下 勾 + 名字 + 小标 + provider。
441
- - **首轮、次轮和追问采用同一个模型**。小窗输入框与设置页双向同步,任一处修改均自动保存。
442
- 旧版独立追问配置与浏览器模型缓存不再覆盖这一选择;三个阶段的思考强度仍分别设置。
443
- - **「恢复默认」立即生效**:恢复各项配置,同时更新模型选择、控件禁用状态与悬浮入口。
444
- - **时间一律按「秒」填**。配置里仍是毫秒(schema 与既有 yml 值不动),
445
- 换算由 spec 上的 `scale: 1000` 声明 —— 边界的唯一事实来源还是 Config schema。
446
- - 写完落在插件自己的目录,**不碰 `cordis.patch.yml`**:
447
-
448
- ```
449
- ${DSH_HOME:-~/.dsh}/selection-explain/settings.json
450
- ```
451
-
452
- 只存**改过的键**;「恢复默认」就是把它清空,回到下面表里的 yml/出厂值。
453
- 那个 yml 是**装配清单**,让 UI 去改它既要人肉重启、又可能把 YAML 结构写坏,所以不碰。
454
-
455
- > 字段的标题 / 说明 / 边界由 host 通过 `GET /selection-explain/api/settings` 下发,
456
- > 客户端只负责画(见 `src/settings.ts`)。这样 UI 的边界和 host 的 clamp 不会各写一份、久了互相漂。
457
-
458
- ### 方式二:写 `cordis.patch.yml`
459
-
460
- 配置写在 profile 的 `cordis.patch.yml` 里同 id 的行上(`dsh plugin add` 装好后,包自带的 `cordis.patch.yml` 里已有可改的样板):
461
-
462
- ```yaml
463
- - insert:
464
- - id: selection-explain
465
- name: '@yfwu2020/dsh-selection-explain'
466
- config:
467
- reasoningEffort: high
468
- sessionContextMaxMessages: 24
469
- ```
470
-
471
- **下表默认值取自 `src/index.ts` 的 `Config` schema,也就是推荐值。** 不填即用默认。
472
-
473
- ### 模型与推理
474
-
475
- | 字段 | 默认 | 说明 |
476
- | --- | --- | --- |
477
- | `provider` | `''` | 固定 provider 路由;留空 = 跟随「设置 → 默认模型」 |
478
- | `model` | `''` | 固定 model id;留空 = 跟随主会话 |
479
- | `reasoningEffort` | `high` | **详解**阶段推理档位(`off` / `low` / `high` / `max`) |
480
- | `translationReasoningEffort` | `off` | **首轮**翻译、解读或注释单独一档,默认关闭思考 |
481
- | `chatReasoningEffort` | `high` | **追问**档位(追问要准不要快;想更快可调 `low`) |
482
- | `temperature` | `-1` | 采样温度。**`-1` = 不传**,交给供应商默认(与主会话一致);`0`~`2` 之间才真的传 |
483
- | `maxTokens` | `0` | 单次输出上限(tokens)。**`0` = 不限制**。填正数可控成本 / 控时延 |
484
- | `timeoutMs` | `300000` | 单次调用超时(含工具轮;客户端断开即中止上游) |
485
-
486
- > 三个阶段的思考强度可分别设置。默认 `translationReasoningEffort: off` 优先让首轮快速回答;代码中的详解初始值为 `reasoningEffort: high`,安装配置为 `off`,追问默认 `high`。
487
-
488
- 输出上限、采样温度和结果缓存不在设置页展示;既有底层配置继续兼容。
489
-
490
- ### 上下文与缓存
491
-
492
- | 字段 | 默认 | 说明 |
493
- | --- | --- | --- |
494
- | `maxSelectionChars` | `4000` | 选中文字长度上限(超了不出现按钮,host 侧也会拦截并给出明确报错) |
495
- | `maxContextChars` | `12000` | 局部上下文上限(选区前后各取一段窗口) |
496
- | `sessionContext` | `true` | 是否注入当前会话的对话背景(判断指代与前提的关键) |
497
- | `sessionContextFastMessages` | `8` | **首轮翻译**用的背景条数;越小首字越快 |
498
- | `sessionContextMaxMessages` | `24` | **详解 / 追问**用的背景条数(自选中文字所在消息向上取) |
499
- | `sessionContextMaxChars` | `0` | 会话背景字符上限;**`0` = 不限**(只按条数取窗口) |
500
- | `quoteContextRounds` | `1` | **引用**带几组上下文:引用所在那一组对话 ± 这么多组(一组 = 用户消息 + 它的回答) |
501
- | `quoteContextMaxCharsPerTurn` | `2000` | 引用上下文里单条消息的上限(超了**围绕引用**截断,不裁掉引用本身) |
502
- | `quoteContextMaxChars` | `6000` | 引用上下文整段上限(超了先丢最早的、再丢引用之后的) |
503
- | `resultCacheTtlMs` | `600000` | 共享结果缓存 TTL(毫秒);`0` = 关闭 |
504
- | `historyMaxEntries` | `20` | 保留最近对话数量;`0` = 不保存新对话,已有对话保留 |
505
- | `maxRequestsPerMinute` | `40` | 本地限流,防误触发刷爆额度 |
506
- | `bridgeSidebarPreview` | `true` | 侧边栏 HTML 预览里的划词桥。关掉 = 预览网页里选中文字不再弹按钮(预览帧不再补 `allow-scripts`,也不注入桥脚本) |
507
- | `chatProvider` | `''` | 旧版兼容字段,不再参与模型选择 |
508
- | `chatModel` | `''` | 旧版兼容字段,不再参与模型选择 |
509
- | `pillEnabled` | `true` | 右下角悬浮胶囊(最近一次划词的状态入口)。关掉 = 右下角不再出现胶囊 |
510
- | `pillIdleMs` | `10000` | 静置多久把胶囊收成小球(毫秒,2000~600000)。只在**没装费用胶囊**时生效(有费用胶囊时两者并排,收球会让它们错位) |
511
-
512
- ### 工具(联网 / 文件)
513
-
514
- | 字段 | 默认 | 说明 |
515
- | --- | --- | --- |
516
- | `tools` | `true` | 是否允许解读过程调用工具 |
517
- | `toolNames` | `advanced_search,platform_search,read,grep,glob` | 工具白名单(逗号分隔)。默认用 free-search 插件的搜索工具,支持多引擎、时间过滤、平台搜索 |
518
- | `fallbackToolNames` | `web_search` | **备用**清单:只在首选里的联网工具一个都不在时才补进来(没装 free-search 时兜底) |
519
- | `maxToolRounds` | `2` | 最多几轮工具调用(用完会补一轮不带工具的收尾,避免"转半天没回答") |
520
- | `toolResultMaxChars` | `3000` | 单个工具结果注入模型前的字符上限 |
521
- | `toolReadRoot` | `''` | 文件类工具(`read`/`grep`/`glob`)的读取边界:`''` = 会话工作目录,`'*'` = 不限制,其他 = 指定目录。越界在**执行前**被拒 |
130
+ **一定要装语音相关的服务吗?** 不用。没装时只是 🎤 与语音卡明说不可用,划词解读一切照常。
522
131
 
523
132
  ---
524
133
 
525
- ## 工作原理
526
-
527
- ```
528
- 浏览器(client half,挂在官方 shell.overlay 槽 —— 帧级浮层,不遮挡 App)
529
- ① 监听 selection(mouseup / Shift+方向键)→ 在选区末端定位浮标
530
- ② 点击后采集三段信息:
531
- · 局部上下文:选区所在容器文本中,选区前后各 1500 字的窗口,【】标出选中部分
532
- · 所在位置:容器类型(对话消息 / 代码块 / 表格 / 页面内容)+ 页面标题与地址
533
- · 当前会话 id
534
- ③ POST /selection-explain/api/analyze → SSE 边收边渲染
535
- ↓
536
- 宿主(host half,进程内)
537
- ④ 解析模型路由:默认取「设置 → 默认模型」
538
- ⑤ 会话背景:按会话 id 读该会话的真实对话(工具调用 / 系统提示 / harness 注入内容全部剔除),
539
- 从最新往前取到预算上限,并把选中文字所在的那条标 ▶
540
- ⑥ 组两节提示词 → ctx.llm.stream(...) → text-delta 直接透传 SSE
541
- (reasoning-delta 另走 thought 事件,只喂"正在想什么"那一行,不参与正文)
542
- ```
543
-
544
- **几个设计取舍:**
134
+ ## 已知限制
545
135
 
546
- - **不建会话**:小窗对话落在插件自己的 `history.json` 里。会话是重型对象,高频功能那样做只会堆死会话列表。
547
- - **缓存 key 不含选中文字之后的内容**:否则主会话后面追加新消息时,同一个词的 key 就变了,表现为"找不着上次的小窗、又生成一个"。
548
- - **上下文窗口锚定选中文字所在的那条消息**,向上取——不是简单取最近 N 条,这样"这个词在这段对话里什么意思"才有前提。
549
- - **侧边栏预览里的选区由帧内桥上报**:那段上下文是在**帧内**用同一套算法(容器 ≥120 字、前后各 1500 字、`【】`标记、key 只取选中前 300 字)算出来的,所以侧边栏网页里的解释质量与主会话里一致,而不是退化成"只有选中文字、没有上下文"。
136
+ - 只覆盖 **DSH 界面内**(含侧边栏 HTML 预览)的选中文字;**内置浏览器标签页里的外部站点**(远端 URL 的 iframe,没有任何注入手段)与**源码视图**不触发。
137
+ - 用**路由 URL**(而非 `srcdoc`/`blob`)的第三方预览器不接管(不动别人的资源改写与生命周期);小窗里自己生成的网页预览(iframe 内部)也不划词——想引用它就用末尾的 `❝ 引用整条`。
138
+ - 语音卡依赖 `macOS 14.4+` 的麦克风探针;🎤 语音输入需要宿主装有语音识别服务(如本机 SenseVoice)。
139
+ - 依赖 DSH 的 `webServer` 与 `llm` 服务;宿主过旧可能不兼容(见 `package.json` 的 peerDependencies)。
550
140
 
551
141
  ---
552
142
 
553
- ## 自检与开发
554
-
555
- ```bash
556
- npm run build # 构建(= bash scripts/build.sh)
557
- npm test # 600+ 条断言(filters / prompt / bridge / smoke / client / transcript / guard / speech / voice)
558
- npm run test:smoke # 只跑划词桥的真浏览器冒烟(本机 Chrome;找不到自动 SKIP)
559
- npm run test:voice # 只跑语音输入的真浏览器冒烟(Chrome + 假麦克风 + 真识别;缺东西自动 SKIP)
560
- npm run typecheck # tsc --noEmit
561
- ```
562
-
563
- | 脚本 | 用途 |
564
- | --- | --- |
565
- | `scripts/test-client.mjs` | 无浏览器集成测试:真实 host 路由 + 最小 DOM 桩,覆盖槽注册 → 划词浮标 → 点击 → SSE 流式渲染 → 两节内容 → 侧边栏网页划词桥(注入 / 来源校验 / 坐标换算 / 帧内清空)→ 清理(host 不在跑时自动跳过在线断言) |
566
- | `scripts/test-bridge.mjs` | **划词桥单测**(无需宿主):从 bundle 里抠出桥脚本与注入函数单独跑 —— 插在哪、CSP 只放宽哪一条、沙箱补哪个 token、帧内报出来的文字 / `【】`上下文 / `keyContext` / 坐标对不对 |
567
- | `scripts/smoke-bridge.mjs` | **划词桥真浏览器冒烟**(本机 Chrome,找不到就 SKIP):起临时 http 服务,用两种预览形态(`srcdoc` / `blob` bootstrap)验沙箱 + CSP + 跨不透明源 postMessage + `document.write` 冲不掉监听 |
568
- | `scripts/test-prompt.mjs` | **提示词契约测试**:把两节结构标题、结论条格式、音标规则、详解来源判断、追问网页模式要求等关键约束固化成断言,误删即 CI 红 |
569
- | `scripts/test-filters.mjs` | host 侧流式过滤器单测(思考泄漏、工具调用残渣等) |
570
- | `scripts/test-transcript.mjs` | 会话背景窗口的离线测试(锚点 / 条数 / 不截断 / 噪音剔除 / 退化路径) |
571
- | `scripts/test-guard.mjs` | 文件工具读取边界的路径校验 |
572
- | `scripts/test-speech.mjs` | **语音输入契约 + 真路由**:`validateWave` 逐字段打表(采样率/声道/位深/长度字段/多余 chunk 全都要拒)+ 真 host 的三个语音路由(坏 base64 / 非规范 WAV / 超大音频 / 非 POST / 正弦波不会"听"成话);给了 `SPEECH_WAV=/tmp/v.wav` 还会真识别一句 |
573
- | `scripts/smoke-voice.mjs` | **语音输入真浏览器冒烟**:headless Chrome + `--use-file-for-fake-audio-capture` 把一段真人语音当麦克风,页面里跑**真的 lib/client.js**,走完 录音 → MediaRecorder → 重采样 → WAV → 真 host 识别 → 插进输入框;顺带断言浏览器产出的 WAV 能过 host 的逐字段校验(缺 Chrome / 缺 `say` / host 没在跑都自动 SKIP) |
574
- | `scripts/dump-prompt.mjs` | 打印实际注入的提示词(system + user + 各段尺寸),排查"模型到底看到了什么" |
575
-
576
- ```bash
577
- # 看某次解读到底喂了什么
578
- node scripts/dump-prompt.mjs <sessionId> "<选中文字>"
579
- node scripts/dump-prompt.mjs --no-session "<选中文字>" # 不带会话背景的对照组
580
- ```
581
-
582
- ## 发布(维护者)
583
-
584
- 发布走 **Trusted Publishing**(GitHub OIDC),**不需要任何长期 npm token**:
143
+ ## 开发者
585
144
 
586
145
  ```bash
587
- # 1) 改 package.json 的 version(例如 0.2.0)
588
- # 2) 打同名 tag 推上去
589
- git tag v0.2.0 && git push origin v0.2.0
146
+ npm test # 单测 + 真浏览器冒烟(本机 Chrome;找不到自动跳过)
147
+ bash scripts/build.sh # host 走 tsc,client 是手写 bundle 直接拷贝到 lib/
590
148
  ```
591
149
 
592
- `.github/workflows/publish.yml` 会自动:校验 tag 与 `version` 一致 → `npm install` → 构建 → 跑全量测试(`npm test`,含真浏览器冒烟,CI 里自动 SKIP)→ 类型检查 → `npm publish --provenance`(附 provenance 签名)。
593
-
594
- npm 侧只需配一次:包设置 → **Trusted Publisher** → GitHub Actions,填 `yfwu2020` / `dsh-selection-explain` / `publish.yml`,并勾选允许 **`npm publish`**(默认只允许 `npm stage publish`)。
595
-
596
- > 注意:`devDependencies` 里的 `@deepseek-ai/*` 钉在 **`0.1.5-rc.3`**(CI 构建校验用的就是这份类型)。
597
- > npm 上这些包的 `latest` 还停在 `0.0.1-rc.x`,API 更旧(缺 `WebServer`、`createSystemMessage` 等),
598
- > 用 `latest` 会编译失败——升级运行时时要同步改这里。
599
- >
600
- > **本机运行时通常比它新**(如 `0.1.7-rc.2`),两者 API 会漂移:例如 `createSystemMessage` 在
601
- > `0.1.5-rc.3` 是 `(text, plugin)` 两个必填参数,在 `0.1.7-rc.x` 只剩 `(text)`。此时**别只按本机类型写**,
602
- > 走一层宽松别名兼容两边(见 `src/index.ts` 的 `systemMessageOf`)——v0.4.0 就是漏了这一步,
603
- > 本机构建全绿、CI 构建报 TS2554,整条发布卡在第一步(与 Trusted Publisher 无关)。
604
- > 想提前发现:把仓库复制到临时目录、`npm install --no-save`(会装到 `0.1.5-rc.3`)再 `bash scripts/build.sh`。
605
-
606
- ## 目录
607
-
608
- ```
609
- src/index.ts host 半:SSE 路由 / 模型调用 / 会话背景 / 历史落盘 / 过滤器
610
- src/prompts.ts 提示词与写作规则(纯字符串常量,无逻辑)
611
- src/client/index.js client 半:划词浮标 + 面板 + 渲染器(手写 ModuleLoader bundle,无打包器)
612
- scripts/ build.sh + 9 个离线测试 + 3 个浏览器冒烟 + dump-prompt + 演示图生成
613
- skills/web-design/ 网页模式注入的设计规范(运行时读取)
614
- assets/ README 的演示图(由 scripts/build-demo-pages.mjs 生成)
615
- cordis.patch.yml 官方装配用的 bundle patch
616
- lib/ 构建产物(已 gitignore,克隆后需 npm run build)
617
- ```
618
-
619
- > **`docs/` 不在本仓库中**:开发期的资料(UI 比选实验台——浮标动效 / 模型选择器 / 折叠实验台 /
620
- > 配色方案等,以及含本机路径与逐轮实测记录的内部开发文档)**仅保留在开发者本地**,已在 `.gitignore`
621
- > 中整体排除,也从未进入 npm 包。它们记录的是"当初为什么这么设计",与插件运行无关。
622
-
623
- ## 已知限制
624
-
625
- - 主会话(以及 DSH 界面本身)里只覆盖**同源页面内**的选中文字。
626
- - 侧边栏 **HTML 文件预览**已覆盖(靠帧内桥,见上);但**内置浏览器标签页里的外部站点**(远端 URL 的 iframe,插件没有任何注入手段)与**源码视图**(没有 iframe)不触发。
627
- - 预览帧被换成路由 URL 而非 `srcdoc`/`blob` 的第三方预览器不接管(不动别人的资源改写与生命周期)。
628
- - 面板**不跟随页面滚动**(滚动即收起浮标 / 面板),符合「划完即看」的一次性使用预期;侧边栏网页内部滚动会重新上报位置,浮标跟着走。
629
- - **小窗里的网页回答预览(iframe)内部**划不了词(不透明源帧,插件不桥自己的预览):想引用它就用气泡末尾的 `❝ 引用整条`,或切到「源码」视图再划。
630
- - 结论来自当前模型,专业领域术语请以人工判断为准。
631
- - 依赖 DSH 的 `webServer` 与 `llm` 服务;宿主版本过旧可能不兼容(peerDependencies 见 `package.json`)。
632
- - **语音输入**还需要宿主的语音识别服务(`@deepseek-ai/dsh-experimental-speech-to-text` + 一个识别器,例如本机 SenseVoice)。没装 / 没准备模型时,麦克风按钮会明说原因,不会静默失败。识别器只收 **16kHz 单声道 PCM16 WAV**(插件在浏览器里按这个格式编码);一次最长 60 秒,**不做边录边出的流式识别**(说完再转,一次给全)。
150
+ 浏览器控制台自带自检钩子:`__dshSelectionExplain.state()` 看状态、`voiceCard()` / `voice()` 看语音、`bridge()` 看侧边栏划词桥、`selection()` 看当前选区。HTTP 接口(`/selection-explain/api/*`)、实现细节与踩坑记录见 [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) 与源码注释 —— 那份文档随本机开发走,刻意不进发布包。
633
151
 
634
152
  ## License
635
153