dsh-find
在 DSH 里按下 Ctrl+F(macOS 用 ⌘F),弹出全局查找面板:输入文字就遍历所有工作区、所有对话, 本地找出包含该文字的对话,命中结果分左右两栏显示 —— 左边是思考链,右边是对话文本(用户消息、 助手回复、工具输出),整段显示并高亮关键词,点击结果直接跳到那段话所在的位置。
搜索全程本地执行,不调用任何模型,不消耗 token。
面板有两个标签页:文本(就是上面这个查找界面)和 图片/视频(按工作区 → 对话逐层展开, 点开哪个对话才读取哪个对话的图片和视频,并标出哪些是用户发的、哪些是 AI 生成的)。
用法
| 操作 | 说明 |
|---|---|
Ctrl+F / ⌘F |
打开查找面板。若页面上有选中的文字,会先用它作为关键词 |
/find-text-ui 关键词 |
在对话输入框里发这条指令,直接打开面板并开始查找该关键词(不经过模型) |
| 对模型说「查找 xxx」 | 模型可以调用 find_text 工具拿一份文本报告(参数 query / maxChars / perSession / total)。扫描本身仍是本地零 token,但报告会进对话上下文,所以优先用上面的 UI。 |
输入文字 → Enter 或点蓝色「查找」按钮 |
手动触发查找:打字不会自动搜,避免半截关键词就扫全部对话 |
| 勾「隐藏已归档」 | 结果里不再列出已归档对话的命中(默认不勾,即照常显示并排在最后) |
| 点击命中条目 | 跳到该段话所在的位置(面板里没有键盘选择,也没有切栏操作) |
| 点顶部「图片/视频」标签 | 切到媒体页:先只列工作区,点工作区 → 点对话,才读取那个对话的图片和视频 |
| 点「一键展开」 | 一次读完所有对话,把全部图片和视频列出来 |
| 点「一键折叠」 | 收起所有工作区和已加载的对话,只留目录(不重新读取日志) |
| 再次点已展开的对话 | 收起它的缩略图,再点又展开(不重新读取) |
| 点「重新扫描」 | 重新列出工作区与对话,并清空已加载的缩略图 |
| 媒体页点缩略图 | 放大查看,视频可直接播放;Esc 或点空白处关掉 |
| 媒体页右键缩略图 | 弹出菜单:「另存为…」(复制到自选位置)、「打开文件所在位置」(资源管理器里选中该文件 / 打开附件目录)、「删除」(红色,需点两次确认;对话内附件不允许删除) |
Esc / 点遮罩 |
关闭面板 |
为什么不提供 /find-text 文本回复命令
早期版本有一条 /find-text 关键词,把结果直接写成聊天里的一段文本。它被删掉了,原因很简单:
文本回复本身就是一次 AI 对话 —— 要消耗 token。 同一份结果,走面板是纯本地渲染、零 token; 写进对话就变成上下文,模型还要读一遍再转述,既慢又花钱。所以查找结果统一走 UI 面板 (
Ctrl+F或/find-text-ui),聊天里不再输出查找报告。
find_text 工具仍然保留,但它的定位是「模型需要时自查」:扫描在本地、零 token,只有返回的那段报告会占上下文。
结果怎么显示
- 面板分左右两栏:左栏标题 思考链(模型 reasoning 块里的文字),右栏标题 对话文本 (用户消息、助手输出、工具结果)。两栏同时显示、各自独立滚动,标题上显示该栏命中数; 两栏都只靠鼠标点击结果跳转,没有键盘选择,也没有切栏操作。
- 每栏内一个工作区一张卡片,卡片之间留小间隔:卡片头是工作区名 + 该工作区命中数; 卡片内再按对话分节(对话标题 + 该对话命中数),节下面是命中段落。
- 每条命中显示命中所在的整段(按空行分段);段落比窗口允许的长度更长时,以命中为中心截取窗口并加省略号, 保证不会撑爆面板。
- 右栏每条命中带来源徽章:蓝字「用户」= 用户自己说的话,绿字「AI」= 助手回复,紫字「工具」= 工具执行结果, 琥珀字「系统」= 平台或插件注入的内容(长期记忆、runtime context、技能目录、复盘任务等,它们同样以 user 消息入库); 左栏整栏都是模型的思考过程,所以不再逐条标注。
- 右栏标题旁有来源筛选:用户 / AI / 工具 / 系统 四个可点的小标签,默认全亮,点一下即隐藏该来源, 只显示还亮着的那些;筛选只影响显示,不会重新搜索,左栏不受影响。
- 字数上限随窗口高度自适应:约
窗口高度 / 2.6,夹在 200–900 字符之间。 - 关键词用
<mark>高亮。 - 已归档的对话照样会被找到:对话标题后带一个红字「已归档」标签,并且这些对话排到卡片底部、整张只含归档对话的卡片排到最底部。 归档对话在界面上不会被挂载出来,所以点它不会跳转,面板底部会给出提示;先取消归档再点即可。
图片/视频 页
切到「图片/视频」标签后,页面先不扫描任何对话,只列出工作区(约 0.8 秒):
- 点工作区卡片头展开,看到它下面的对话列表(标题、已归档标记、读取状态)。
- 点某个对话,才去读这一个对话的日志,把它的图片/视频铺成缩略图(约 0.2–0.3 秒); 这个对话没有图片/视频时,行尾直接显示「无」。
- 已经加载过的对话,再点一下就能收起它的缩略图(再点又展开),不会重新读取日志; 行首的 ▾ / ▸ 表示当前是展开还是收起。
- 右上角「一键展开」= 读全部对话、载入所有媒体(就是以前的整页wall,较慢); 「刷新列表」只重新读取工作区/对话清单,不读对话内容。
展开之后每个工作区一张卡片,卡片内按对话分块,块内是缩略图网格:
- 每个工作区一张卡片,卡片内是网格缩略图;卡片头显示工作区名与该卡片的媒体数量。
- 每个缩略图左上角带来源徽章:蓝「用户」= 用户发来的,绿「AI」= 助手生成或工具产出的。
- 已归档对话的媒体照常列出,缩略图半透明并带红字「已归档」,排在卡片末尾/最底部; 勾「隐藏已归档」可以不显示它们。
- 标题旁有「用户 / AI」两个筛选标签,默认全亮,点一下只看另一边(纯前端过滤,不重新扫描)。
- 点缩略图放大;视频显示 ▶ 角标,点开后可以直接播放。底栏显示已加载了多少媒体、涉及多少个对话。
- 右键缩略图弹出三项菜单:
- 另存为… —— 弹出系统保存对话框,把这张图/这段视频复制到你选的位置(原文件不动)。 浏览器不支持保存对话框时,退化为普通下载到默认下载目录。
- 打开文件所在位置 —— 本地文件会在资源管理器里选中该文件;对话附件没有独立文件名,打开它所在的
attachments/v1/objects/<hex 前 2 位>目录。 - 删除(红色字)—— 第一次点击变成「确认删除」,再点一次才真正删除磁盘上的文件,删完直接从列表里移除。 对话附件不允许删除:它属于会话日志本身,删掉会让那段对话的图片永久失效,菜单里会给出说明。
媒体是从哪来的
- 附件:日志里
{type:'image', attachment:{attachmentId:'sha256:<hex>', mediaType, bytes, width, height, name}}形式的块,实体文件在$DSH_HOME/attachments/v1/objects/<hex 前 2 位>/<hex>,由GET /dsh-find/media/attachment?sha=<hex>流式回传。 - 本地文件:视频从来不会变成附件块,只以路径出现在消息文本里(例如
E:/out/promo.mp4, 常被 markdown 转义成E:////out////promo.mp4)。插件从text块里提取 Windows 绝对路径, 校验扩展名白名单 + 文件确实存在 + 不超过 600 MB,再由GET /dsh-find/media/file?path=<绝对路径>回传。 - 同一个文件被用户和助手都提过时,算「用户」。
点击之后发生什么
- 切到命中所在的工作区/对话(
uiWorkspace.openSession); - 把会话事件加载到命中的那条消息(
sessions.using+ISession.loadThrough(seq)); - 在页面上轮询找到这段文字的可见节点,滚动到视口中央并脉冲高亮 4 秒。
实现
Host 半(
index.js):注册同源路由GET /dsh-find/search、GET /dsh-find/media, 以及媒体文件回传GET /dsh-find/media/attachment、GET /dsh-find/media/file, 以及懒加载用的GET /dsh-find/media/tree(只列工作区/对话)、GET /dsh-find/media/session?id=(只读一个对话), 以及右键菜单用的GET /dsh-find/media/reveal?id=、POST /dsh-find/media/delete?id=(先GET /dsh-find/health可探活,返回version: 6)。 搜索用ctx.sessionQuery:listSessions()拿到全部会话(跳过origin === 'subagent'的子代理会话);- 每个会话
filterEvents(id, [{kind:'text', text}, {kind:'type', values:[...]}])先用索引判断有没有命中; - 命中的会话再
readSession(id)取原始事件,从message.content里按块拆分:{type:'reasoning'}→channel:'thinking',{type:'text'}→channel:'conversation'。 这样每条命中都带channel,前端才能分栏(索引里的text是 JSON 序列化,无法区分块); readTitleSnapshots()补对话标题,ctx.workspaceRegistry.list()用cwd匹配工作区名称;ctx.workspaceRegistry.archivedSessionIds(登记表全局的归档集合)给每条命中打archived标记, 并把归档会话整体排到普通会话之后;- 8 路并发;客户端断开(
res.on('close'))时用AbortController取消。
Client 半(
client.js):只用fetch调上面的路由,UI 挂在shell.overlay槽位(iddsh-find-dialog), 键盘捕获挂在document的keydown(捕获阶段,preventDefault掉浏览器原生查找)。命令桥(
/find-text-ui):命令只能在 Host 半注册(ctx.commands.register),而面板在页面里, 所以用一个长轮询把手:- 命令 handler 把
{seq, query, at}写进 Host 内存队列(publishFind)并返回一条成功提示; - Client 半启动
startPendingPoll(),请求GET /dsh-find/pending?since=<seq>; - 服务端没有新请求就把这条 HTTP 请求挂起最多 20 秒,有新请求立即返回;
at超过 60 秒的旧请求不再下发,避免刷新页面时重放; - 页面收到
query就调openPaletteWith(query)打开面板并开始搜索。
整个过程没有任何模型调用。
- 命令 handler 把
返回结构
{
"query": "...", "tookMs": 1291, "sessionsScanned": 27, "sessionsMatched": 4,
"counts": { "thinking": 3, "conversation": 3 },
"truncated": false,
"truncatedChannels": { "thinking": false, "conversation": false },
"archivedHits": 4,
"totalHits": 6,
"hits": [
{ "sessionId": "...", "title": "...", "workspaceTitle": "插件", "archived": true,
"seq": 412, "type": "assistant/message", "role": "assistant",
"channel": "thinking", // thinking | conversation
"snippet": "...", "occurrences": 2,
"clippedStart": false, "clippedEnd": true }
]
}
查询参数
GET /dsh-find/search?q=<关键词>&maxChars=420&perSession=3&total=150
| 参数 | 默认 | 范围 | 含义 |
|---|---|---|---|
q |
— | ≤ 200 字符 | 关键词(空则返回空结果) |
maxChars |
420 | 120–2000 | 每条命中片段的字数上限 |
perSession |
3 | 1–12 | 每个对话每栏最多展示几处命中 |
total |
150 | 1–400 | 每栏命中上限(超过则 truncated: true) |
媒体接口
GET /dsh-find/media?total=400 会逐个会话 readSession() 扫一遍媒体,返回:
{
"tookMs": 3674, "sessionsScanned": 32, "sessionsMatched": 16,
"total": 282, "truncated": false,
"counts": { "user": 17, "assistant": 265, "image": 277, "video": 5 },
"items": [
{ "sessionId": "...", "title": "...", "workspaceTitle": "插件", "archived": false,
"kind": "image", // image | video
"speaker": "user", // user | assistant
"source": "attachment", // attachment = 日志里的 image 块;file = 文本里的本地路径
"name": "screenshot.png", "mediaType": "image/png", "bytes": 66356,
"width": 1280, "height": 720,
"seq": 42, "type": "user/message",
"url": "/dsh-find/media/attachment?sha=<hex>" }
]
}
GET /dsh-find/media/file?path=<绝对路径> 与 GET /dsh-find/media/attachment?sha=<hex>
只接受媒体扩展名 / 64 位 hex,文件不存在返回 404,超过 600 MB 返回 413。
安装
先从 GitHub 取一份源码(或直接用本地工作副本):
git clone https://github.com/A-2788553793/dsh-find.git
再把它以 source link 方式装进当前 profile:
plugin_manager install_bundle <克隆下来的 dsh-find 目录>
# 改过代码要重装:先移除再安装(重复 install 会报 already-installed)
plugin_manager remove_bundle dsh-find
plugin_manager install_bundle <克隆下来的 dsh-find 目录>
装/重装后刷新一次页面(Client 半的 JS 由 bundle 提供)。
已知边界
- 只在会话事件里搜(用户消息、助手消息、工具结果),不含压缩摘要之外的隐藏状态。
- 工具调用的参数(
{type:'tool-call'}块)不参与匹配,避免搜到一堆命令噪音。 - 命中定位依赖文字真的出现在页面上;若目标对话被虚拟列表折叠得很远,
loadThrough之后仍需滚动加载, 轮询 7 秒仍未找到时只完成跳转、不高亮。 - 已归档的对话能搜到但打不开(界面不挂载它),点击只在面板底部给提示、不跳转;它们也总排在结果最后。
- 面板本身是
shell.overlay的常驻占用者,未打开时渲染null。 - 媒体页只认本机路径:远程 URL 形式的图片(网页截图里的
https://…png)不会列出, 工具回显的网页图片仍会以附件形式出现。 - 媒体扫描要读全部会话日志,第一次切换约 3–5 秒;结果缓存在面板里,切换标签不会重扫。
更新日志
0.3.9
- 删除斜杠命令
/find-text(文本回复版)。原因:文本回复本身就是一次 AI 对话,要消耗 token; 查找结果改为统一走 UI 面板(Ctrl+F//find-text-ui),纯本地渲染、零 token。 - 移除 0.3.8 为它加的自定义命令卡片渲染(
conversation.chat.commandview里的find-text槽位),命令只剩/find-text-ui一条。 find_text工具保留:扫描零 token,只有返回的报告会进上下文,供模型在需要时自查。
0.3.8
/find-text 关键词的结果改用自己的聊天卡片渲染(接管conversation.chat.commandview里find-text这一格):工作区一节、对话一张小卡、每条命中一行,带来源徽章和正常行高,不再是挤成一行的小灰字 + 可展开的<pre>。默认展开,点标题行可收起。- 报告首行改短(只放「找到 N 处、涉及 M 个对话」),扫描耗时与截断提示挪到第二行。
0.3.7
- 新增 agent 工具
find_text:模型可以自己调用本地索引,参数query/maxChars/perSession/total,返回按工作区、对话分组的文本报告(含思考链 / 对话文本、说话人、seq、是否归档)。 - 新增斜杠命令
/find-text 关键词:同样的文本报告,直接在对话里回复;/find-text-ui仍然负责打开窗口。 - 文本报告默认更紧凑:每条片段 220 字符、每个对话每栏 2 条、总共 40 条。
0.3.6
- 斜杠命令
/find-text改名为/find-text-ui(避免和别处注册的同名命令冲突)。旧的/find-text不再注册。
0.3.5
- 图片/视频页:「一键展开」和「重新扫描」之间新增「一键折叠」,一次收起所有工作区与已加载的对话。
0.3.4
- 图片/视频页:已加载的对话可以点击收起/展开缩略图(行首 ▾ / ▸ 指示状态),收起不重新读日志。
0.3.3
- 修复媒体页右键菜单失效(0.3.2 重构时误删了菜单相关函数,菜单打不开、两项都点了没反应)。
- 右键菜单新增**「另存为…」**:用系统保存对话框把文件复制到自选位置;不支持时退化为普通下载。
0.3.2
- 图片/视频页改成懒加载:打开页面只列工作区,点工作区才展开对话、点对话才读取那一个对话的媒体, 没有媒体时显示「无」;新增「一键展开」按钮(等价于旧版的整页扫描)。
- 新增路由
GET /dsh-find/media/tree与GET /dsh-find/media/session?id=;/health的version升到 6。
0.3.1
- 媒体页缩略图支持右键菜单:「打开文件所在位置」(资源管理器里选中文件 / 打开附件目录)与「删除」(红字,二次确认)。
- 删除只允许作用于插件这次扫描真实列出过的本地文件(按
id白名单,路径不来自浏览器);对话附件拒绝删除,避免破坏会话历史。
0.3.0
- 新增标签页:面板顶部多了「文本」与「图片/视频」两个页签,Ctrl+F 仍默认打开「文本」。
- 新增图片/视频页:把本机所有对话里出现过的图片和视频列成缩略图墙,按工作区卡片分组, 每个缩略图标出是「用户」发的还是「AI」生成的;支持「只看用户 / 只看 AI」与「隐藏已归档」, 点缩略图放大(视频可直接播放),头部按钮区可重新扫描。
- 新增路由
GET /dsh-find/media、GET /dsh-find/media/attachment?sha=、GET /dsh-find/media/file?path=;/health的version升到 4。
0.2.0
- 新增来源标注:右栏每条命中带徽章 —— 蓝「用户」、绿「AI」、紫「工具」、琥珀「系统」。
系统注入(长期记忆、runtime context、技能目录、复盘任务等)同样以 user 消息入库,
这里按消息的
source.kind判定,只有真正由人输入的消息才算「用户」。 - 新增来源筛选:右栏标题旁「用户 / AI / 工具 / 系统」四个标签,默认全亮,点一下隐藏该来源; 纯前端过滤,不重新搜索,左栏思考链不受影响。
- 变更:徽章文案「你」改为「用户」;AI 来源从灰色改成绿色,四种来源各有一种颜色。
0.1.0
- 首个版本:Ctrl+F 全局查找面板、思考链 / 对话文本双栏、整段展示与高亮、点击跳转定位、
卡片式工作区分组、已归档对话标注与置底、
/find-text-ui命令、隐藏已归档开关、手动「查找」按钮。
No comments yet. Be the first to write one.