Development & Runtime

dsh-discuss-mode

buguoshixc/dsh-discuss-mode · ★ 1

buguoshixc

Discussion mode for DeepSeek Harness: the agent consults as a domain expert before development, acting tools are denied while the mode is active, only the user can leave it…

README body is shown in Chinese.

License
MIT
Added
2026-10-30
Maintenance
active
Install kind
github
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:39:10.123Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add 'github:buguoshixc/dsh-discuss-mode#94e8c0e397e149f37639648536224573b9de3198'

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: zh · Read Chinese README

Table of contents
  1. 用法
  2. 侧边栏面板
  3. 命令
  4. 模式下拉框(Agent 预设)
  5. 收敛时拿到什么
  6. 为什么这样设计
  7. 退出权不对称 —— 这是要害
  8. 换的是终点,不是速度
  9. 硬拦截,而不是好言相劝
  10. 状态从会话日志折叠,而不是另存一份
  11. 配置
  12. 兼容性
  13. 安装
  14. 已知边界
  15. 实现速览
  16. 推送(网络受限时)
  17. 验证

在动手开发之前,先和一位行业专家把想法讨论清楚——而不是被一个急于执行的助手敷衍过去。

DSH 里模型天然是任务导向的:它倾向于尽快收敛成方案、尽快开始写代码。平时这没错,但在"我还不知道要做什么"的阶段,这种倾向恰恰是最没用的。这个插件给会话加一个讨论模式:模型换成资深从业者的姿态,动手类工具被直接拒绝,而退出讨论模式只能由你发起。

用法

侧边栏面板

装好并重启后,左侧栏会多出一个「讨论模式」图标(排在自带的 Plugins、Schedule 之后)。点它打开面板:

区域 作用
状态 实时显示当前会话是否处于讨论模式
按钮 进入 / 退出。点完自动回到对话——因为进入模式之后,你要做的正是说出那个还没想清楚的想法
策略原文 把 AI 在此模式下被告知的内容摊开给你看
什么时候用它 三种典型场景

面板上的按钮不是另一套状态机:它执行的就是下面这条命令本身。

命令

/discuss            进入讨论模式
/discuss <你的想法>  进入,并把这句话作为讨论的开场
/discuss off        退出

命令也会出现在 Web 输入框的 / 菜单里,所以在对话中直接敲更顺手——进出都不必离开输入框。

模式下拉框(Agent 预设)

输入框上方的模式下拉框——和「标准模式 / PTC 模式 / 极简模式 / 创造模式」同一处——里也有一条讨论模式。那是 DSH 的 Agent 预设:一条预设定义的是一整个会话的插件组装,所以它的语义和 /discuss 不完全一样:

模式下拉框 /discuss、侧边栏面板
作用对象 新会话(选的是"这条会话用什么组装") 当前会话
生效时机 一进去就在讨论模式里,不用先敲命令 敲下去那一刻
能否改主意 会话一旦开始就不能换预设(DSH 拒绝给已开始的会话换组装) 随时进、随时出
退出之后 就是完整的标准会话——组装本来就是标准模式那套工具面 同左

所以它是入口,不是第二套状态机:退出仍然只有你能发起,/discuss off 与面板按钮在这条会话里照常管用。

这条预设的组装是标准模式工具面的快照(presets/discuss.patch.yml),理由就是上表最后一行——出讨论模式之后你还得能干活。DSH 没有"引用另一条预设"的写法,所以那份清单是抄来的:它抄自安装里 @deepseek-ai/dsh-web-app@0.2.1-rc.2 的 presets/standard.patch.yml,夹在文件里的 BEGIN/END 生成标记之间。这次对齐还修掉一处会炸的差异——0.2.1 移除了 {{cwd}} 提示词变量,而旧快照里 persona 的 suffix 用了它,讨论模式的会话会因此连第一次请求都发不出去。

升级 DSH 之后想让两边一致,重抄也是命令化的:

$env:DSH_APP_ASAR='D:\DSHD\resources\app.asar'
node tools/sync-preset.mjs            # 就地重写生成区
node tools/sync-preset.mjs --check    # 只比对:过期时 exit 1,一致时 exit 0

