dsh-second-opinion
Expose a second-opinion tool: when the main agent is stuck it hands the problem to an independent session, receives a plan back, and continues — without waking or impersonating the user.
Install
dsh plugin --profile web add dsh-second-opinion
Sandbox test
This plugin has not been tested yet. Submit a test →
README
dsh-second-opinion
给 DeepSeek Harness 的「第二意见」插件——一个会话卡住时,把问题交给另一个独立会话单独思考,把方案拿回来。
求助方在自己的工具调用里同步等顾问答完,答案作为工具结果直接返回:不需要唤醒闲置会话,也不冒充用户发言。
反方向也通:用户可以跑到顾问窗里让它去看看主会话在干什么、留一张便条,全程不打断主会话。
[English](README.en.md) | 中文
dsh-proxy-on-demand — 按需代理,用完自动关
dsh-game-guard — 游戏时收起打扰面
dsh-hot-reloader — 改完插件源码不用重启
dsh-session-clock — 按会话授时与空闲间隔
dsh-second-opinion — 卡住时找另一个会话要第二意见(本仓库)
dsh-onebot-companion — QQ 通道一体化:出站沉默守卫 / 行为规则 / 对话节奏 / 人设 / 语音
一句话说明
模型陷入思维僵局时有个结构性缺陷:打转当下每一步看上去都像在推进。
站在里面的那个会话识别不出自己在原地绕圈,它只会继续试下一个变体。
打破僵局需要的是一个外部视角——不了解你的思路、不会被你的假设带着走,
从头读一遍问题然后说「你第三条假设错了」。
本插件提供这个视角,成本是一次工具调用:
A 卡住 → 调 ask_advisor → 插件为 A 建一个专属顾问会话(名为「辅助+前三字」)→ B 独立分析 → 答案回到 A
每个求助会话有自己的顾问(没求助过的会话不会被建),同一个问题想让第二个独立会话再想一遍时,传 advisor: 2 再要一个。
为什么需要它(宿主已有的两条路都不合用)
DSH 已经有跨会话通信能力,但两条现成通道都做不成这件事:
通道 · 能叫醒闲置会话 · 对方看到什么 · 为什么不用
sessionController.prompt · ✅ 自动 resume · 「用户本人发言」 · 它把消息来源硬编码成 kind: "user"(@deepseek-ai/dsh-api-session-controller 内 const source = { kind: "user", rpcId: request.requestId, ... })——冒充用户,不可接受
send_message · ✅ 起新 turn · agent-message · relay · 授权档位把它限制在父子链上(dsh-subagent 里比对 session.header.parentSession),够不着两个平级会话
缺口 = 跨任意会话 + 能唤醒 + 不冒充用户——这三项组合宿主没有提供。
本插件的办法是绕开「跨会话投递」这件事本身:求助方不把问题发给顾问、
等它回头;而是在自己的工具调用里同步等。工具结果天然属于发出调用的那个会话,
根本不需要任何投递,于是上面三条限制一条都不撞,也没有任何人被冒充。
代价是求助方在等待期间阻塞——而这正是想要的语义:卡住的那个会话本来就该
停下来等一个外部判断,而不是一边打转一边等。
工作原理
┌──────────────┐ ask_advisor(...) ┌─────────────────┐
│ 会话 A │ ──────────────────────▶ │ dsh-second- │
│ (卡住了) │ │ opinion │
│ │ ◀────────────────────── │ │
└──────────────┘ 工具结果 = B 的答案 └────────┬────────┘
│ ① resolveAgent / create
│ ② followup(prompt)
│ ③ 等 B 的 status 变回 idle
│ ④ snapshotEvents(边界) → 取最后非空 assistant 消息
▼
┌─────────────────┐
│ 顾问会话 B │
│ (自动创建,每 │
│ 个求助方一个) │
└─────────────────┘
四个关键实现决定:
1. 答案边界靠 session.seq 快照。 投递之前记下顾问的日志长度,
投递后只读那段新增事件。这样即使顾问会话被复用了很多次,也只会取到
本次咨询的答案,不会串到上一次。
2. 取答案用宿主自己的规则。 lib/compose.js 的 extractAnswer 与
@deepseek-ai/dsh-subagent 的 finalAssistantOutput 保持一致:取
最后一条非空 assistant 消息,空 content 的消息(记账用)不能覆盖已有正文。
这样顾问「先给正文、再补一次只有工具调用的空消息」时,正文不会被吞掉。
3. 同一个顾问槽位串行排队。 每个求助会话有自己的顾问,但同一个槽位可能被
并发咨询(同一会话的两处工具调用、或它派生的子代理),此时后者的答案边界会和
前者重叠、读到别人的回答。per-advisor 的 Promise 链把并发请求排成队列。
4. 等待以 agent.status 为准,不靠 whenIdle() 单打独斗。 宿主
wakeDriver 在 agent 已经不是 idle 时会直接 return、不新建 activityDone
(dsh-agent-loop 的 wakeDriver() 开头 if (this.phase.kind !== "idle") { ... return; })。
那种情况下 whenIdle() 解析的是上一轮的 promise,会在本轮答案产生之前
就返回,让我们读到空结果。所以这里轮询 status,whenIdle() 只当加速信号。
工具
ask_advisor
把一个卡住的问题交给独立的顾问会话。
参数 · 必需 · 说明
question · ✅ · 要顾问回答的具体问题。写成一个明确的问句,而不是「帮帮我」
situation · ⭕ · 你卡在哪:正在做什么、期望什么、实际发生什么
tried · ⭕ · 已试过哪些做法、各自为什么不行。这段最有价值——它让顾问不必重复你的死路
context · ⭕ · 相关上下文原文:文件路径、代码片段、报错信息
advisor · ⭕ · 用第几个顾问,默认 1。想让同一个问题由另一个完全独立的会话再想一遍时传 2、3……每个槽位是独立顾问会话,互不知道对方的存在
返回:
{
"ok": true,
"advisor": { "sessionId": "session-...", "title": "辅助正则匹", "slot": 1, "cwd": "D:\\...\\你的工作目录" },
"elapsedMs": 48213,
"timedOut": false,
"answer": "……顾问的完整回答……"
}
失败或超时返回 { ok: false, ... },不抛异常——工具内部整段 try/catch,
出错只返回错误对象,绝不会阻断对话。
advisor_status
查本会话的顾问状态,或重置它。
参数 · 必需 · 说明
reset · ⭕ · 设为 true 则丢弃本会话的顾问记录,下次咨询会新建一个(不删除旧会话本身)
all · ⭕ · 设为 true 则额外列出所有求助会话各自的顾问
返回 callerSessionId / callerTitle / advisors(数组,每项含 slot /
sessionId / title / cwd / live / consults / lastConsultAt /
createdAt)/ droppedAdvisorSessionIds / legacyAdvisorSessionId /
maxAdvisors / stateFile / notes。
advisor_watch(顾问专用)
上面两个工具是「求助方 → 顾问」方向;这个和下面的 advisor_note 是反方向:
用户自己跑到顾问窗里说「去看看主会话在干什么、搭把手」。
查看你正在辅助的那个主会话现在在干什么:在不在跑、进度停在哪一步、距上次
求助过了多久、有没有换话题、最近说过什么、最后调了哪些工具。
参数 · 必需 · 说明
detail · ⭕ · summary(默认)给完整实况;tools 只给最近的工具调用清单
全程只读。 它走 sessionController.inspect——宿主的注释明确写着
without activating its Agent:会话已附加时读内存快照,冷会话读持久化,两种情况
都不会 resume、不会唤醒对方、不会往对方的日志里写任何东西。运行状态另从
sessionController.list() 取(同样不 resume)。这正是「不打断主会话思考」的技术保障。
返回 `{ ok, principal: { sessionId, slot, title, running, cwd, lastActivityAt,
idleMs, sinceLastConsultMs, topicMoved }, report, note }。report` 是给人读的
散文实况;running 拿不到时是 null,报告里如实写「运行状态未知」——**绝不默认
成在跑或空闲,那是编造**。
advisor_note(顾问专用)
给你辅助的那个主会话留一条便条。主会话下次调用 ask_advisor 或
advisor_status 时会连同工具结果一起看到它,从而知道你在旁边看着、以及你发现了什么。
参数 · 必需 · 说明
text · ✅ · 便条正文:你观察到了什么、建议它做什么、依据是什么
便条只是留言:它不弹窗、不改变主会话正在进行的思考,要等主会话自己下一次
调用才会读到。所以写的时候把结论和依据一起写清楚——主会话看到时可能已经又往前
走了一段,你得让它能自己判断这条是否还适用。
便条按求助会话归档,队列上限 maxNotes(默认 5 条,超限丢最旧——它们也最
可能已经过时),并且是消费式的:主会话读过就清空,不会每调一次工具就重新看到。
谁能调这两个工具? 只有本插件创建出来的顾问会话能——靠反
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::2,最后更新 2026-11-01