DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Client Ui Zero Friction — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
C

dsh-client-ui-zero-friction

Client Ui Zero Friction

零摩擦单输入智能体工作区:一个可将请求路由到实际会话的输入框、注意力队列、学习到的路由偏好,以及摩擦计数器。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:anhaoXie/dsh-client-ui-zero-friction#120437ebdaa808b1e0039153ff1845fb91565d85
README兼容性版本

兼容性与来源证明

Client Ui Zero Friction 以 dsh-client-ui-zero-friction 发布,当前版本为 0.1.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/13

版本

0.1.0stable
2026/10/13

相关插件

正在加载相关插件…

最新版
0.1.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/10/13
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 agents-orchestration 分类下经过校验的插件。

Hindsight Coding Agents@vectorize-io/hindsight-coding-agents面向编程代理的仅反思式 Hindsight 长期记忆(可插拔支持多种运行框架:opencode、……),自动在后台导入(无需设置 CLI)。Ds Botds-botDS Bot:DeepSeek Harness 的一组长期运行的 Bot,支持主 Bot、Bot 间消息和群聊Agency Agents@michengai/dsh-agency-agentsDSH Agency Agents — 为 DeepSeek Harness 提供 331 名可召唤的领域专家DSCODE Desktop@toddzheng024/dscode-desktop为 Harness Desktop 提供编码协作、记忆、浏览器工作流和任务管理功能。

README

dsh-client-ui-zero-friction

零摩擦单框交互 —— 一个真正的 DSH 客户端插件,跑在现有的 DeepSeek Harness Web GUI 里。

侧栏出现 零摩擦 入口,主区域是一个工作区:一个输入框,打完字回车,插件判断这句话该发给哪个真实会话;左侧是按「多需要你看」排序的注意力队列;右侧是学到的偏好与跨会话检索;顶部是摩擦计数器。

关于截图:开发过程中留下了 17 张真实 GUI 的验证截图,但它们拍到了真实会话的标题与结论,属于私有数据,因此没有随仓库发布。本文里的验证记录保留了每条证据的原始描述,去掉了图片路径。


它解决什么问题

对话越来越多是一个趋势,而每一次「点开某个对话框 → 想清楚该发给谁 → 切过去 → 打字」都是一次分叉。分叉是摩擦的主要来源,等待是另一个。

这个插件试着取消分叉,而不是优化它:

  • 不问「你要发给谁」——判断不了就发给最可能的那个,把「不是它?改投给…」摆在手边。误投的代价被压到一次点击。
  • 队列不按时间排,按**「多需要你看」排。只有只有人类能推进**的事才进最上面那一组。
  • 摘要不用额外调模型——主机已经把 projectionValues.turnOutline 算好挂在列表快照上了。

能力与数据来源

这一版没有 mock 数据。所有东西都接到了运行中的应用上:

