lynkas
dsh-think-flow-flow
DeepSeek Harness client plugin: constant-rate typewriter reveal for assistant output and reasoning, with per-model gating.
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 13, 2026
- Updated
- Aug 14, 2026
Introduction
dsh-think-flow-flow
English | 中文
一个 DeepSeek Harness 的客户端插件:把助手的回复和思考(reasoning)以恒定速率的打字机效果逐字渐显——无论模型是逐 token 流式,还是一次性整段抛出,显示都按你设定的速度逐字出现。
- 🖋️ 恒定速率打字机,回复与思考一视同仁。
- 🧠 懂思考:折叠态的「最新一行」滚动条原样保留(连贯不卡);展开后的完整思考才逐字渐显。
- ⏭️ 顺序渐显:回复会等思考渐显完毕再开始,期间思考自动提速让路;整轮生成结束后,剩余尾巴快速追平。
- 🎛️ 按模型开关 + 设置面板(总开关、模型白名单、速度)。
- 🛡️ 不卡页面:代码块直接显示不节流,文本扫描有上限,每帧最多采集一次——长回复 / 多代码块也不会把 DSH 网页卡死。
纯客户端视觉效果。不触碰模型请求、不拖慢 host 上的 agent loop、不重发任何事件。
为什么用 DOM 节拍(而不是事件层重切)
最直觉的想法——在客户端拦截 assistant/chunk 事件流、把大 delta 拆成多个小事件重发——在 DSH 里行不通。客户端事件窗口有严格的 per-session seq 不变量(@deepseek-ai/dsh-client-runtime 的 acceptLiveEvent / appendLive):
seq > tail + 1→ 判定为丢包 → 触发历史重拉;seq <= tail→ 判定为重复 → 丢弃;- 只有
seq === tail + 1才会入窗。
host 是 seq 的唯一签发者,所以把一个 chunk 拆成多个事件在客户端不可能。节流只能在渲染层做:插件观察已渲染的助手([data-streaming])与思考([data-variant="think"])容器,记录每个文本节点的完整原文,按一个固定速率推进的游标来裁剪显示。
服务端方案(llm/stream waterfall)是官方钩子、能正确重切,但它会节流 agent loop 本身,拖慢 agentic 流程里的工具派发;本插件完全规避了这个代价。
安装
这是一个 profile bundle。先构建,再加进你的 DSH profile:
git clone https://github.com/lynkas/dsh-think-flow-flow.git
cd dsh-think-flow-flow
npm install
npm run build # → lib/client.js(浏览器半边)+ lib/index.js(host stub)
dsh plugin --profile web add "$(pwd)" # 用 pnpm 装进 profile
然后重启 dsh web(让 host 加载新 bundle)并刷新浏览器,打开 Settings → Think Flow。
卸载:
dsh plugin --profile web remove dsh-think-flow-flow
profile 位于 $DSH_HOME/profiles/<name>/。当 pnpm run dev:web 在重建 client bundle 时,本插件可热重载;否则刷新页面即可。
add之前先build:包的./client导出指向构建产物lib/client.js,profile 安装时它必须存在。本地迭代用npm run dev监听重建。
配置
打开 Settings → Think Flow(通过 settings.section slot 贡献)。配置存在 localStorage 的 dsh-think-flow-flow:v1。
| 字段 | 含义 |
|---|---|
| Enabled | 总开关。 |
| Models | 一行一个模式(对模型标签做大小写不敏感子串匹配)。留空 = 所有模型。 |
| Speed (chars/sec) | 打字机速率。游标不会超过较慢模型本身的到达速度。 |
按模型启用
白名单匹配的是从 composer 模型座读到的当前模型标签。读不到时默认启用。示例:
deepseek-reasoner
glm-5.2
诊断日志
逐帧详细日志默认关闭。在浏览器 console 里开启:
localStorage.setItem("dsh-think-flow-flow:debug", "1");
刷新后会看到 [dsh-think-flow-flow] apply() called / active … 加载日志,以及节流时每 ~0.5s 一条的 {revealed, total, backlog, raf}。
节流原理
- 一个挂在 document 上的
MutationObserver把节流容器([data-streaming]、[data-variant="think"])送进每帧一次的采集。 - 每次采集重新读取活文本节点,并保留每个节点的完整原文(当前值若是已知全文的前缀,就判定为「我自己的裁剪」,保留全文;否则才是 React 重渲染)。否则 observer 会回采被清空的节点,
total坍缩为 0。 - 一个
requestAnimationFrame循环推进 per-element 游标并按它裁剪:- 生成进行中且已追平 → 基础速率;
- 文本到达比速率快 → 加速,封顶(~6 字/帧);
- 回复在思考还有积压时挂起等待;思考在回复排队时提速;
- 生成结束 → 剩余部分快速追平。
- 一个看门狗只有在循环确实死了 ~1.5s 后才强制还原,保证最坏情况是「没有打字机」,而绝不会什么都不显示或周期性大段 dump。
不卡页面的保障
| 保障 | 防的是 |
|---|---|
<pre>(代码块)不纳入节流 | 语法高亮的代码块有成百上千节点、频繁重建——节流它会把 UI 卡死 |
MAX_NODES 上限 + 提前终止 | 病态元素不会让每帧采集/裁剪变成 O(超大) |
| 采集合并到每帧一次 | observer 反馈(我自己的编辑)不会每帧跑好几遍 O(n) |
| 变更批次守卫(>5000 条) | 病态重渲染直接跳过,不同步处理 |
已知取舍
- 代码块不节流(立即显示)——上面的性能权衡。正文与思考照常节流。
- 折叠态思考不节流——它是实时自动滚动的一行;裁剪它会让滚动跳变、显得断续。展开 Think 行即可看到完整思考逐字打出。
- 模型标签来源:开关匹配的是 composer 模型座的可见文本,不是精确 model id,用出现在下拉里的子串匹配即可。
- DOM 耦合:依赖会话标记的稳定钩子(
[data-streaming]、[data-variant="think"]、[data-follow-end])。DSH 若改名这些选择器,需同步更新。
目录结构
src/client.tsx apply():节拍器 + 设置面板 + 配置存储(浏览器半边)
src/index.ts Node 安全的 host 半边(apply 为空操作)
cordis.patch.yml loader 条目插入(profile bundle 补丁)
build.mjs esbuild → window.__ModuleLoader__.load factory 形式 + host ESM
lib/ 构建产物(gitignore)
package.json 同时声明 dsh.client(web 客户端半边:platform、inject)与 dsh.bundle.patch(使其成为 profile bundle)。构建把 client bundle 包成 DSH 模块加载器要求的 window.__ModuleLoader__.load({ id, factory }) 形式,React 与 @deepseek-ai/* 保持 external,运行时从 shell 的模块表解析。
License
MIT