DSH Plugin Store
Back to home

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
UI
GitHub repo

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-runtimeacceptLiveEvent / 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 贡献)。配置存在 localStoragedsh-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}

节流原理

  1. 一个挂在 document 上的 MutationObserver 把节流容器([data-streaming][data-variant="think"])送进每帧一次的采集。
  2. 每次采集重新读取活文本节点,并保留每个节点的完整原文(当前值若是已知全文的前缀,就判定为「我自己的裁剪」,保留全文;否则才是 React 重渲染)。否则 observer 会回采被清空的节点,total 坍缩为 0。
  3. 一个 requestAnimationFrame 循环推进 per-element 游标并按它裁剪:
    • 生成进行中且已追平 → 基础速率;
    • 文本到达比速率快 → 加速,封顶(~6 字/帧);
    • 回复在思考还有积压时挂起等待;思考在回复排队时提速;
    • 生成结束 → 剩余部分快速追平。
  4. 一个看门狗只有在循环确实死了 ~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 客户端半边:platforminject)与 dsh.bundle.patch(使其成为 profile bundle)。构建把 client bundle 包成 DSH 模块加载器要求的 window.__ModuleLoader__.load({ id, factory }) 形式,React 与 @deepseek-ai/* 保持 external,运行时从 shell 的模块表解析。

License

MIT