能力接的是什么状态
会话列表ctx.sessions.list(Session 控制器的反应式快照)✅ 真实
投递消息ctx.sessions.using(id, …).prompt([{type:'text'}], 'queue')✅ 真实
打开会话ctx.uiWorkspace.openSession(id)(走 ctx.get 反射,不占激活依赖)✅ 真实
跨会话检索本地搜 projectionValues.turnOutline(主机全文索引在 web/desktop profile 是关的,见坑 #4)✅ 真实
路由偏好localStorage,按天持久化,可停用✅ 真实
摩擦计数真实记录每一次手动切换 / 自动投递 / 纠正✅ 真实
自动摘要projectionValues.turnOutline:主机维护的逐轮大纲,每轮带 prompt + response✅ 真实
自动命名主机自己给的 title(LLM 生成),队列直接显示✅ 由 DSH 提供
「真的在等你」判定userQuestions.active / plan.pending —— 真信号,不靠猜✅ 真实
进度与健康sessionStats.turns、contextPressure、goal.objective/phase/roundsStarted✅ 真实
自动归入(新话题开新会话)ctx.sessions.create({cwd}):接不住的句子不硬塞进旧会话✅ 真实
路由候选集只含主会话;子智能体(parentId / origin === "subagent")显式排除,标题上带 ↳ 标记✅ 真实
摩擦账本按天累计(localStorage),带一个可证伪的指标✅ 真实
低把握发送前护栏把握 < 65% 时,发送之前就在预览里警示并列出改投目标(一键选定,不拦回车)✅ 真实
没证据就问你标题匹配不上时不再替你猜(旧版会静默"沿用上一次用过的"):按回车弹出选择面板,默认「开个新会话发出去」✅ 真实
就地汇报视图选中会话即在中间栏展开它的逐轮「你问 / 汇报」;不跳页面,子智能体作为"下属"列出来✅ 真实

关键发现:这些全都不需要读消息正文。DSH 已经把一切都算进 projectionValues 里挂在列表快照上了——零额外 RPC、零模型调用、零解析。我原本打算去啃事件窗口(reference.binding.eventSource),探针一跑才发现根本不用。

队列因此从"近似"变成了真判断:

  • 🔴 真的在等你 —— userQuestions.active 或 plan.pending(即使它没在跑)
  • 🟡 正在跑 —— running === true,或队列里还有活等它处理
  • ⚪ 已停下 —— 其余,按最后互动时间排
  • ↳ 子智能体

摘要卡片给的是:目标(🎯 objective · phase · 第 N 轮)、你问、结论、上一轮、轮次、上下文占用,外加「打开」和「让它自己总结」(后者会向那个会话投一句话,是唯一走模型的路子)。


安装

环境:Windows + DeepSeek Harness 桌面版。脚本会在 pwsh 不可用时回退到 Windows PowerShell 5.1,两者都测过。

一键方式:双击 install.cmd。它是幂等的,并且会先备份 profile、写完自校验、失败自动回滚。

install.cmd                              # 装进默认的 desktop profile
install.ps1 -ProfileDir <路径>            # 装进别的 profile
install.ps1 -DshHome <路径> -ProfileDir <路径>   # 完全自定义

手动方式,等价于脚本做的三件事:

  1. profile 的 package.json 里 dependencies 增加一项、dsh.profile.bundles 增加一项:

    {
      "dependencies": {
        "dsh-client-ui-zero-friction": "link:<本仓库的绝对路径>"
      },
      "dsh": { "profile": { "bundles": ["…", "dsh-client-ui-zero-friction"] } }
    }
    

    用 link: 而不是 file: 是刻意的:file: 会被 pnpm 硬链接复制,源码改了 profile 里还是旧的;link: 建立 Junction 指向源码,改动实时生效。

  2. 用 DSH 自带的 pnpm 安装:

    $pnpm = "…\DeepSeek Harness\resources\runtime\pnpm\bin\pnpm.mjs"
    cd ~\.dsh\profiles\desktop
    node $pnpm install
    
  3. 这个包自己是 bundle:dsh.bundle.patch 指向 cordis.patch.yml,里面插入一行 - id: dsh-client-ui-zero-friction name: dsh-client-ui-zero-friction。 行 id、包名、浏览器模块 id 三者刻意完全一致 —— 客户端半区是挂在「说明符恰为包名」的那一 Loader 行上的,三者一致可以消掉一整类挂载失败。

dsh plugin --profile desktop add … 用不了:CLI 明确拒绝,profile "desktop" is managed exclusively by the Electron application。所以走的是改 profile 文件 + pnpm 这条路。

install.cmd 里 .cmd 包装器是必需的:.ps1 默认未签名会被执行策略拦下,而且包装器保证用对编码(见坑 #5)。

启用

先刷新页面(Ctrl+R)。如果侧栏没出现「零摩擦」,再重启 DeepSeek Harness。

为什么可能刷新就够:dsh-client-modules 的宿主半区会监听 Loader 图变化并重新组合浏览器模块名单,而 dsh-hmr 的 profile 配置默认是热重载的。所以安装之后应用有可能已经重组过、刷新即可生效。

为什么也可能需要重启:模块名单是宿主半区在启动时组合进 window.__DSH_BOOT__ 的,如果那次热重载没被触发,就得重启才重编。

注意:不要用不带 token 的 http://127.0.0.1:<port> 打开界面——那只会得到 dsh web authentication required。正确的地址是 dsh web 启动时打印的那一条(带 ?token=…)。

卸载

uninstall.cmd                                  # 移除依赖与 bundle 项
uninstall.ps1 -RemoveNodeModules               # 连 node_modules / lockfile 一起清

卸载后同样需要刷新或重启。

它改动了什么

只改 profile 的 package.json(加一项依赖 + 加一项 bundle),并用 DSH 自带的 pnpm 在 profile 里建一个指向源码目录的 Junction。原始文件备份在 profile 的 .install-backup\ 下。


实测出来的 API 事实

DSH 的客户端插件 API 没有公开文档,下面每一条都是在真实运行的应用里探出来的(window.__ZF_DEBUG__.dump()),不是从压缩代码里猜的。写别的 DSH 插件时能省很多时间。

事实说明
ctx.get(name) 能拿到没声明的服务我用它取 uiWorkspace。ctx.get('uiWorkspace') → 有值,openSession 是函数
ctx.name 取没声明的服务会抛错cannot get property "uiWorkspace" without inject —— 不是返回 undefined。所以漏写 inject 的后果是异常,被 try/catch 吃掉后就变成"功能静默失效"
sessions.list.getSnapshot() → { ids, byId, phase }byId 每项:id / displayTitle / running / blank / updatedAt / cwd / retainedBy / parentId / origin / projectionValues
sessions.using(id, { source }, cb)cb 收到 reference;reference.binding = { sessionId, session, eventSource, ctx };投递用 binding.session.prompt([{type:'text', text}], 'queue') → {ok:true}
sessions.create(opts)直接返回新会话 id 字符串,失败抛 SessionCreateError。opts 是 { workspaceId } 或 { cwd }
sessions.search(query)本 profile 下不可用:{ok:false, error:{code:"gateway/internal"}}(全文索引 openAt: never)
projectionValues.turnOutline逐轮 { turn, seq, prompt, response } —— 现成的摘要来源
projectionValues 其它有用字段title / goal(objective,phase,roundsStarted) / sessionStats.turns / contextPressure / userQuestions.active / inbox / plan.pending / sessionListMetadata.lastPromptAt
inbox 的真实语义装的是排队等 agent 处理的消息(含你自己刚投的那条),非空代表"agent 还有活要干",不是"在等你"(见坑 #8)
eventSource.getSnapshot(){ entries, hasMore, revision, change },entry 是原始事件 {type:"event", event:{type, seq, time, data}}(不是渲染好的消息,别指望直接读文本)
客户端 bundle 形态window.__ModuleLoader__.load({ id: 包名, factory: (require) => {...} }),可 require("react") / ,

踩过的坑

这十条都是真踩过的,其中几条差点让功能"看起来能用其实永远是坏的"。

1. Cordis 只暴露声明过的服务 —— 差点让整个面板变成空的

第一版我写的是 const inject = ["slots"],但代码里用了 ctx.sessions。Cordis 只把 inject 里声明过的服务挂到插件上下文上,没声明的就是 undefined。

后果极具欺骗性:ctx.sessions 是 undefined → 代码走防御分支 → 面板正常渲染,只是永远显示「会话 0」、永远发不出消息。和一个"确实没有会话"的空实例表现完全一样。

修法:const inject = ["slots", "sessions"]。参照系是 DSH 自己的插件: ui-conversation 声明 ["slots","sessions","fileUpload","uiSession","uiWorkspace","locale","configForms"], ui-workspace 声明 ["slots","sessions","workspaces","locale","remote",…]。

uiWorkspace 刻意不声明——它只在点「打开会话」时用到,晚到无所谓,走 ctx.get("uiWorkspace") 反射读取,少一个激活依赖。

tools/verify.cjs 里有一条静态检查专守这个:扫出 bundle 里所有 app.X / ctx.X 访问,凡是不在 inject 里的就报错。

2. 我一度得出「必须重启」的错误结论

因为 GET /plugins/<包名>/client.js 返回 404。那个 URL 是错的。 真实 URL 是 combo 形式:

plugins/??dsh-client-ui-zero-friction/client.js&rev=a740b732a93a

?? 本身既是路径也是查询分隔符,且必须带精确 rev。少了 rev,任何模块都 404,与插件装没装无关。这条假证据一度被当成了结论。

3. 测试台曾经"印证"了一个错误的返回值假设

真机实测发现:会话确实建出来了,但界面报「投递失败:新建会话没有返回 id」。

原因是猜 sessions.create() 返回 { ok: true, value: { sessionId } },而真机是直接返回 id 字符串、失败时抛错(ClientSessions.create 的最后一行就是 return result.value.sessionId)。

真正的问题不在猜错,而在假 sessions 服务也用了我猜的那个结构 —— 于是 49 项回归全绿,却完全测不出这个错误。测试台在用我的假设验证我的假设。

修法:把假服务改成照抄真机契约(返回字符串、失败抛错),并在注释里写明"必须照抄,否则测试台会印证错的假设"。

教训:mock 的返回值必须来自真机观测,不能来自"我觉得应该是这样"。

4. 「跨会话检索」曾经是一个永远返回空的功能

第一版直接调 ctx.sessions.search(query),README 里还写着「接主机的内容索引」。真机一探:

{ "ok": false, "error": { "code": "gateway/internal", "isDSHRemoteError": true } }

web/desktop profile 的主机全文索引是关闭的 —— dsh-web-app 的 patch 把 session-query-sqlite 设成了 openAt: never("Full-text session search is opt-in")。所以那个功能一次都没成功过,而且在界面上表现得只是"没命中"——静默失效,比报错更糟。

修法:改成本地检索。主机已经把每一轮的 prompt/response 算进 projectionValues.turnOutline 了,直接在内存里搜即可 —— 零 RPC、零延迟、不依赖任何主机开关。顺带把「边打边搜」做了(不用按回车),并加了「探测主机索引」按钮:它把主机的真实回答原样报出来,而不是让功能继续静默失效。

教训:一个依赖外部开关的功能,必须有一条路径能让它把"我这里没开"说出来。

5. 安装脚本会把应用弄到起不来(写 BOM)

第一版 install.ps1 用了:

$json | ConvertTo-Json -Depth 12 | Set-Content $pkgFile -Encoding UTF8

在 Windows PowerShell 5.1 下,Set-Content -Encoding UTF8 会写进 UTF-8 BOM。而带 BOM 的 JSON,DSH 的 Loader 解析不了:

❌ Unexpected token '', "{ "n"... is not valid JSON

.cmd 包装器在找不到 pwsh 时会回退到 powershell(5.1),所以这条路真的会被走到。如果当初直接双击 install.cmd,重启后应用就起不来了——而症状会指向 DSH 本身,不是这个脚本。

三个修法叠在一起:

  1. 写文件改用 [System.IO.File]::WriteAllText(... UTF8Encoding($false)) —— 明确不要 BOM
  2. 写后自校验:读回来检查首字节不是 BOM、并且 ConvertFrom-Json 能解析;失败就立刻用备份回滚并中止
  3. .ps1 本身加 UTF-8 BOM —— 因为 5.1 读 BOM-less 文件按 ANSI 解,中文注释会让它报"括号不匹配"

顺带发现 .cmd 里不能写中文注释:cmd.exe 按 OEM 代码页读 .cmd,中文字节会被当成命令。

教训:一个会修改"应用启动所依赖的配置"的脚本,必须自己校验自己写出来的东西,并且能回滚。

6. 测试台渲染了「上一个组件」的状态,让规模测试说谎

写规模用例时构造了 200 个会话 × 40 轮,结果它 2ms 跑完、摘要卡片 0 张——因为实际渲染的是上一次那个 4 会话的组件状态。

原因:mini-React 把 hooks 按调用顺序存在一个数组里,渲染第二个组件时没有重置。两个组件的 hook 序列不同,于是 useSyncExternalStore 读到了上一个组件留下的快照。

修法是加 RUNTIME.reset(),并且在渲染另一个组件前必须调用。加完之后规模用例才真的开始测 200 个会话——立刻又抓出两个真问题:摘要面板没封顶(200 张卡片)、队列的「正在跑」分组没封顶(53 张卡片)。

教训:测试台自己出错时,比没有测试台更危险——它会给你绿灯。

7. 「连点两次回车」的竞态,以及测试台一度测不出它

零确认界面里没有对话框兜底,所以同一条消息投递两次是很伤的。闸门原来用的是 React state:

if (text === "" || busy) return;   // busy 是 state

state 是异步的:两次事件落在同一次渲染周期里,会都读到 busy === false,于是投递两次。改成同步的 busyRef,并且把整个函数体放进 try/finally(否则路由里任何一处意外抛错,busyRef 就永远停在 true,面板从此再也发不出消息)。

有意思的是测试这条的过程中,测试台骗了我两次:

  1. 第一版测试捕获同一个按钮元素点两次 —— 抓到了 bug。但后来"改进"成每次重新取元素,测试就再也抓不到了:mini-React 在 setState 时同步重渲染,第二次取到的是 busy=true 的新闭包。真实 React 是批处理的,不会这样。
  2. 更要命的是,我一度以为这条测试有效——直到按纪律把 busyRef 删掉重跑,它照样全绿。

所以那条测试现在是刻意"捕获同一个闭包"的写法,注释里写明了为什么必须这样。

由此得出的纪律:每一条守卫 bug 的测试,都必须先证明它在未修复的代码上会变红。 做不到这一点,那条测试就只是一句安慰。

8. 我把 inbox 的语义搞反了 —— 而且离线测试一直在替我背书

「真的在等你」这个分组里,原来把三个字段都算作"需要你":

if (activeQuestions.length) needs.push(...);           // agent 提问 → 只有你能答  ✓
if (inboxCount) needs.push(inboxCount + " 条排队中");    // ✗ 语义反了
if (planPending) needs.push(...);                      // 计划待批 → 只有你能批  ✓

真机实验(让会话进入 running,再投一条,看投影怎么变)给出了答案。inbox 里装的是:

"next-turn": [ { "content": [ { "type": "text",
  "text": "……第二条:这条应该被排队" } ] } ]

那是刚投进去、排队等 agent 处理的消息。 inbox 非空意味着"agent 还有活要干",不是"你在等"。

后果很实在:那个会话本来在正确的「🟡 正在跑」,被代码抬到「🔴 真的在等你」并标上「需要你:1 条排队中」——而它什么都不需要你做。注意力队列最不能有的就是这种噪音。

更正后:needs 只留 userQuestions.active 与 plan.pending(两者都意味着 agent 停下来等你);有排队工作的会话归入「正在跑」,并以中性提示「队列里还有 N 条等它处理」呈现。

这件事的方法论意义比 bug 本身大:那条离线用例之所以一直是绿的,是因为它断言的就是我那个错的信念——假数据由我构造,于是它忠实地印证了我的误解。只有拿真机投影做实验才能推翻它。

顺带一提,这个实验不需要凭据:让会话进入 running、再投一条,排队就会发生——"没有模型"并不妨碍验证数据模型。

9. 学习回路在第一次真机使用时,就学出了一条会"越用越错"的规则

「改投一次 → 记住这个特征」是这个插件的核心承诺。它从没在真机上点过,所以去点了。

机械上是通的(重投成功、纠正计数 +1、账本记下),但持久化里躺着的规则是:

{ "match": "工具", "sessionId": "…视频摘要与口语练习工具" }

消息是「每日领域前沿信息自动报告工具 再加一个数据源」,改投目标是「视频摘要与口语练习工具」。两者唯一共享的 2 字片段就是**「工具」。于是以后任何带「工具」的句子都会被吸到那个会话**——包括「帮我做个工具…」。

这条规则会持续地把东西投错地方,而且它会以"我学聪明了"的面目出现。

改法(两层):

  1. 关键词只能来自"消息与目标标题真正共享的片段",删掉"从消息里任意取一段中文"的回退路径 —— 那条路会学出与目标毫无关系的词。
  2. 排他性检查:这个片段如果在别的会话标题里也出现(或属于明显泛化的词表),就不写规则。

学不到的时候如实说:「这句话和「X」没有共同特征,所以只改了这一次。一条不准确的偏好会长期把东西投错地方,比这次不学更糟。」

取舍说明:这样一来能学到的规则变少了。这是刻意的——一条错的规则会静默地长期投错,而少一条规则只是让你再纠正一次。

10. 一个看起来像插件崩溃、其实是测试点错了的假警报

第一次跑改投实验时,点完候选之后面板整个消失。看起来像渲染崩溃。按纪律去抓浏览器异常——一条都没有。

没有异常才是线索:面板消失不是崩了,而是应用导航走了。点击助手搜的是整个文档,而 DSH 自己的左侧会话列表里有同名会话、且它在 DOM 里更靠前 —— 点中的是原生侧栏。

把点击范围收进 .zf-reroute 之后一切正常。

教训:在宿主页面里做测试,选择器必须限定在自己的容器内;宿主的 UI 里到处是同名的东西。

11. "让匹配读懂意思" —— 我试了内容匹配,真机两轮把它否掉了

有人指出路由"匹配度不太对,希望读懂意思,而不是文字表面匹配"。当时的算法是:

for (const gram of grams(row.displayTitle))   // 只把会话**标题**切成 2/3/4 字片段
  if (text.includes(gram)) score += gram.length;

三个缺陷:只比标题不比内容;没有证据时静默替你选("沿用上一次用过的");纯字面。

第一步我加了内容匹配:把会话最近几轮的内容也切成词元,消息在标题里找不到证据时再去内容里找。听上去对,真机跑了两轮:

第一轮(词元取 2/3 字):太松。「量子计算机的散热方案该怎么设计」被一个无关会话以「内容匹配 · 62%」吸走——因为"怎么""设计""方案"在任何长会话里都有。一个 62% 的假阳性比一句"没把握"更糟,因为它会跳过询问。

第二轮(词元改成 3/4 字):该匹配的匹配不上了。「归档是怎么回事」不再命中任何会话——而"归档"恰恰是那个会话真正的关键词,只是它只有 2 个字。

结论:字面匹配在长中文文本上无法区分"有意义的短词"和"到处都有的虚词"。 所以最终做法是把它降级——内容相似度只排序,不决定:

  • 路由的决定只看标题匹配与学到的偏好;两者都没有,就是"没把握",交给人
  • 内容相似度只用来给询问面板排序(回答"如果是这几个,哪个最像")

代价是"读懂意思"这件事没有真正做成,这里不含糊:真正的语义路由要过模型。插件侧没有可用的语义服务,唯一能过模型的路是"往某个会话投一句话",那要花掉一轮、还有延迟。所以现在的答案是:宁可问你一次,也不假装懂你。

顺带一个方法论的教训:这两轮失败都不是靠推理发现的,是在真实会话上跑出来的。离线夹具里没有长中文内容,第一轮的假阳性根本不会出现——type() 一句话,真机就把它打回来了。


变异审计:证明每条守卫测试都会红

第 7 条那个纪律光立不够,所以这里把它做成了可重复运行的审计:

tools\mutation-audit.cmd      # 逐个把修复退回去,跑套件,看是否红在预期的测试上

十四个变异,十四个都红在它该红的那条上:

变异(把修复退回)红在哪条测试
1 去掉 sessions 声明声明了 sessions 服务
2 create 返回值解析退化新话题真的开了一个新会话
3 路由不再排除子智能体命中子智能体标题时也不会投给它
4 本地检索返回空边打边搜:不需要回车就已经出结果
5 摘要面板不封顶摘要面板默认封顶在 12 张(实际 200)
6 子智能体不带 ↳ 标记子智能体在标题上带 ↳ 标记
8 检索结果不截断规模检索命中并被截断渲染(实际渲染 8200 行)
9 HUD 去掉「本该打断你」HUD 显示「本该打断你 N 次 · 实际 0 次」
10 默认页签改成偏好摘要面板默认展示
11 账本表去掉「本该打断」列账本表头是 日期/投递/新建/纠正/本该打断
12 封顶提示去掉数字封顶处给出「还有 N 个」提示
13 把排队当成需要你有排队工作的会话不算「需要你」
14 关键词不再要求与标题有共同特征有共同特征时学到规则,且指向被改投的那个会话
15 去掉排他性闸门特征在别的会话标题里也有时,不写规则
16 去掉发送前的低把握警示把握低于 65% 时,发送前就出现警示
17 选中不再钉住发送目标钉住后真的投给被钉住的会话
18 看汇报改回跳页面点「看汇报」不会打开原生界面
19 汇报视图不显示下属汇报视图里点名了它手下的子智能体
20 询问面板不按内容排序(退回按时间)面板把内容最接近的排在第一位
21 没证据时不问、直接发没证据时按回车不发送,而是把选择面板摆出来
22 没证据时退回"选上一次用过的"没证据时如实标注「没有把握」

第 07 条已移除(编号保留以示来历):它守的是"剥尾部虚词"的分支,而第 9 条那个改动删掉了"任意取一段中文"的回退路径之后,这个分支成了防御性代码,现有夹具再也触发不到。按纪律,审计里不留不会红的条目。

第 23 条同样已移除:它守的是"内容匹配的最小片段长度",而第 11 条那个改动把内容相似度降级成"只排序、不决定"之后,这个长度不再影响任何决定,夹具触发不到它。

审计本身抓到两件事

一是弱断言。 原来「边打边搜」断言的是整页文本里含会话标题——可队列里本来就有那个标题,于是检索返回空它照样通过。同理「摩擦计数器显示『二次确认 0』」:那个词早就从 HUD 里换掉了,它能通过是因为输入框下方的提示文案里有「不做二次确认」——它测的是一句文案。这两个都属于同一类:断言的字符串在别处也出现。

二是"失败时把整轮跑挂"。 变异 10 让套件直接崩溃而不是干净地变红。根因是一行详情表达式:

"第一张: " + JSON.stringify(...).slice(0, 150)

JSON.stringify(undefined) 返回的是 undefined 值而不是字符串,.slice 就抛了。这条检查恰恰在它失败的时候把整轮跑挂——比弱断言更糟,因为它连"哪里坏了"都不告诉你。修完,同一个变异从"崩"变成干净的 9 条失败。

审计工具自己也踩了一次:它最初没有 finally 恢复,变异 10 一崩就把源码留在了变异状态。现在恢复写在 finally 里,而且子进程写 stderr 不再中止审计。


验证记录

不是"我觉得能跑",是真的在运行的 DSH 里跑过。下面每一条都是实际执行过的。

#验证项证据
1官方 CLI 接受这个包并安装在隔离 profile 里 dsh plugin --profile zfcheck add <路径> → + dsh-client-ui-zero-friction,exit 0,自动用 link:
2组合进 profile 树dsh --profile zfcheck --dump-config → exit 0,行 - id: dsh-client-ui-zero-friction
3进入真实浏览器模块名单从运行中服务器的 __DSH_BOOT__ 里读出 {"id":"dsh-client-ui-zero-friction","url":"plugins/??dsh-client-ui-zero-friction/client.js&rev=a740b732a93a","rev":"a740b732a93a","inject":["@deepseek-ai/dsh-api-session-controller"]}
4模块真的能取到用上面的 combo URL 请求 → HTTP 200,27022 字节,内容就是 window.__ModuleLoader__.load({ id: "dsh-client-ui-zero-friction" …
5浏览器里真的激活无头 Chrome 打开真实 GUI → 侧栏出现 零摩擦
6主面板真的渲染点开后完整出现工作区标题、摩擦计数器、三列布局、输入框与发送按钮
7降级路径在真实 GUI 里成立零会话时回车 → 界面原样输出「还没有任何会话,先在正常界面里开一个对话。」
8逻辑回归node tools/verify.cjs
9修正 inject 后插件仍能激活隔离实例里侧栏「零摩擦」照常出现——这本身就是声明正确的证明:Cordis 等不到 sessions 就根本不会挂载这个插件
10真实会话进了队列新建一个会话后,面板显示 会话 1,队列出现「已停下,等你看 / default-workspace」
11真实路由输入后路由预览实时显示「→ default-workspace · 延续当前会话 · 50%」
12真实投递成功回车后回执显示**「已投递给「default-workspace」」**,并且 DSH 自己的左侧会话列表同时出现了这条消息,还据此自动给会话起了标题
13摩擦计数器HUD 显示 自动投递 1,与第 12 项那次真实投递一致
14真实会话全部读到把真实 sessions + storages 复制进隔离 DSH_HOME 后,队列显示 8 个真实会话
15真实自动摘要摘要面板对每个真实会话给出:目标(active · 第 3 轮 / complete · 第 1 轮 / blocked · 第 40 轮)、你问、、、轮次、上下文占用

最终验收的做法

验证分两类环境:

  1. 隔离的 web profile 实例(复制真实 sessions 与 storages,独立 DSH_HOME)——用于功能逐项验证。
  2. 真实 desktop profile 的副本(同样的 cordis.yml / package.json / node_modules / 会话数据)——用于最终验收。

第二类是最接近真实使用环境的:同样的 bundle 列表、同样的依赖链接、同样的会话数据。

还没在真机上验过的

🔴 「真的在等你」里 userQuestions.active 与 plan.pending 两个字段的真机触发。 这两个字段只在 agent 真的提问或提交计划时才非空——那需要一个能跑通的模型,而开发环境刻意没有配置凭据(也不应该拿别人的凭据去跑)。这一条由离线用例覆盖,用的字段结构来自真机 dump;分组代码路径与「正在跑」完全相同,而后者已经真机验证过。


开发

node tools/verify.cjs          # 离线回归(86 项)
tools\mutation-audit.cmd       # 变异审计(14 个变异)

tools/verify.cjs 把真实的 lib/client.js 装进一个假客户端环境里跑:模拟 window.__ModuleLoader__、一个最小 React 运行时(hooks + 渲染 + 事件)、以及一个假 ctx(slots + sessions)。它验证的是:

  • 模块契约:是否调了 __ModuleLoader__.load、模块 id 是否等于包名
  • 静态检查:有没有"用到却没声明"的服务(坑 #1)
  • slot 注册:main 的 key 是否等于侧栏 sidebar.panellist 的 id
  • 渲染:首次渲染不抛错、真实会话标题出现、分组正确、规模下不膨胀
  • 交互:路由预览、回车投递真的打到正确的那个会话、走 queue 模式、改投真的重投
  • 学习:纠正后写入偏好、下一次路由真的命中;不可靠的特征不写偏好
  • 检索:本地搜大纲命中,且零 RPC
  • 降级:零会话、以及 sessions 服务整个缺失时,渲染都不能抛错

改完 lib/client.js 直接重跑这个脚本,比重启应用快得多。

设计要点(为什么这么做)

  1. 取消选择,而不是优化选择。 路由不问「要找谁」;判断不了就发给最可能的那个,把「不是它?改投给…」摆在手边。
  2. 路由是纯函数。 routeMessage(text, rows, prefs) 不碰网络、不碰 React,所以能被 verify.cjs 直接测。这是它能被反复调参的前提。
  3. 学到的偏好必须可见、可停用。 一个偷偷学习的黑盒和没有学习是一样的——用户不会信它,也就不会用「不是它」这个动作,学习回路就断了。
  4. 任何服务缺失都不能让界面炸掉。 sessions 拿不到就少一块能力、显示一行说明,而不是白屏。
  5. 该短就短。 唯一保留摩擦的地方是不可逆动作;除此之外不该有「请确认」。
  6. 宁可少学一条,也不要学错一条。 错误的偏好会静默地长期投错地方,而缺失的偏好只是让你再纠正一次。
  7. 护栏放在"发送之前",不是"发送之后"。 投错一条消息会污染那个会话的上下文,还要白烧一轮算力 —— 那是不可逆的。所以把握 < 65% 时,警示和改投目标出现在你按回车之前;但它不拦你:按回车照样发给最可能的那个。零摩擦的意思是"不制造分叉",不是"看着你投错也不吭声"。
  8. 层级式汇报:只跟"上级"说话。 选中一个会话就等于指定它是你的对接人 —— 输入框永远是同一个,它的逐轮汇报就地展开在中间栏,子智能体是它的下属。主按钮是「看汇报」(不跳页面),原生界面 降级成次要入口。真要跳走的时候你才跳。
  9. 打平局时选最近用过的那个。 两个会话标题匹配分数相同时,旧代码保留数组里靠前的那个,而那个顺序来自主机、语义未知。改成按 updatedAt 打平局:更像答案,而且可预测。
  10. 没证据就不猜。 标题匹配和学到的偏好都给不出证据时,旧版会静默替你挑一个(先是"延续当前会话",再是"接到最近会话")。现在如实标成"没把握",把选择摆出来,默认答案是开新会话。唯一例外是看起来在追问的短句(很短、且没有新话题特征),那接着当前会话说是合理的——但仍然会被问一次。
  11. 相似度只能排序,不能决定。 内容相似度用来回答"如果是这几个,哪个最像",由人拍板。它一旦参与决定,就会在长中文文本上产生 62% 这种看起来很确定的假阳性,而那比"没把握"糟得多——因为它跳过了询问。

已知限制

  • 摘要不是重写过的综述。 现在直接展示主机大纲里那一轮的原文(已截断),够用来判断"要不要点进去"。想要真正的一句话综述,得让某个会话来生成——所以「让它自己总结」那个按钮就是这条路,代价是花掉那个会话一轮。自动(免模型)和综述(花模型)选了前者做默认,因为它零延迟、零成本,而且原文常常比综述更可信。
  • 路由是字面的,不是语义的。 它比对标题片段与学到的偏好,不理解同义词、指代和言外之意。这是刻意的取舍:真正的语义路由要过模型(一轮 + 延迟)。缺的那部分用"没证据就问你"补上(见坑 #11)。
  • 「真的在等你」的两个字段未在真机触发过(见上)。
  • 只在 Windows 上测过。 install/uninstall 脚本依赖 PowerShell;客户端插件本身是跨平台的。
  • 开发探针留着没删:window.__ZF_DEBUG__.dump(i) 会把第 i 个会话的完整 projectionValues 和事件窗口结构原样吐出来。会话事件窗口没有文档、磁盘上的会话文件又是二进制,这个探针是唯一可靠的信息来源。只读,不改变任何状态。

许可

MIT

require("react/jsx-runtime")
无需构建工具链
模块 URLplugins/??<id>/client.js&rev=<hash> —— ?? 既是路径也是查询分隔符,少 rev 必 404
bundle patch包根放 cordis.patch.yml,package.json 里 dsh.bundle.patch 指向它;包名加进 profile 的 dsh.profile.bundles
结论
上一轮
16摘要内容正确抽出的结论与真实会话内容一致,例如「LB idle timeout 已调平」「88 项操作全部成功,0 失败」「五个功能全部做完并端到端验证通过」
17「真的在等你」由真信号驱动离线用例:带 userQuestions.active 的会话即使 running === false 也进入该分组,卡片显示「1 个问题等你回答」
18真实自动开新会话真机输入新话题 → 会话数 2→3、回执显示「新话题 · 自动开了新会话」、HUD 新建会话 1
19真实摩擦账本真机账本页显示 10-12 投递 1 新建 1 纠正 0 本该打断 1,合计行一致
20「本该打断 vs 实际打断」可证伪同一次投递里把握度 35%(旧设计会问),HUD 显示「本该打断你 1 次 · 实际 0 次」——这个数字会随真实使用增长,所以「0 次打断」不是空话
21修掉 create 契约错误后重验见坑 #3:这个 bug 是真机测出来的,测试台当时是全绿的
22探出检索是死功能真机 search('502') → {ok:false, error:{code:"gateway/internal"}}(见坑 #4)
23本地检索在真实大纲里命中真机搜「零摩擦」→ 命中「… · 你问 · 第 2 轮」,高亮 1 处;零 RPC
24主机索引如实报告点「探测主机索引」→ 日志原文「主机全文索引在这个 profile 里是关闭的(gateway/internal)」
25在真实 desktop profile 的副本上完整工作把 ~/.dsh/profiles/desktop 整体复制到隔离 DSH_HOME 并从 CLI 启动(不是 web 模板)→ 侧栏入口、8 个真实会话、自动摘要、goal 状态(active · 第 6 轮)全部正常
26install / uninstall 端到端可用在一个全新 profile 上跑完整循环:安装 → dump-config exit 0 且行入树 → 卸载 → 行移除且 JSON 始终可解析、无 BOM
27安装器的自校验真的会回滚见坑 #5:BOM 那个 bug 就是被这条检查暴露的
28「打开会话」真机可用点摘要卡片的「打开」→ 主视图切走了(面板还在: false),并显示出那个真实会话的完整对话记录。路由 → 投递 → 打开,三件事全部真机验证完毕
29打开失败不再静默openSession 抛错时会在日志里显示「打不开这个会话:…」;离线用例覆盖(含一条把 openSession 换成会抛错的桩的用例)
30规模:200 个会话 × 40 轮渲染 2ms、不抛错;「真的在等你」19 个全部渲染(刻意不封顶),其余分组封顶(共 35 张卡片,不是 200 张);摘要面板封顶 12 张并给出「显示全部 200 个」,点开后确实渲染 200 张
31规模下真机仍正常复制真实会话启动隔离实例,队列与摘要照常渲染
32子智能体不会被当成投递目标一条通篇在说子智能体标题的消息,不会被投进那个子智能体;改投候选里也没有它。离线用例覆盖(真机测不了:需要一个真的子智能体,那要有模型)
33过滤改动没误伤主会话(真机)复制真实会话启动实例:无匹配的消息 → 判定「新话题」并真的开了新会话;匹配标题的消息 → 标题匹配 90% → 成功投递。两条分支都在真机上验过
34连点两次只投递一次见坑 #7:这条测试在未修复的代码上确实是红的(实际投递 2 次),修复后为 1 次
35失败之后闸门一定被释放让 sessions.create 抛错走真实失败路径:如实报错、不误投到别的会话、草稿被还回来,且下一次投递仍然成功
36规模检索的成本200 会话 × 40 轮(8000 条大纲)下一次按键 13–18ms,命中被截断到 60 行渲染
37守卫测试本身是真的变异审计:全部红在预期的测试上。tools\mutation-audit.cmd 可重复运行
38审计抓出弱测试并修掉见「审计本身抓到两件事」
39审计扩到 12 个变异,全中新增的 4 个针对加强过的断言(HUD 指标 / 默认页签 / 账本表头 / 封顶提示)
40修掉"失败时把整轮跑挂"变异 10 曾让套件崩溃而非干净变红。修完变成干净的 9 条失败
41审计工具自身的健壮性恢复源码写进 finally;子进程 stderr 不再中止审计;套件崩溃单独归类
42投递真的启动了一个 agent 回合(真机)经插件投一条消息进真实会话后:running 从 false 变 true,turnOutline 从 18 条涨到 19 条,lastAgentError/promptError 仍是 null。不只是 prompt() 返回 ok
43「🟡 正在跑」分组真机生效同一个会话在投递后实时从「已停下」移到「正在跑」,卡片显示「在跑 · 刚刚 · 16 轮 · 上下文 62%」
44摘要随回合实时增长新回合一进 turnOutline,摘要面板第一张卡片立刻出现新的「你问」。摘要不是快照,是跟着主机投影走的
45真机实验推翻了 inbox 的语义让会话 running 后投第二条 → inbox.next-turn 里是刚投进去的那句话(见坑 #8)
46修正后真机复验有排队的会话留在「🟡 正在跑」,显示中性提示「队列里还有 2 条等它处理」;不再出现在「真的在等你」
47变异审计扩到 13 个新增变异(把排队重新算成"需要你")→ 红在对应测试
48改投交互真机跑通点「不是它?改投给…」→ 候选列表正确(排除被改投的那个、排除子智能体、含「取消」)→ 重投成功、纠正 1、账本记下
49真机发现学习回路会学出泛词第一次真机改投学到的关键词是「工具」,会劫持以后所有带「工具」的句子(见坑 #9)
50修正后真机复验同一场景:rules: []、learned: 0、纠正 1——不再写出那条泛词规则;下一句正常路由,没有被坏规则劫持
51变异审计扩到 14 个新增两个变异均红在对应测试上;同时删掉已过时的第 07 条并注明原因
52最终验收:15/15(真实 desktop profile 的副本)面板加载 → 真实队列 → 真实摘要 → 本地检索命中真实大纲 → 投递回执 → 改投候选 → 纠正记账 → 不学泛词 → 摩擦账本 → 打开会话切换主视图 → 全程零未捕获异常
53低把握发送前护栏(真机)输入「改版的事情」→ 预览显示「延续当前会话 · 50%」→ 出现警示「⚠ 把握只有 50%,可能投错」+ 三个一键改投按钮(列出真实会话标题);此时没有发送
54发送前改投(真机)点「改投「零摩擦会话插件」」→ 预览变成「→ 零摩擦会话插件 · 你指定的 · 100%」,警示消失,仍然没有发送
55就地汇报视图(真机)点队列卡片 → 中间栏展开该会话的逐轮「你问 / 汇报」(真实内容),头部显示「已停下 · 16 轮 · 上下文 13%」+「改回自动」「原生界面打开」;面板没有被导航走(document.querySelector('.zf-root') 仍存在)
56改回自动(真机)点「改回自动」→ 汇报收起、预览恢复「延续当前会话 · 50%」,回到自动路由
57没证据就问你(真机)输入「量子计算机的散热方案该怎么设计」→ 预览「没有把握 · 没有任何会话和这句话有共同点 · 20%」→ 回车不发送,弹出面板:[开个新会话发出去] [发给「DSH询问」] [发给「零摩擦会话插件」] [发给「default-workspace」] [取消];日志确认没有回执
58选了就真的开新会话(真机)点「开个新会话发出去」→ 回执「已投递给「新会话」· 没有像的,你选择开新会话」,会话数 8→9,HUD 新建会话 1
59账本新增「问你」列(真机)账本显示 10-13 投递 1 新建 1 纠正 0 问你 1 本该打断 0,与第 57 步一致
60内容匹配的假阳性是被真机否掉的见坑 #11:第一轮「量子计算机的散热方案该怎么设计」被无关会话以 62% 吸走;第二轮「归档是怎么回事」又匹配不上了。最终把它降级成"只排序、不决定"