--asar <path> 也可以代替 DSH_APP_ASAR;工具只改写生成区,文件顶部的说明注释和生成标记保持原样。拿不准两边是否一致时先跑 --check。

收敛时拿到什么

讨论快到头的时候,它先摆出一版 MVP 草案:做什么、不做什么、最大风险在哪,外加它替你补的假设和自己拿不准的地方;然后停住:「范围我理解对了吗?你还有没有没说完的?」——你明确点头之后,才交出一段交接 prompt,单独一个代码块,可以直接粘进一个全新的会话。新会话里的执行者看不到你们说过的任何一句话,所以那段 prompt 要求自足:目标与验收、范围、已经定下的决定、已知的坑、从哪下手。

这是刻意的分工:讨论留在这里,动手去那边做。讨论模式的价值在于判断没有被"顺手开始实现"冲掉;把结论写成一段能带走的东西,比让同一个会话继续跑下去更贴近这个目的——没有它,讨论的产物就只是聊天记录。而"聊完了没有"这个判断交回给你:草案只是提案,点头才算成交。

为什么这样设计

退出权不对称 —— 这是要害

plan mode 让模型调用 exit_plan_mode 来结束规划。它仍然是一个任务形态:模型在为你交回一份可执行的计划。

讨论模式反过来:模型没有任何结束这个模式的工具。唯一的出口是你在面板上点按钮,或在输入框敲 /discuss off。

理由很直接:如果一个"我要开始实现了"的动作可供调用,一个急于执行的模型就会去抓它。把可供性拿掉,急迫感在结构上就没有落脚点——这比在提示词里写一百遍"不要着急"都管用。模型仍然可以用自然语言说"我觉得聊得差不多了",但决定权在你手里。

换的是终点,不是速度

这是设计上最容易搞错的一点。讨论模式不是"慢下来",而是收敛到别的东西:

终点
默认(无模式) 代码
讨论模式 一个有依据的判断 + MVP 范围
plan mode 一份已批准的计划

所以要防的是三种失败,而不只是一种:

  1. 急着动手——判断还没立住就去写代码
  2. 急着交付——把"我懂了"当成"聊完了",用户还在说,草案和交接 prompt 已经摆出来了
  3. 用提问拖延——每轮抛一堆澄清问题、列一堆权衡、永远不表态。这只是把讨好换了个形式,而且更省力,因此更常见

前两种的机制不一样:动手是工具调用,所以能被 ctx.tools.guard 硬拦;交付是生成的文本,不经过任何工具,没有可供拦截的调用点,只能在话轮协议里把"什么时候算聊完"交回用户。策略文本里写死了两件事:"不允许连续两轮只提问不给判断",以及收敛的先后顺序——判断随时给 / 对齐(草案 + 两条确认 + 停)/ 用户明确点头后才交付 prompt。

硬拦截,而不是好言相劝

只靠提示词约束行为是不可靠的。模式激活时,一个单调的 ctx.tools.guard() 会拒绝动手类工具,并把拒绝理由返回给模型:

  • 被拒绝:write、edit、pwsh、bash、subagent、subagent_fork、workflow、spawn_teammate、send_message、create_goal、update_goal、job_kill、schedule_*、agent_teams_*、team_task_*
  • 照常可用:read、glob、grep、read_image、web_search、web_fetch、ask_user_question、todo_write、skill、各类只读的 list/status 工具

只读探索是故意保留的:讨论已有项目时,策略要求它先读相关代码再开口。

"单调"是 DSH 的机制保证——拒绝一旦成立,任何后续监听器都无法把它翻回允许。拒绝理由会作为 Error: 内容回到模型上下文里,所以模型能立刻改道,而不是反复撞墙。

状态从会话日志折叠,而不是另存一份

模式状态是一个 sessionProjections 单元,从两处日志折叠出来:/discuss 命令自己的 command/run / command/done 记录,以及会话一开始就记着的 agentPreset(表头字段与 agent-preset/selected 事件)。这带来三个白拿的好处:会话恢复、fork、重放都能自动还原状态。

这也是必须的做法:DSH 不允许插件 append 自定义类型的事件——不认识该类型的读取方会拒绝重新打开会话。

