dsh-quote-shot 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +341 -2
- package/client.js +755 -0
- package/cordis.patch.yml +7 -0
- package/index.js +453 -0
- package/install.mjs +103 -0
- package/package.json +36 -4
- package/session-reader.js +269 -0
- package/test.mjs +282 -0
- package/verify.mjs +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Twelve212
|
|
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,342 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-quote-shot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**按 id 引用整条消息**:点助手消息下方的引号图标,输入框里只多一行
|
|
4
|
+
`[引用消息 <id>——请先调用 quote_read 读取全文,再回答]`,
|
|
5
|
+
模型按需调 `quote_read` 读全文——**不把几百字塞进 prompt**。
|
|
6
|
+
|
|
7
|
+
## 为什么做这个插件
|
|
8
|
+
|
|
9
|
+
引用不是「搬运一段文字」,而是**告诉模型「这句话是你说过的」**。
|
|
10
|
+
|
|
11
|
+
官方的引用把选中的文本作为 `> ` blockquote 塞进消息——模型看到的是一段**来源不明的引用文本**:可能是你说的话,可能是它自己说的,也可能是文档里的。它分不清。
|
|
12
|
+
|
|
13
|
+
而按 id 引用时,模型拿到的是**一条具体的历史消息**,它知道自己是在读「自己第几轮的输出」,能看到当时的完整上下文。**只有这样,你才能真正回到那个具体的点上去继续讨论**——「你上一轮这里说的 X,和我现在问的 Y,是不是矛盾?」,而不是让你把它的意思重述一遍再问。
|
|
14
|
+
|
|
15
|
+
**用户转述会失真,id 不会。** 这是这个插件存在的理由:它让「想进一步讨论的那个点」变得可以精准定位,用户和模型可以在同一个事实上对齐,而不是各自理解一个模糊版本。
|
|
16
|
+
|
|
17
|
+
和官方内置的「选中文字 → `> ` blockquote」是两条不同的路:
|
|
18
|
+
|
|
19
|
+
| | 官方 `dsh-message-quotes` | 本插件 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 触发 | 必须先选中文字 | 一键引用整条 |
|
|
22
|
+
| 内容 | **全文写进输入框** | **只传一行 id** |
|
|
23
|
+
| 模型知道出处 | ❌ 只看到一段文本 | ✅ 知道是哪条消息 |
|
|
24
|
+
| 能回到具体讨论点 | 只能看到你截取的那段 | ✅ **整条 + 原始上下文** |
|
|
25
|
+
| context 成本 | 全文(几百~几千 token) | **25 字符** |
|
|
26
|
+
|
|
27
|
+
两者注册点不同,**可以共存**:引一句用官方,引整条并回到原文用本插件。
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
git clone https://github.com/Twelve212/dsh-quote-shot.git
|
|
33
|
+
cd dsh-quote-shot
|
|
34
|
+
node install.mjs # 链接进 profile 并写入 dsh.profile.bundles
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`install.mjs` 通过 `DSH_HOME`(默认 `~/.dsh`)和 `DSH_PROFILE`(默认 `desktop`)
|
|
38
|
+
定位 profile,不写死路径。
|
|
39
|
+
|
|
40
|
+
装完**重启 DSH**。回滚:`node install.mjs --uninstall`。
|
|
41
|
+
|
|
42
|
+
**要求**:DSH `>= 0.2.1-rc.1`,Node `>= 20`。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 引用:怎么用
|
|
47
|
+
|
|
48
|
+
1. 在助手消息下方点引号图标
|
|
49
|
+
2. 输入框上方出现一张 **引用卡片**(一行预览 + ×)
|
|
50
|
+
3. 正常写问题并发送 —— **卡片随后自动收起**
|
|
51
|
+
|
|
52
|
+
**两种粒度**:先选中一段文字再点 → 只引用那段;直接点 → 引用整条消息。
|
|
53
|
+
|
|
54
|
+
**取消**:点卡片右侧的 × 只移除卡片。**注入发生在点击那一刻,撤不回**(DSH 会话是 append-only)。详见「已知边界」。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 实现
|
|
59
|
+
|
|
60
|
+
### 链路
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
点图标 → 读消息正文(DOM)
|
|
64
|
+
→ POST /quote-shot/inject { sessionId, text }
|
|
65
|
+
→ Host: ctx.agents.get(sessionId).inject({ role:'user', content, source:{form:'notice', summary} })
|
|
66
|
+
→ 注入一条折叠的上下文行(UI 显示 summary,模型读 content)
|
|
67
|
+
→ 前端添加卡片;发送后清空
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`source.summary` 是卡片上那行预览,`content.text` 才是模型读到的全文 —— 两者分离,输入框全程干净。
|
|
71
|
+
|
|
72
|
+
### 三个必须知道的实现细节
|
|
73
|
+
|
|
74
|
+
**1. 消息正文是「动作栏最外层祖先的前兄弟」,不是最近的那个**
|
|
75
|
+
|
|
76
|
+
实测 DOM 结构:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
button
|
|
80
|
+
└ span
|
|
81
|
+
└ span prev = button
|
|
82
|
+
└ div prev = button
|
|
83
|
+
└ div.actions prev = div(10 字,不够长)
|
|
84
|
+
└ div.root prev = null
|
|
85
|
+
└ div prev = null
|
|
86
|
+
└ div.flowItem ★ prev = div.flowItem(897 字) ← 正文在这
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **深度要到 7**(原来写 6,永远够不到)
|
|
90
|
+
- **必须取最外层候选**,不能「第一个够长的就返回」—— 消息里夹带文件路径/代码片段这类小容器时,会在浅层先命中(实测抓到过 87 字符的路径而不是 897 字符的正文)
|
|
91
|
+
|
|
92
|
+
**2. `sessionId` 有两种形态**
|
|
93
|
+
|
|
94
|
+
registry 的 key 是 `session-<uuid>`,客户端 slot 给的是裸 `<uuid>`。Host 端做了双向兼容(先原样查,再补/去 `session-` 前缀)。
|
|
95
|
+
|
|
96
|
+
**3. 发送完成信号 = 输入框从「有字」变「空」**
|
|
97
|
+
|
|
98
|
+
`useInput` 的 published state **没有 `phase` 字段**(它藏在 composer 内部的 snapshot 里),所以拿不到"提交中"状态。改用 400ms 轮询输入框长度:
|
|
99
|
+
`上次 > 0 且 这次 == 0` → 认为发送完成 → 清卡片。
|
|
100
|
+
|
|
101
|
+
**边界**:引用后一个字都不打直接发送,检测不到(输入框一直是空的)。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 九个坑(都踩过,别再踩)
|
|
106
|
+
|
|
107
|
+
### 外部插件注册工具的三个专属坑
|
|
108
|
+
|
|
109
|
+
写外部插件(`~/.dsh/profiles/*/plugins/`)时,下面三条**第一次写工具的人必踩**,而且都**不会报错给你看**:
|
|
110
|
+
|
|
111
|
+
| # | 坑 | 后果 | 教训 |
|
|
112
|
+
|---|---|---|---|
|
|
113
|
+
| 7 | `import { defineTool } from '@deepseek-ai/dsh-tools'` | **整个插件不加载**:路由 404、UI 全消失 | 该包在 `app.asar` 里,外部插件 `ERR_MODULE_NOT_FOUND`。**手写 tool 对象,别 import** |
|
|
114
|
+
| 8 | 工具注册失败时 `throw error` | 小失败放大成插件崩溃 | `ctx.effect` 里**绝不重抛**:`catch → trace + return () => {}` |
|
|
115
|
+
| 9 | `output.render` 返回字符串 | 调用时 `content.some is not a function` | `render` 必须返回**内容分片数组** `[{ type:'text', text }]` |
|
|
116
|
+
|
|
117
|
+
**手写 tool 对象的完整形态**(`defineTool` 只是包装器,`register` 直接吃这个):
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
ctx.tools.register({
|
|
121
|
+
name, description,
|
|
122
|
+
parameters: { type:'object', additionalProperties:false, properties:{...}, required:[...] },
|
|
123
|
+
output: {
|
|
124
|
+
schema: { type:'object', additionalProperties:false, properties:{...}, required:[...] },
|
|
125
|
+
render(args, value) { return [{ type:'text', text: '...' }] } // ← 必须是数组
|
|
126
|
+
},
|
|
127
|
+
async execute(args) { /* return 一个符合 schema 的对象 */ }
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 引用功能本身的九个坑
|
|
132
|
+
|
|
133
|
+
| # | 坑 | 后果 | 教训 |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| 1 | 从 `inject` 里删掉 `agents` | 每次引用 409,功能全废 | **Cordis 的 `inject` 是服务访问的唯一开关**,删了不报错只静默失效 |
|
|
136
|
+
| 2 | 用裸 uuid 查 registry | 永远 `agent-unavailable` | registry key 带 `session-` 前缀,先看 `available` 再写代码 |
|
|
137
|
+
| 3 | 前端失败静默 `return` | 「点了没反应」,无从排查 | **每个提前 return 的分支都要上报 + UI 反馈** |
|
|
138
|
+
| 4 | DOM 提取深度只到 6 | 找不到正文 | 没看过真实 DOM 就别猜深度 |
|
|
139
|
+
| 5 | 「第一个够长就返回」 | 抓到路径碎片(87 字) | 取**最外层**候选,不是最近的 |
|
|
140
|
+
| 6 | 用 `useInput` 的 `phase` 判断发送 | 那个字段不存在 | **未验证的 API 假设 = 建在沙子上** |
|
|
141
|
+
| 7 | **`innerText ?? textContent`** | **0.2.2 后引用全空** | 见下,这是最隐蔽的一个 |
|
|
142
|
+
| 8 | 拿「最后一个 live agent」当调用者 | 后台 subagent 一活跃就 `not-found` | **live agent ≠ 调用者**;后台 reviewer 也是活 agent |
|
|
143
|
+
| 9 | 只认 `session-<uuid>` 会话目录 | 大部分会话读不到 | DSH 两种目录命名并存,裸 uuid 也要试 |
|
|
144
|
+
|
|
145
|
+
### ⚠️ 坑 7 展开:`innerText` 对未渲染元素返回 `""`,不是 `null`
|
|
146
|
+
|
|
147
|
+
**现象**:升级到 DSH 0.2.2 后,引用按钮还在、点击也有响应,但**每次都报 `no-text`**,日志里关键一行是:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{"tag":"div","cls":"V4vHZW_flowItem","prevTag":"div","prevCls":"BE0jCW_root","prevTextLen":0}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`prevTextLen: 0` —— 前兄弟节点**存在于 DOM,但读出来是空的**。
|
|
154
|
+
|
|
155
|
+
**根因**:0.2.2 对消息列表做了**虚拟化**,缓存区里的消息节点**在 DOM 里但不渲染**。此时:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
el.innerText // → "" (空字符串,不是 null!)
|
|
159
|
+
el.textContent // → "正文…" (不依赖渲染,读得到)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
而这段兜底代码**永远不会生效**:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
String(clone.innerText ?? clone.textContent ?? '') // ❌ "" 不触发 ??
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
因为 **`??` 只在 `null` / `undefined` 时回退**,空字符串不触发。
|
|
169
|
+
|
|
170
|
+
**正确写法**(显式判断长度):
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
const rendered = clone.innerText;
|
|
174
|
+
const text = (rendered === undefined || rendered === null || rendered.length === 0)
|
|
175
|
+
? String(clone.textContent ?? '')
|
|
176
|
+
: String(rendered);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**给所有写 DSH 客户端插件的人**:任何从消息 DOM 读文本的地方,**不要用 `innerText ?? textContent`**。0.2.2 之后消息会被虚拟化,未渲染节点的 `innerText` 是空字符串。
|
|
180
|
+
|
|
181
|
+
### 贯穿全局的一条
|
|
182
|
+
|
|
183
|
+
**先埋可观测性再交付。** 这次连续失灵四轮、每次都要用户重启,根因不是难,是**故障不可见**。现在的形态:
|
|
184
|
+
|
|
185
|
+
- Host 每条请求、每个拒绝分支写一行到 `~/.dsh/logs/quote-shot.log`
|
|
186
|
+
- 409 时一并返回 `available: [...]`(实际可用值,省一轮诊断)
|
|
187
|
+
- 前端每个失败分支 POST 到 `/quote-shot/log`,并在按钮旁闪 ✕
|
|
188
|
+
|
|
189
|
+
**判据:如果一个功能失败时你既看不到日志也看不到 UI 变化,它就不算「做完」,只算「还没验证过」。**
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 文件
|
|
194
|
+
|
|
195
|
+
| 文件 | 作用 |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `index.js` | Host:注入路由 + 客户端日志路由 + `quote_read` 工具 |
|
|
198
|
+
| `session-reader.js` | 从会话日志按 message id 取全文(多帧 zstd + 索引缓存) |
|
|
199
|
+
| `client.js` | 浏览器:引用按钮、卡片、DOM 提取、发送检测 |
|
|
200
|
+
| `test.mjs` | 离线单测(格式化、DOM 提取、注入调用、slot 注册) |
|
|
201
|
+
| `verify.mjs` | 安装预检(manifest、patch、bundle、profile 接线) |
|
|
202
|
+
| `install.mjs` | 幂等安装/卸载 |
|
|
203
|
+
| `cordis.patch.yml` | 一条 `insert` 行 |
|
|
204
|
+
|
|
205
|
+
## 按 id 读消息(`quote_read`,已实现并验证)
|
|
206
|
+
|
|
207
|
+
模型可调用的工具,用来解决「引用不可撤回」的地基问题。
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
quote_read(messageId) → { found, messageId, text }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**实测性能**(4.7 MB / 5683 条事件的会话):
|
|
214
|
+
|
|
215
|
+
| 项 | 耗时 |
|
|
216
|
+
|---|---|
|
|
217
|
+
| 首次读(解压 + 建索引) | **197 ms** |
|
|
218
|
+
| 二次读(缓存命中) | **0 ms** |
|
|
219
|
+
|
|
220
|
+
**实现要点**(`session-reader.js`):
|
|
221
|
+
|
|
222
|
+
- 会话日志是**多帧 zstd**(每次追加一帧),普通解压只出第一帧,必须扫 magic `28 B5 2F FD` 逐帧解开
|
|
223
|
+
- 消息 id 位置:`user/message → data.id`,`assistant/message → data.message.id`
|
|
224
|
+
- 同一 id 可能跨多条事件(流式分片),文本要**累加**而非覆盖
|
|
225
|
+
- 按**文件路径**缓存索引(同一日志可能被两种目录名指向),文件增长时才重建
|
|
226
|
+
- 会话目录有两种命名:`session-<uuid>` 与裸 `<uuid>`,**两种都要试**
|
|
227
|
+
- 工具拿不到 sessionId(`exec` 只带 `signal`),所以按三级定位:① 客户端点击时上报的 `messageId → sessionId`;② 遍历**全部** live agent;③ 扫最近 25 个会话日志
|
|
228
|
+
|
|
229
|
+
**已验证**:`quote_read('eb3fc73f-...')` 返回 113 字符原文,与日志里的 `len=113` 一致。
|
|
230
|
+
|
|
231
|
+
**回归验证**:同一个 message id(`02dc5192-...`)在修复前返回 `not-found` 且日志无记录;修复后日志为
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
hint message=02dc5192-... session=session-1db3d041-...
|
|
235
|
+
quote_read session=session-1db3d041-... message=02dc5192-... len=894 via=hint
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 开发
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
node test.mjs # 离线测试
|
|
242
|
+
node verify.mjs # 安装预检
|
|
243
|
+
node install.mjs # 安装 / 刷新
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- 改 `client.js` → **刷新页面**(Ctrl+R)即可
|
|
247
|
+
- 改 `index.js` / `session-reader.js` → **必须重启 DSH**
|
|
248
|
+
- 日志:`~/.dsh/logs/quote-shot.log`
|
|
249
|
+
- 紧急回滚:桌面 `回滚引用插件.lnk`
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 引用:可撤回模式(当前默认,已实测通过)
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
点引用 → 读文本(仅卡片预览)→ 草稿插入 [引用消息 <id>——请先调用 quote_read 读取全文,再回答]
|
|
257
|
+
点 × → 从草稿删掉标记 → 真正撤回
|
|
258
|
+
发送 → 标记随消息带出 → 模型调 quote_read 取全文
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- ✅ **可撤回**:发送前会话从未被写过
|
|
262
|
+
- ✅ **省 context**:消息里只有一行标记,不是几百字全文
|
|
263
|
+
- ✅ **卡片有内容**:预览从 DOM 读,不需要注入
|
|
264
|
+
- ⚠️ **代价**:草稿里会多一行标记(DSH 没有 before-send 钩子,标记必须点击时就位)
|
|
265
|
+
|
|
266
|
+
### ⚠️ 标记必须带祈使句(这条是血的教训)
|
|
267
|
+
|
|
268
|
+
**裸标记 `[引用消息 <id>]` 会失效。** 实测:用户在「开机动画」会话连发 3 次,`quote_read` 调用次数 **= 0**(工具在列表里、标记也在消息里,模型就是不读)。
|
|
269
|
+
|
|
270
|
+
**原因**:模型判断"这条内容不需要,凭上下文能答"——对它来说判断**是正确的**,但用户的意图是"请你看一下"。
|
|
271
|
+
|
|
272
|
+
**修法**:把标记写成**显式指令**:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
[引用消息 <id>] ← 失效
|
|
276
|
+
[引用消息 <id>——请先调用 quote_read 读取全文,再回答] ← 生效
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**对照实验**(同一会话、同一个 message id `93a9d3e7`):
|
|
280
|
+
|
|
281
|
+
| 标记形态 | 结果 |
|
|
282
|
+
|---|---|
|
|
283
|
+
| 裸标记 | `quote_read` 调用 0 次 |
|
|
284
|
+
| 显式指令 | ✅ 调用,`len=2318` |
|
|
285
|
+
|
|
286
|
+
**可复用的通用经验**:给模型配工具时,想让它「总是调用」,**不要写条件许可**("当…时调用"),**要写显式指令**("请先调用…再…")。前者是选项,后者是要求——服从度不是一个量级。
|
|
287
|
+
|
|
288
|
+
**旧模式(代码保留,可回滚):全文注入**
|
|
289
|
+
|
|
290
|
+
`injectQuote()` 仍在 `client.js` 里但不再被调用。它把全文作为 `role:'user'` 消息注入会话,卡片只做提示——**即时响应、输入框干净,但不可撤回**。
|
|
291
|
+
|
|
292
|
+
### 实测记录
|
|
293
|
+
|
|
294
|
+
| 验证项 | 结果 |
|
|
295
|
+
|---|---|
|
|
296
|
+
| 能读到会话**第一条**消息 | ✅ 能,不依赖上下文 |
|
|
297
|
+
| 跨会话引用(「各种插件」会话) | ✅ 读到 5926 字符 |
|
|
298
|
+
| 无引用标记时 | ✅ 不调用工具 |
|
|
299
|
+
| 首次读取 4.7 MB 会话 | 197 ms |
|
|
300
|
+
| 缓存命中 | 0 ms |
|
|
301
|
+
| **裸标记触发** | ❌ **0 次**(设计缺陷) |
|
|
302
|
+
| **显式指令标记触发** | ✅ 调用并读到 2318 字符 |
|
|
303
|
+
|
|
304
|
+
**风险与边界**:
|
|
305
|
+
|
|
306
|
+
1. **模型可能不调**——这次是因为提问直接指向"引用的内容",工具是唯一手段。如果引用只是背景("按上面说的改一下"),模型可能凭印象作答而跳过工具。
|
|
307
|
+
2. **极冷的会话仍可能读不到**——三级定位覆盖「点击时上报的会话」「存活会话」「最近 25 个日志」。如果引用的是一条更早、更冷会话里的消息,扫盘窗口没覆盖到,仍会 `not-found`(此时日志会写明 `MISS` 和试过哪些会话)。
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 工具是怎么被触发的(机制说明)
|
|
312
|
+
|
|
313
|
+
不是规则引擎,是**模型自己决定的**。完整链路:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
1. 注册 ctx.tools.register({ name, description, parameters, ... })
|
|
317
|
+
↓ 进 registry(全局或 agent scope)
|
|
318
|
+
2. 构建 agent 每轮调 LLM 前,把可见工具转成 tool schema
|
|
319
|
+
{ type:'function', function:{ name, description, parameters } }
|
|
320
|
+
↓ 随请求发给模型
|
|
321
|
+
3. 决定 模型原生支持 function calling。它读 description + 当前上下文,
|
|
322
|
+
自行判断「要不要调、调哪个、传什么参数」
|
|
323
|
+
↓ 输出结构化的 tool_calls(不是文本)
|
|
324
|
+
4. 执行 agent loop 查 registry → 调 execute(args, exec)
|
|
325
|
+
↓ 结果作为 tool/result 事件回填
|
|
326
|
+
5. 再生成 带着工具结果再调一次模型
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**关键认知**:`description` 就是给模型的**唯一提示**。写「当用户消息包含 `[引用消息 <id>]` 时调用它」等于告诉模型触发条件;不写,模型不会主动去找。
|
|
330
|
+
|
|
331
|
+
**所以提高调用率的手段只有三个**:描述里写清触发条件、标记做得显眼、必要时在系统提示里加强约束。
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 已知边界
|
|
336
|
+
|
|
337
|
+
- **草稿多一行标记**:`[引用消息 xxx]` 会出现在输入框里,发送后成为消息正文的一部分。
|
|
338
|
+
- **模型可能不调工具**:见上,引用可能被忽略。
|
|
339
|
+
- **极冷的会话读不到**:定位覆盖点击时上报的会话、存活会话、最近 25 个日志;更早且未在窗口内的会话会 `not-found`。
|
|
340
|
+
- **抓取的是「当时的 DOM」**:卡片预览是点击瞬间的快照。
|
|
341
|
+
- **引用后不打字直接发送**:卡片不会自动收起(检测不到发送)。
|
|
342
|
+
- **排版丢失**:DOM 提取只取 `innerText`,代码块/表格会变成纯文本。
|