Bundle
dsh-searchflow
Search-flow visualizer + snapshot accelerator for DeepSeek Harness: real-time animated tool-call lifecycle icons (SSE-driven) and tools/execute interception that short-circuits web_snapshot / browser_open from cache and prefetches top search results.
- Source
- Dayi-Z
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-searchflow — 搜索流程动效可视 + 快照加速
DSH 实时搜索加速插件,两件事:
1. **可视化** — 把 `web_search → web_fetch → web_snapshot → browser_open/click → read` 每一步实时呈现在工具调用行内:用动效 `wb_*` 图标(运行中蓝光呼吸 / 完成绿勾 / 失败红 X)替换默认的通用图标,黑盒等待变成可见的前进。同时用 `⚠ 挑战 / ⚠ 404 / ⚠ 噪声` 徽标标记 Cloudflare 挑战页与 404 页等「假成功」结果。任务级另见下方「搜索流程卡片」——`搜索中 → 阅读中 → 撰写中` 阶段进度条 + 渐进结果流 + 完成摘要行。
2. **加速** — 在 `tools/execute` 瀑布中拦截(不改任何上游包):
- `web_snapshot` 命中缓存直接短路(90s 全量渲染 → 5ms)
- `browser_open` 命中缓存立即返回已渲染内容,真实导航在后台预热(省 networkidle 8s + 全页截图)
- 搜索结果 Top-3 自动预取预热内存缓存
- URL 变体(www / 非 www / 尾斜杠 / 参数顺序)共享同一缓存键
## 架构
```
┌─ Host (lib/index.js) ─────────────────────────────────┐
│ ctx.on('tools/execute') ── 观察 web_*/browser_* 工具 │
│ → EventBus (start/completed/error, 含 cache/rule) │
│ GET /searchflow/events (SSE) ── 回放 + 实时事件流 │
│ web_snapshot / browser_open 缓存拦截 + Top-N 预取 │
└────────────────────────────────────────────────────────┘
│ EventSource
┌─ Client (lib/client.js) ──────────────────────────────┐
│ 工具调用行内动态图标(搜索/抓取/快照/点击/阅读/导航) │
│ running/done/error 实时切换 + 行级高亮 + 光晕 │
│ 搜索流程面板:better-sidebar 在场 → 页签;缺席 → 浮动卡片 │
└────────────────────────────────────────────────────────┘
```
客户端采用 ModuleLoader 单文件自包含(图标内联,无相对路径依赖,无需构建步骤),与 dsh-icon-workbench / dsh-guardian 同型。
## 泳道映射
| 阶段 | 工具 | 图标 |
|---|---|---|
| search | `web_search` / `web_search_pro` / `web_platform_search` / `web_exa_search` / `github_issue_list` | 拉匣寻物 |
| fetch | `web_fetch` / `web_fetch_pro` / `web_exa_contents` | 抓取 |
| snapshot | `web_snapshot` / `browser_screenshot` | 取景·定影 |
| click | `browser_open` / `browser_click` / `browser_type` / `browser_recipe_run` / `browser_scroll` | 指尖按靶 |
| read | `browser_read` / `web_rule` / `web_history` / `web_search_stats` / `github_issue_read` 等 | 16宫格扫描 |
| navigate | 其它导航类 | 路径绘制 |
每个工具调用附带状态:`缓存`(fromCache 命中)与 `规则`(usedRule)徽标,加速效果可感可验证。
## 搜索流程卡片(Wave 1 观感升级)
工具行内图标之外,新增任务级「搜索流程」浮动卡片(右下角,搜索开始时出现,可收起/点击展开):
- **⚡ 深度模式(默认关)**:卡片头部金色闪电开关,开启后宿主在搜索前自动扩展查询——CJK 查询加英文学术关键词(`overview survey`),英文/拉丁查询加中文综述关键词(`最佳实践 综述`)。卡片实时显示「深度」徽章。通过 `GET/POST /searchflow/deep` 端点同步状态。
- **阶段进度条**:`搜索中 → 阅读中 → 撰写中` 三段芯片,**直接复用现有动效图标**(进行中呼吸 / 完成绿勾绘制 / 空闲静置),配渐变连线与对齐标签行;「撰写中」由事件空闲 3.5s 推断(模型开始作答),完成后 9s 自动折叠成小胶囊。结果行带域名 hash 色徽章,双轮命中显示金色交叉验证勾。
- **渐进结果流**:搜索完成事件携带 top-6 结果(标题 + 域名 + 摘要截断 ≤140 字符),客户端逐个浮现(70ms 错峰),可点击直达原文。
- **完成摘要 chips**:进入「撰写中」后底部出现统计胶囊 —— `总耗时 · N 来源 · 去重 · 验证 高/中/低 · ≈tokens`,速度/广度/可信度/省 token 一次可感知,**不暴露任何引擎归属**(引擎是内部实现细节)。进入撰写中时 `doneAt` 记时点,时间从「进行中 Xs」冻结为「总耗时 Xs」,不再继续累计。
- **数据来源**:宿主 `tool:completed` 事件新增 `durationMs / resultCount / distinctDomains / sourcesTop / charCount`;token 估算 = 摘要字符数 / 3.5;交叉验证置信度 = 多次搜索重复域名占比(≥2 次搜索才显示)。纯观察层实现,零上游依赖。
- 无障碍:`prefers-reduced-motion` 下禁用全部动画;任务状态经 `window.__SF_DEBUG__.task` 透出便于诊断。
## 宿主形态:better-sidebar 页签 / 独立浮动卡片
搜索流程面板(阶段进度 + 结果流 + 摘要)有**两种寄生形态**,同一份实现(`SearchFlowCard` 的 `variant`),自动选择:
| 形态 | 触发条件 | 表现 |
|---|---|---|
| **页签** | `ctx.betterSidebar` 在场 | 注册为侧栏页签(id `searchflow:panel`,`single`,order 96):铺满容器、结果区占满剩余高度并自滚动、显示空态、不自带浮动外框与收起按钮 |
| **浮动卡片** | better-sidebar 缺席 | 维持右下角卡片(fixed / 368px / 毛玻璃 / 可收起成胶囊) |
- **软依赖**:better-sidebar 是可选同伴,只按结构探测 `ctx.get('betterSidebar')`,**不 import 它的类型**(否则软集成会变成硬构建依赖)。注册后回读 `getTab` 校验;读不到就抛错并退回浮动卡片——宁可有卡片,不可没面板。
- **迟到接入**:better-sidebar 比本插件晚挂载时,`ctx.inject(['betterSidebar'])` 一到就换成页签并撤掉浮动卡片,避免同一份数据在屏幕上有两个出处。
- **可见性**:宿主给 `visible=false`(页签被切走)时停止 400ms 重绘定时器。
- **手动覆盖**:`localStorage['sf.host'] = 'dock' | 'tab'` 强制形态(默认 `auto`)。
- **诊断**:当前形态写在 `document.body.dataset.sfHost`(`tab` / `tab-late` / `dock`),面板根节点带 `data-sf-variant`。页签角标 = 进行中流程的结果数。
- **已知边界**:宿主事件流不区分会话(`tools/execute` 是进程级观测),所以页签展示的是全局搜索流程,不随会话隔离;页签声明 `single`,只会有一个。
### 检索记录(悬挂,仅页签形态)
侧栏空间够,所以**一次检索结束不等于它消失**:新的搜索开始时,上一轮不会把面板就地覆盖,而是**下移成「检索记录」里的一条**挂在页签底部,可以随时翻回去。
- **一条记录 = 一行**:查询词(`tool:start` 的 summary)· 时间(HH:MM)· N 次搜索 · M 个独立来源。点一下展开,看它自己的结果列表 + 摘要 chips(总耗时 / 来源 / 去重 / ≈tokens)。
- **顺序与上限**:最新在前(进行中的那轮在上面,历史在它下面);最多 `SF_HISTORY_MAX = 20` 条,展开单条最多列 `SF_HISTORY_RESULTS = 30` 条结果。
- **不记录空任务**:只跑过 `browser_open` 之类、既没搜索也没结果的任务不占位置。
- **没有进行中的检索时**,页签显示「当前没有进行中的检索」+ 记录列表,而不是一块空面板。
- **浮动卡片不显示记录**:卡片就那么点地方,堆历史只会挤掉当前任务 —— 这条只在页签形态出现。
- **换会话不会清空**:切换会话时,**正在跑的那一轮会被归档成记录**再放手(而不是像以前那样直接丢掉),记录本身跨会话保留 —— 宿主事件流不区分会话,记录也就是全局的。
- **生命周期**:记录是**页面会话级**的(内存),刷新后由宿主 SSE 回放重建最近一轮;不做 localStorage 持久化。
### 什么算作一次「搜索」
只有宿主 `TOOL_PHASE` 里映射为 `search` 的 **4 个工具**才算一轮检索的开始:
| 工具 | 阶段 |
|---|---|
| `web_search` / `web_search_pro` | search |
| `web_platform_search` | search |
| `github_issue_list` | search |
**其余网页工具都不算搜索**:`web_fetch` / `web_fetch_pro` / `web_exa_contents`(fetch)、`web_snapshot` / `browser_screenshot`(snapshot)、`browser_open` / `browser_click` / `browser_type` / `browser_recipe_run` / `browser_scroll`(click)、`browser_read` / `web_rule` / `web_history` / `web_search_stats` / `web_backend_status` / `web_deps` / `github_issue_read` 等(read)。
三条规则:
1. **只有搜索能开一轮新任务**。fetch / snapshot / click / read 只**顺带推进**已经在跑的那一轮;没有在跑的检索时它们什么都不开 —— 否则 agent 单纯打开一个网页,面板就会以「搜索流程」为标题冒出来显示「0 次搜索」。
2. **搜索次数按工具身份计,不按"有没有带回结果"计**。一次真的搜索哪怕 0 结果(宿主没有 `sources` 可发、事件里就没有 `sourcesTop`),也记「1 次搜索 · 0 来源」—— 这和"根本没搜过"是两回事。
3. **既没搜索也没结果的任务不进记录**(例如只有 `browser_open` 的一轮)。
## 加速细节
| 拦截点 | 行为 | 收益 |
|---|---|---|
| `web_snapshot` | 内存 LRU(200/1h) + web-search-pro store.db(只读)双层缓存,命中短路返回 `{value}` | 90s → 5ms |
| `browser_open` | 命中缓存立即返回已渲染内容;真实导航 fire-and-forget 后台预热(active page 保持就绪,后续 click/type 语义不破坏) | ~13-18s → ~0ms 前台等待 |
| 搜索结果 Top-3 | `web_search*` 完成后对结果 URL 串行预取渲染写入内存缓存 | 后续 open/snapshot 直接命中 |
| URL 规范化 | 去 www / 尾斜杠 / 默认端口 / 参数排序,变体共享缓存键 | 变体重复渲染归零 |
| 噪声检测 | Cloudflare 挑战 / 404 页 / 噪声查询 → ⚠ 徽标 | 假成功一目了然 |
可选性:`browser` 服务通过 `ctx.get` 软依赖(未安装 dsh-browser 时预取静默降级,不影响可视化与 snapshot/browser_open 缓存)。
## 安装
```bash
dsh plugin --profile web add dsh-searchflow # 已发布/已收录后
dsh plugin --profile web add /path/to/dsh-searchflow # 或直接从源码目录/GitHub 地址装
```
重启 profile 后生效。卸载:`dsh plugin --profile web remove dsh-searchflow`。
> 发布为即插即用插件:本包是标准 DSH bundle(`dsh.bundle.patch` + `dsh.client`),`pnpm pack` / git 地址均可用 `dsh plugin add <spec>` 安装。
## 开发
- `lib/index.js` — host:`tools/execute` 观测 + 缓存拦截 + SSE(需 `webServer`、`tools` 注入;`browser` 为可选软依赖)
- `lib/client.js` — client:ModuleLoader 单文件自包含(内联图标,相对路径 require 不解析)
- `lib/icons.js` — 共享图标源(供抽取参考;实际 client 内联)
- `preview.html` — 独立泳道预览(vendor/ 本地 React UMD),含成功/错误双流程演示脚本
- `test-realistic.mjs` — 回归测试(node 直跑,无依赖)
- `test-sidebar-tab.mjs` — better-sidebar 集成回归(jsdom + 真实 React 18;jsdom/react 从 DSH 应用运行时解析,解析不到则 SKIP)
## 验证
```bash
node --check lib/index.js lib/client.js lib/icons.js
node test-realistic.mjs
node test-wave1.mjs
node test-sidebar-tab.mjs
```
覆盖:B#1 快照同 URL 短路、B#3 www/尾斜杠变体共享键、B#4 browser_open 缓存命中 + 后台 warm 刷新、B#5 无 browser 服务时预取静默降级。host 运行时以 node 模拟 cordis ctx 实测通过。
`test-sidebar-tab.mjs`(46 项)覆盖六组:**A** better-sidebar 在场(注册页签 / 不挂卡片 / 空态可渲染 / 事件驱动后阶段条与结果流出现 / 角标有数)、**B** 缺席(浮动卡片 / 登记迟到接入)、**C** `sf.host=dock` 手动钉住、**D** 浮动卡片回归(fixed 定位、368px、结果与摘要、收起胶囊)、**E** 检索记录悬挂(旧的一轮被归档而非覆盖、记录保留查询词与结果、记录区渲染、展开可见结果、20 条上限、空任务不占位)、**F** 浮动卡片不长出记录区。E 组用可推进的假时钟跨过 30s 任务间隔。
## License
MIT
Install
dsh plugin --profile web add github:Dayi-Z/dsh-searchflow
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-searchflow from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.