两个入口在折叠时是有先后的,不是并列的:只要有一次 /discuss 跑完并落进日志,之后这条会话就听那个决定——否则在预设会话里敲 /discuss off 会被预设当场撤销。在那之前,会话的预设说了算,所以预设入口不需要任何命令就生效。

侧边栏面板读的就是这个投影的 wire.view,并通过执行同一条命令来改变它,所以界面不可能和日志脱节——预设入口也是同一个 view,面板显示的"已开启"和你看到的一致。

配置

作为 bundle 安装时,默认值是内置的,无需任何配置。要覆盖,在 profile 的 cordis.patch.yml 里重述你保留的字段(patch 是整体替换 config,不是深合并):

- id: discuss-mode
  name: 'dsh-discuss-mode'
  config:
    blockedTools: ['write', 'edit', 'pwsh', 'bash', 'subagent']
字段 默认 含义
section 内置中文策略文本 讨论模式激活时渲染为 discuss:policy 提示词段落的内容
denial 内置中文拒绝语 工具被拒绝时返回给模型的理由;{tool} 会替换为工具名
blockedTools 见上文清单 被拒绝的工具;精确名,或以 * 结尾表示前缀匹配(如 agent_teams_*)
presetId discuss 哪条 Agent 预设的会话一进去就在讨论模式里;写 null 关掉这个入口,只留 /discuss

未知的配置键会在激活时报错,而不是被静默忽略——cordis.patch.yml 里的笔误会立刻暴露。

策略文本是可以改的,而且应该改。 它才是这个插件的正文;代码只是把它接到正确的时机上。注意面板里显示的是随插件发布的默认文案,若你覆盖了 section,以你的版本为准。

兼容性

本版本(0.2.1)针对 DSH v0.2.1-rc.2 编写:

声明 值
engines.dsh >=0.2.1-rc.2 <0.3.0
五个 @deepseek-ai/dsh-* peer 0.2.1-rc.2
dsh.manifestVersion 1

DSH 的插件兼容性预检(v0.2.1-alpha.3 起)先读 manifest 自己的 engines.dsh,没有才拿运行时版本逐个匹配 @deepseek-ai/dsh-* peer(includePrerelease 语义)。把范围写进 manifest,装错宿主版本时由预检直接拒绝,而不是等运行到一半才炸。

DSH 0.2.0-rc.2 及更早不再支持:旧清单把五个 peer 固定为 0.2.0-rc.2,精确范围不满足运行时 0.2.1-rc.2,预检会判为不兼容并拒绝加载插件的行。

安装

要求 DSH v0.2.1-rc.2(见上文「兼容性」)。本包是一个 bundle(package.json 里的 dsh.bundle.patch 指向两个补丁层:cordis.patch.yml 挂插件行,presets/discuss.patch.yml 声明那条 Agent 预设),安装它就会自动激活,无需手工编辑 profile:

& 'D:\DSHD\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'D:\OneDrive\Desktop\Code\dsh\dsh-discuss-mode'

装完需要完全退出并重启 DeepSeek Harness。两个原因:本机正在运行的进程早于这次安装;而宿主侧与浏览器侧的新模块都需要在启动时重新组装。

升级时注意:profile 默认跑 HMR,往已装过的包上再执行一次 add 会被以 already-installed 拒绝(换版本、换路径也一样)。要换,先移除再装:

& 'D:\DSHD\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop remove dsh-discuss-mode

而这个仓库是以 linked checkout(本地目录)方式挂进 profile 的:只是更新了检出内容时不必重装,完全退出并重启 DSH 即可。

重启后侧边栏出现「讨论模式」图标即安装成功。也可以在 Plugins 页确认并开关。

已知边界

  • PTC 模式的 run_code 没有被拦截。 它只在非 native 呈现模式下可见,而拦截它会连带切断模型调用一切工具的通道。通过它发起的嵌套工具调用仍然会被本插件的 guard 按名字拦下,但如果 run_code 的运行时有裸的文件系统访问,那是一个缺口。native 模式(默认)下不存在这个工具。
  • 子代理在模式激活时被拒绝启动,因为子代理不在本 guard 的管辖范围内——它们的工具调用属于另一个 agent 会话。堵住入口比事后追溯可靠。
  • 讨论模式与 plan mode 可以同时激活,此时两段策略文本都会渲染(顺序为 500 的 plan:policy,然后是 550 的 discuss:policy)。实际使用中你不会想同时开两个。
  • 面板的按钮切换的是"当前显示的会话",判定方式是 retainedBy.mainView > 0——与 DSH 自带的 Workspace、Open-in-app 插件用的是同一个办法。没有打开会话时按钮禁用。
  • 模式下拉框那条预设只对新会话生效。 DSH 的 Agent 预设是会话级的:空白会话可以换,开过一轮之后宿主直接拒绝(agent-preset/locked)。所以"当前会话进讨论模式"永远走 /discuss 或面板,下拉框只负责开一条新的。
  • 预设里那份工具面是快照,不是引用。 它抄的是 @deepseek-ai/dsh-web-app@0.2.1-rc.2 的 presets/standard.patch.yml:这一版的标准模式多了 time-context、tool-schedule、tool-plugin-market(最后一个 disabled),少了 tool-subagent-codex、tool-subagent-claude-code、tool-ralph 三行以及 backgroundMode 配置。升级 DSH 后标准模式再变时,讨论模式预设里那份不会自己跟着长——跑 node tools/sync-preset.mjs 重抄;node test/verify.mjs 会检查生成标记、{{cwd}} 缺席与这些关键行的有无,tools/sync-preset.mjs --check 则直接告诉你它是否已经过期。宿主没有"预设继承另一条预设"的写法,这是唯一的选择。
  • 预设的 id 是可配置的(presetId,默认 discuss):只装插件、不装这条预设的部署完全不受影响——没有哪条会话的 agentPreset 会等于它,模式照旧只由 /discuss 开关。
  • 0.2.1 新增的 working_directory 不在拒绝清单里。 它是"查询当前目录"和"进入另一个目录"二合一的工具:只读查询对讨论有用,换目录也只是把读取范围挪进项目内部,真正会改变世界的仍是 write / edit / pwsh 那些。想要更严格的立场,把它加进 blockedTools 即可。
  • 不强制"必须讨论 N 轮":那不是约束能解决的问题,只会制造噪音。这个模式只保证两件事——模型没法自己出去,也没法动手。
  • 对齐是话轮协议,不是状态机。 "什么时候算聊完"由模型自己判断,而交付是纯文本、没有工具调用可供拦截,所以策略文本仍可能被绕过——模型仍可能把草案和交接 prompt 放进同一轮。若这种情况频繁发生,下一步不是继续加措辞,而是把"要 prompt"做成用户侧事件(面板按钮或命令),让交付由用户触发。

实现速览

没有构建步骤:两个手写的普通 JS(宿主半 + 浏览器半)、两层补丁(插件行 + Agent 预设声明)、图标与两份 locale;Agent 预设那份快照由仓库里的 tools/sync-preset.mjs 重抄(开发期工具,不随包发布)。

关注点 机制
兼容性 package.json 声明 engines.dsh: '>=0.2.1-rc.2 <0.3.0'、五个 @deepseek-ai/dsh-* peer 固定 0.2.1-rc.2、dsh.manifestVersion: 1——宿主兼容性预检据此放行
提示词段落 ctx.systemPrompt.section({ name: 'discuss:policy', order: 550 })
工具拒绝 ctx.tools.guard(exec => string | undefined),按会话状态条件判断
斜杠命令 ctx.commands.register({ name: 'discuss', input: { hint: '[off|message]', attachments: true } })
状态 ctx.sessionProjections.register({ key: 'discuss', apply, wire }),stateVersion: 2
预设入口 package.json 的 dsh.bundle.patch 是有序数组:cordis.patch.yml 之后追一层 presets/discuss.patch.yml,后者插一行 @deepseek-ai/dsh-agent-preset(config.id: discuss)
预设会话的默认状态 投影 init(header) 读表头的 agentPreset,apply 再折叠 agent-preset/selected;一旦有 /discuss 的 settled 决定,决定优先于预设
侧边栏入口 ctx.slots.inject('sidebar.panellist', ...),order: 20
面板页面 ctx.slots.inject('main', ...),keyed,key 同为 discuss-mode
面板 → 宿主 ctx.remote.commands.execute(sessionId, '/discuss' | '/discuss off', [])
宿主 → 面板 sessions.binding(id).session.projections.faceOf('discuss')
生命周期 每个注册都包在 ctx.effect(...) 里——服务侧注册挂在服务自己的 fiber 上,不包的话插件重载会留下陈旧段落并导致重名冲突

客户端不引入任何 DSH 客户端包(它们随版本变化,而手写 JS 没有类型检查),只用模块表里的 react,样式走 --dsw-alias-* 主题变量并带兜底值。

参考实现:@deepseek-ai/dsh-plan-mode(同体系的姊妹模式)、@deepseek-ai/dsh-persona(ctx.effect 包装的写法)、dsh-mobile-remote(侧边栏面板 + sidebar.panellist)。

推送(网络受限时)

本机到 github.com:443——也就是 git push 用的那个端点——不稳定,实测会连接超时或被重置;而 gh 走 api.github.com 完全正常。所以仓库带了一个走 Git Data API 的推送脚本:

powershell -ExecutionPolicy Bypass -File tools\push-via-api.ps1

它不是复制:git 的对象 id 是内容的纯函数,上传同样的 blob / tree / 作者 / 提交者 / 时间 / 提交信息必然得到同一个 SHA。脚本在每一步都比对 git 自己的答案,对不上就拒绝移动 ref——所以本地和远端不会悄悄分叉,git status 保持准确,等网络恢复后 git push 也没有多余的事可做。

前提是远端仓库已有至少一个提交(GitHub 对完全空的仓库会以 409 Git Repository is empty 拒绝创建 blob/tree)。

验证

test/verify.mjs 是随附的隔离测试:给 @deepseek-ai/dsh-llm 打桩后真实导入宿主侧代码,用假 ctx 跑通 apply() 的接线,并检查状态机(两个入口的先后、/discuss off 不会被预设撤销)、配置校验、工具匹配器、命令分支、两层补丁的清单与形状、客户端契约(含两半策略文本必须逐字相同的一致性断言)。0.2.1 适配后它还盯住兼容性声明与预设快照:version 是 0.2.x、engines.dsh 的范围、五个 @deepseek-ai/dsh-* peer 固定 0.2.1-rc.2、dsh.manifestVersion === 1,以及生成标记存在、{{cwd}} 缺席、time-context / tool-schedule / tool-plugin-market 在列、codex / claude-code / ralph 行与 backgroundMode 不在列。设了 DSH_APP_ASAR 时它还会顺带跑一次 sync 的 --check,没设就跳过,不算失败。

node test/verify.mjs

不需要安装任何依赖——桩模块由脚本自己生成在 test/.sandbox/(已 gitignore)。应输出 N passed, 0 failed(N 为检查项数)并以 0 退出。

tools/sync-preset.mjs 回答的是另一个问题:生成区是否还等于安装里的标准预设。

$env:DSH_APP_ASAR='D:\DSHD\resources\app.asar'; node tools/sync-preset.mjs --check

一致时 exit 0;不一致时 exit 1,并说明 presets/discuss.patch.yml 需要重抄(去掉 --check 就是就地重写)。它依赖装好的 DSH(从 app.asar 里读标准预设),所以和离线可跑的 test/verify.mjs 分开。

tools/asar-extract.mjs 是查装好的那份 DSH用的:仓库旁边的参考检出往往落后于安装版本(本插件这一版就是照着安装的 0.2.1-rc.2 适配的),而 presets/standard.patch.yml、dsh-agent-preset-registry 的实现都躺在 app.asar 里。它只读不写:

node tools/asar-extract.mjs 'D:\DSHD\resources\app.asar' list 'dsh-agent-preset'
node tools/asar-extract.mjs 'D:\DSHD\resources\app.asar' read 'dsh/node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml'
node tools/asar-extract.mjs 'D:\DSHD\resources\app.asar' dump <路径> 落地文件