ysr666/dsh-vision-router
ysr666/dsh-vision-router为纯文本 Agent 提供视觉能力:内置免 Key 视觉链 + 像素级视觉工具(看图问答、定位、裁剪、像素对比、取色、OCR、矢量化、抠图、截图);粘贴图片即可用。
基于 MiniMax 的多模态插件:实时语音通话模式(流式对话、悬浮胶囊 UI)、语音模式与麦克风语音输入,并提供图像/视频/音乐/语音生成与视觉检查工具。
English | 简体中文
一个动态 Cordis 插件,通过 mmx CLI(MiniMax) 为 DeepSeek Harness 赋予多模态超能力:
guide_dog_vision / guide_dog_inspect)
描述图片,让没有原生视觉输入的模型(如 DeepSeek)也能审查前端设计、图表、
截图和生成的图片。image-01)、视频(MiniMax-H3 / Hailuo)、
语音(MiniMax TTS)、音乐(music-3.0)、文本(MiniMax-M3)与联网搜索。/guide-dog/media/<file> 提供,并内联渲染在对话工具卡片中
(<img>、<audio controls>、<video controls>);另有 Guide Dog
设置页(认证状态 + 朗读测试器)。guide_dog_speak 复用你现有的
audio-conversation 与
speech-mmx 技能管线
(文本转换、中日韩自动检测、分语言音色、宿主机播放);技能脚本缺失时
回退到原生 mmx speech synthesize。guide-dog-vision,order 110)指示
智能体对任何需要视觉检查的任务自动调用检查工具,尤其是当活动模型无法
看图时。a11y 配置块(自动朗读 /
视觉云端 / 摘要优先);无障碍功能是下一步路线图,将在通话模式稳定后
测试并上线。Guide Dog 已被 dsh-recommend 收录——这是一个社区精选的 DSH 插件目录。本项目通过了其认证评审,并在本 README 顶部挂着金色 精选认证(certified) 徽章;分数徽章随每次 registry 数据同步自动更新。
| 文件 | 用途 |
|---|---|
plugin-host.js | Host 半区 — 唯一事实来源(工具、RPC、媒体路由、提示词段、语音模式) |
plugin-client.js | Client 半区 — 唯一事实来源(工具卡片、设置页、语音集群) |
bundle/ | 由两半区生成的静态 web-profile 包(deploy/convert_bundle.py) |
deploy/ | convert_bundle.py(源码 → 包)与 publish.py(包 → ~/.dsh/dsh-guide-dog + web profile 注册) |
package.json | 根 manifest,声明 dsh.bundle(使仓库可通过 dsh plugin add 安装) |
README.md | 本文件(英文版) |
README.zh-CN.md | 本 README 的简体中文版 |
用标准 DSH 插件命令直接从本仓库安装——根 package.json 已声明
dsh.bundle manifest,profile 的 pnpm 层会自行解析该包(无需发布 npm):
dsh plugin --profile web add github:AtropinolTT/dsh-guide-dog
plugin-host.js / plugin-client.js。python3 deploy/convert_bundle.py — 重新生成 bundle/lib/。python3 deploy/publish.py — 复制到 ~/.dsh/dsh-guide-dog,幂等地注册
到 ~/.dsh/profiles/web(依赖链接 + bundles 条目 + node_modules 符号
链接),并移除被取代的 autoload 包。dsh web)— 包在启动时解析。没有动态插件、没有审批卡片、没有每会话实例:DSH 重启后,工具与语音 UI 随 profile 一起恢复。完整细节与坑见下文「重启恢复」一节。
plugin-source.js 是动态时代的遗留产物(两半区拼接);仅作参考保留,
当前部署流程不使用。
| 工具 | 参数 | 返回 |
|---|---|---|
guide_dog_speak | text*、voice(auto)、speed、language、playOnHost | {ok, url, voice, bytes} mp3 |
guide_dog_image | prompt*、aspectRatio、n、width、height、seed、promptOptimizer、watermark | {ok, urls[], files[]} |
guide_dog_video | prompt*、model(默认 MiniMax-H3)、image、subjectImage、duration、ratio | {ok, url, taskId} mp4(轮询直到完成) |
guide_dog_vision | image*、prompt | {ok, answer} VLM 描述 |
guide_dog_inspect | image*、focus(general/frontend/figure/screenshot/ocr)、prompt | {ok, answer, focus} 结构化评审 |
guide_dog_voices | language | {ok, voices[]} |
guide_dog_music | prompt*、lyrics、instrumental、vocals、genre、mood、model | {ok, url} mp3 |
guide_dog_text | message*、system、model、maxTokens、temperature | {ok, text} |
guide_dog_search | q* | {ok, results[]}(最多 10 条) |
* 必填
插件运行时,系统提示词段指示智能体:
guide_dog_inspect
(结构化)或 guide_dog_vision(通用)——绝不能声称看过没有检查过的图。/guide-dog/media/<file> 提供给用户;智能体必须附带返回的
url 字段,方便用户预览。guide_dog_speak。在 DeepSeek 上的视觉检查示例流程:
1. (agent) 生成图表/截图文件,如 chart.png
2. (agent) guide_dog_inspect { image: "chart.png", focus: "figure" }
→ 对坐标轴/标签/可读性/编码做结构化评审
3. (agent) 迭代图表、重新检查,最后附上 url 定稿
4. (user) 在 Web UI 卡片中预览 chart.png
~/.dsh/guide-dog/.guide-dog/media — 即
GLOBAL_ROOT = ~/.dsh/guide-dog 下的全局存储(自 2026-08-16 起整个
web profile 只有一个实例;不再是每工作区沙箱根——见下文「重启恢复」)。/guide-dog/media 提供,具备:
jpg/jpeg/png/gif/webp/mp3/wav/m4a/ogg/mp4/webm),Accept-Ranges: bytes 与真实的字节范围响应(视频拖动进度),.index.json 保存元数据(prompt、voice、ts、kind),供设置页画廊
(guide-dog/list-media RPC)使用;索引损坏时从目录重建。guide_dog_speak 严格遵循你的两个技能的管线:
~/.agents/skills/audio-conversation/scripts/transform.py — 剥离
markdown/代码/URL(缺失时回退到内置 JS 转换)。English_Trustworthy_Man / Chinese (Mandarin)_Gentle_Youth),与技能
env 契约一致。显式 voice 覆盖;language 增强口音。~/.agents/skills/speech-mmx/scripts/mmx_tts.py speak --input … --out …
(回退到 mmx speech synthesize)。playOnHost: true 时宿主机扬声器也会
播放——一次一个文件(先终止上一个播放),与技能的 latest-only 规则一致。在 dsh 进程环境中仍生效的技能环境变量:AUDIO_CONVERSATION_VOICE(_EN/_ZH)、
AUDIO_CONVERSATION_SPEED、AUDIO_CONVERSATION_DIR、AUDIO_CONVERSATION_NO_PLAY、
AUDIO_CONVERSATION_KEEP_FILES、TTS_GEN。回合文件沿用 turn-NNN.mp3
命名规范。
设置 → Guide Dog(id guide-dog):
mmx auth status 结果,密钥打码(sk-c…xxxx);
绝不完整打印。guide-dog/voices),在浏览器中播放 mp3。session/event 监听器监视
assistant/message 事件,提取回复文本(event.data.content 中
type === 'text' 的块),检查该会话语音模式是否生效(会话覆盖优先,
否则全局默认),并把 TTS 结果({url, key})或错误入队到该会话的
voiceQueue。client 每秒轮询队列并用模块级 Audio 对象播放,或显示
右下角 toast + 提示音 6 秒。conversation.input.left 条目 guide-dog-voice
(order 30),位于输入框左下角,使用 DSH 令牌主题(--dsw-alias-*),
继承应用字体:
guide-dog/set-config 配合 voiceMode.sessions);悬停提示
"语音模式:开/关 · 全局默认:开/关"。Audio 对象上,切换会话不会重播或
打断:当前片段播完为止,除非新的播放任务(任意会话的新队列条目)覆盖。guide-dog/transcribe
转写。识别文本通过 inputActions.setDraft(text) 插入输入框(配置后可
用 inputActions.submit() 自动发送)。错误状态:mic_denied、
no_device、empty_speech、stt_failed、stt_timeout、
engine_unavailable、insert_failed(绝不静默)。🎙 打开录音页 链接,
指向独立页面 /guide-dog/recorder(GET 提供自包含 HTML 录音器;POST
/guide-dog/transcribe-upload 接受原始 audio/webm,上限 20 MB,走同一
transcribeImpl)。guide-dog/get-config /
guide-dog/set-config / guide-dog/status 支撑。位于 ~/.dsh/guide-dog/.guide-dog/config.json(由默认值自动创建;所有键
可选,在默认值之上深合并):
{
"voiceMode": { "default": false, "sessions": { "<sessionId>": true } },
"voiceInput": {
"autoSend": false,
"engine": "whisper",
"language": "auto",
"maxSeconds": 60,
"whisper": { "python": "python3", "model": "small" }
},
"tts": {
"voiceEn": "English_expressive_narrator",
"voiceZh": "Chinese (Mandarin)_Gentle_Youth",
"speed": 0.95,
"format": "mp3"
}
}
voiceMode.sessions 把会话 id 映射为布尔覆盖;default 是回退值。
输入框左下角的喇叭按钮切换当前会话的覆盖。voiceInput.engine:whisper(唯一已实现的引擎;sherpa/minimax
为预留——选中会返回 engine_unavailable)。voiceInput.maxSeconds 强制麦克风录音停止。whisper 引擎调用随附的 Python 脚本
(.guide-dog/scripts/whisper_transcribe.py),基于 faster-whisper:
pip install faster-whisper # 需要 Python 3.8+;会安装 torch cpu wheels
python3 -c "import faster_whisper; print(faster_whisper.__version__)"
host 在启动时探测可用性,并把结果写入 .guide-dog/status.json
(whisperAvailable、whisperVersion、whisperPython),显示在
设置 → STT 行。模型选择:base(快)/ small(准);首次运行会下载
模型权重。
node --check plugin-host.js && node --check plugin-client.js # 语法检查
curl -s http://127.0.0.1:3080/guide-dog/recorder | head -5 # 录音页返回 HTML
curl -s -X POST http://127.0.0.1:3080/guide-dog/api/guide-dog/status \
-H 'content-type: application/json' -d '{}' | head -5 # status RPC(兼容层)
cat ~/.dsh/guide-dog/.guide-dog/status.json # whisper 探测结果
手动检查(部署后):点喇叭按钮(语音模式开,变绿)→ 发消息 → 助手回复 自动朗读;播放中切换会话 → 片段播完且不会重播;用麦克风按钮 → 识别文本 出现在输入框;设置 → Guide Dog 显示 语音模式 / 语音输入 / STT 三个区块。
/guide-dog/call-transcribe
(webm/opus,≤20MB,复用 Phase 1 transcribeImpl 与本地 whisper 管线)
→ {ok, text, language, durationMs};下行 GET /guide-dog/tts-stream
走 chunked HTTP 流(host 按句 spawn mmx speech synthesize --stream --format pcm --sample-rate 24000,stdout 管道增量喂 res.write;client
用 fetch().body.getReader() 读流 → PCM→WAV → Web Audio 无缝调度)。
传输层无 WebSocket 新协议面,浏览器与 CLI 复用同一管线。call.mode='vad'):
MediaRecorder(audio/webm;codecs=opus,250ms timeslice 整通录音)+
并行 AnalyserNode 能量检测(RMS ≥ vad.threshold、静音 vad.silenceMs
判定说话结束、vad.minSpeechMs 最短语音、vad.maxSegmentSeconds 单段
上限)——说话-停顿-说话自动成两段回合;面板可切换 ptt 按住说话
(按住麦克风说话、松开即发送;VAD 参数不参与端点判定,仅做打断监测)。guide_dog_call_consensus systemPrompt
variable,聊天式措辞:先理解意图、不清楚就问、写入/修改前说明并等用户
点头)+ 机制硬保证(tools/pre-execute 瀑布拦截:write/edit 与 bash
破坏性命令启发式 rm/mv/cp/truncate/dd/覆盖重定向/git push 等 → 未确认
返回 {kind:'deny', reason:'needs_voice_confirmation'},模型语音提问;
用户确认词命中 → 本轮放行;每次执行前 host 用工具入参生成一句话摘要
直接 TTS 播报(不走模型),随后开启 consensus.summaryWindowMs 打断
窗口,窗口内用户发声即中止本次执行——工具物理上尚未启动)。拦截器自身
失败 → 拒绝并口播"共识检查失败"(宁可拦错不可放错,spec §6.8)。agent/status
(running → "正在处理")、tools/result(工具名→短语:write/edit→
"正在修改文件"、web_search→"正在搜索网页"、
guide_dog_image/video/music/speak→"正在生成媒体"、
bash 仅破坏性命令(与共识拦截同口径 DESTRUCTIVE_BASH_RE)→"正在执行命令";
read/grep/glob/skill/非破坏性 bash/未知工具静默)、agent/error
("处理出错:<短原因>");同短语 4s 冷却去重(多步同类操作只报一次);
通话中 >120s 无任何事件 → 心跳播报"仍在处理,请稍候"。播报与回复朗读
共用队列:播报优先(队首)、回复让路;播报走流式通道
(与回复同一 WebAudio PCM 链)→ 单播放器构造性串行,一条接一条,不可能重叠。stream.sentenceSplit 字符集
。!?.!?\n;stream.maxSentenceChars 超长句强制截断)逐句合成;每句
经 guide-dog/tts-token 重新签发一次性 token(单次消费、5 分钟有效、
绑定 sessionId);句间预合成(当前句播放期间 client 提前请求下一句流,
解码帧按播放时间链无缝追加调度——前一句未播完即续接下一句,长回复完整
朗读、播放间隙 ≤400ms)。实测中文短句首字节 ~600ms,满足"首音频 <1.5s"
判据。只播回合最终消息(RC13):中间步骤的 assistant 消息(带工具
调用块)不入队——逐 step 播放近同文案是"同一内容反复播报"的根因;中间
步骤由进度播报覆盖。终结型工具回合(最后一条消息仍带工具调用)由
turn/end 兜底播缓冲文本,不静音。vad.interruptMinMs
300ms 防误触)→ 浏览器立即停止播放并清空未播缓冲 → 停播为 10ms 淡出
(RC13)——src.stop() 硬切会在句切断处产生咔哒爆音。打断后的首个转写
段经 interrupt RPC 直达当前回合(agent.steer,RC11),不排队成新回合
→ abort 当前 tts-stream fetch → 新语音自然成为下一回合(Pipecat
InterruptionFrame 语义)。guide-dog/call-command RPC 提供 clear-queue 等 host 侧命令。guide_dog_speak(playOnHost=true) 已在本机
扬声器播过的文本,不再经语音模式/通话队列通道重播(消费即删,同文本
只挡一次)——消除"本机 + 浏览器双响"。sanitizeSpeechText,F1) — 入队前对回复文本做一次
markdown/URL/emoji 剥离:[标题](url) 保留标题去 URL;裸 URL
(https?://、www.)整段移除;行首列表/引用标记(-/+/*/>)
与行首有序列表标记(1. 1、 1))剥除;**加粗**/反引号等
markdown 标记剥除;emoji 区段(U+1F000-U+1FAFF 等)剥除。通话场景
只朗读人话,不读网址/**/-/📢 等元字符,避免 "thepaper/newsD/
weather.com" 这类 URL 碎片被打散朗读。splitSentences,F2) — 默认分隔符 '。!?.!?\n' 中的
. 改为智能规则:仅当 . 后紧跟空白+大写字母/数字/CJK 才拆
('Hello. Next' 拆成 2 句;'8.17 的上海' 保持 1 句;URL 内部的 .
不会被打断)。中文分隔集合追加 ;;,避免 …; 把一句中文回复拆成两半。VOICE_QUEUE_MAX,F3) — 由 10 提到 40,
丢队尾保内容:超限时 while (q.length > VOICE_QUEUE_MAX) q.pop()
(先入内容优先;旧 splice(0, …) 从队头删的策略会把主内容先裁掉,
保留 URL 碎片)。announce/hb 的 progress 仍用 pop()(unshift 到队首,
progress 优先)。announce 的 progressDedupe 冷却
由 4s 延长到 30s:web_search 多次结果间隔 ~4.3s 时不会连播 3 次
「正在搜索网页」。progressDedupe 函数本体未动;repro-progress.js
语义保持。wasHostSpoken /
markHostSpoken 统一使用 sanitizeSpeechText 处理后的文本作为键:
downlink、turn-end flush、voice-mode 三处 wasHostSpoken 都按净化键
匹配;speakImpl 在 playOnHost 成功后同时注册原始键与净化键(双键),
下游任一通道都能正确去重。已知边界:transform.py 改写文本时两个键可能
不完全一致(属可接受边缘)。[gd-host],DSH 终端可见):
enqueue from=downlink|turnend|voice-mode|consensus|announce|heartbeat n=... qlen=...、
shift key=... remain=...、skip host-spoken sid=... text=...、
QUEUE-DUP text=...。[gd],浏览器 DevTools):
playStreamEntry ... times=...(每次播放按 entry.key || entry.text
累加计数)、PLAY-SUMMARY key=N | ...(队列空时汇总当前所有计数后
清空)。QUEUE-DUP → host 双入队;
PLAY-SUMMARY key=2 → client 双播;两者皆无但仍两遍 → tts-stream
音频双写;enqueue from= 同源两次同文本 → 事件重放。playVoiceEntry,F1) — 语音模式播放从「每次
new Audio() + 临时 URL」改为 fetch + Blob + 单元素复用:整段音频
一次 fetch 到 Blob,经 URL.createObjectURL 绑定到唯一 <audio>
元素,后续条目只替换 src 与播放回调,不再反复创建/销毁 Audio 对象
——消灭 ERR_CONTENT_LENGTH_MISMATCH 引起的重试风暴(每次重建 Audio
都会重放 mismatch 前的部分内容,长音频时表现为「反复重播 + 卡顿」)。play() rejected):进入「待播放挂起」状态,绑定首次用户手势
(click/keydown/touchstart,capture 阶段、常驻监听)后自动继续;
被拦条目不再丢弃,手势后重放。stopCurrent 中断时正确释放 busy 并
回队(防 busy 死锁吞条目)。voice-requeue,F3) — 播放失败(解码/网络/被拦)时
client 调 host voice-requeue RPC 把该条目重新入队(requeueEntry 纯
函数:新文本插队、重复文本跳过、队尾 pop 截断),每条目最多重试 3 次
(attempts map)——不再丢内容。replayDup,F4) — host 对「通话下行 +
语音模式」两通道的 enqueue 增加 10s 窗口的 last-text 去重
(lastStreamText/lastVoiceText 双 map):同文本 10s 内再次入队直接
跳过([gd-host] skip replay text= / [gd-host] skip voice-dup text=
埋点)。根因:语音模式 + 通话下行的事件重放会让同一文本入队两次——
男声回复「7 遍」即该窗口缺失所致。PLAY-SUMMARY 汇总日志覆盖语音模式条目
(playCounts 按 entry.key || entry.url 计数,队列空时汇总清空)——
url 条目的播放次数可追踪,复测定位不再靠猜。rc15-20260817
(plugin-client.js 源与 bundle/lib/client.js 同步;硬刷新后 DevTools
控制台可见该行;已被后续版本取代——当前标记为 rc20-20260817)。Phase 1 的 config(~/.dsh/guide-dog/.guide-dog/config.json)基础上新增,
全部可选、深合并默认值(spec §4 复制):
{
"call": {
"mode": "vad",
"vad": {
"method": "energy",
"threshold": 0.02,
"silenceMs": 700,
"minSpeechMs": 300,
"maxSegmentSeconds": 60,
"interruptMinMs": 300
},
"stream": {
"format": "pcm",
"sampleRate": 24000,
"sentenceSplit": "。!?.!?\n",
"maxSentenceChars": 200
},
"voice": "English_expressive_narrator",
"speed": 1.0,
"progress": true,
"consensus": { "enabled": true, "summaryWindowMs": 3000 }
},
"a11y": {
"enabled": false,
"autoNarrate": true,
"visionCloud": true,
"summaryFirst": true
}
}
call.mode:vad(默认,自动端点检测)| ptt(按住说话)。call.vad.method:energy(Phase 2 v1,RMS 能量阈值;背景噪声大时调高
threshold)→ 升级位 silero(web-vad 浏览器 WASM)/ sherpa
(VAD+ASR 一体)。call.stream.format/sampleRate:mmx speech synthesize --stream 参数
(s16le 单声道 PCM;24000 为显式覆盖值,mmx 自身默认 32000)。call.consensus:enabled 开关共识优先(仅通话/a11y 生效);
summaryWindowMs 摘要播报后等待用户打断的窗口。a11y:Phase 3 无障碍模式配置(本阶段仅 enabled 参与通话流式/共识
判定;autoNarrate/visionCloud/summaryFirst 为 Phase 3 预留)。| 方法与路径 | 用途 |
|---|---|
POST /guide-dog/call-transcribe | 上行:整段音频(client 以 raw audio/webm body 发送,x-session-id 头;host 对整段请求体 base64 后交 whisper),≤20MB 硬上限;host 复用 Phase 1 transcribeImpl → {ok, text, language, durationMs} |
GET /guide-dog/tts-stream?token=…&sid=…&text=<句> | 下行:chunked PCM 音频流(content-type: audio/pcm,cache-control: no-store);需 guide-dog/tts-token 签发的一次性 token——无/错 token → 403,该会话有在途流 → 429 |
RPC 风格接口(tts-token / call-active / call-command)走同一 JSON
POST 兼容层,物理 URL 为 /guide-dog/api/guide-dog/<name>(双重前缀,
同 Phase 1 guide-dog/status 示例)——见下方 RPC surface 表新增三行。
node --check bundle/lib/index.js && node --check bundle/lib/client.js # bundle 语法 ×2
curl -s -X POST http://127.0.0.1:3080/guide-dog/call-transcribe \
-H 'content-type: application/json' -d '{}' | head -5 # 上行路由可达(空音频 → 错误 JSON)
curl -s -o /dev/null -w '%{http_code}\n' \
'http://127.0.0.1:3080/guide-dog/tts-stream?token=bad&sid=x&text=hi' # 无有效 token → 403
手动验收清单(完整判据见 specs/2026-08-14-guide-dog-v2-design.md
§6.9,部署并重启 DSH 后逐项验证):
threshold 可调)。tools/pre-execute 拦截路径)。| 方法 | 参数 | 返回 |
|---|---|---|
guide-dog/speak | {text, voice?, speed?, language?, playOnHost?} | {ok, url, file, voice, bytes} |
guide-dog/list-media | {limit?} | [{name, kind, prompt, voice, ts, bytes, url}] |
guide-dog/auth-status | — | {ok, method, source, keyMasked} |
guide-dog/voices | {language?} | {ok, voices[]} |
guide-dog/get-config | — | {ok, config}(合并默认值) |
guide-dog/set-config | {patch} | {ok} / {ok:false, error} |
guide-dog/status | — | {ok, status}(whisper 探测 + probeAt) |
guide-dog/transcribe | {audioB64, mime, sessionId?, language?} | {ok, text, language, durationMs} / {ok:false, error} |
guide-dog/beep | — | {ok, dataUri}(WAV 提示音 data URI) |
guide-dog/voice-queue | {sessionId} | {ok, entry} — 弹出一个条目(播放/错误)或 null |
guide-dog/tts-token | {sessionId} | {ok, token} — 一次性流 token(5 分钟、单次使用、绑定会话) |
guide-dog/call-active | {sessionId, kind ('session'|'speaking'), active} | {ok} — 会话持久 vs 瞬时说话标记(C4) |
guide-dog/call-command | {sessionId, cmd} | {ok} — host 侧通话命令(clear-queue …) |
~/.mmx/config.json);插件从不
读取或转发它。subprocess 服务(播放器必须比沙箱的
--die-with-parent bwrap profile 活得更久);每次新播放会终止上一个。mmx 未找到 / 认证缺失 — 工具返回 {ok:false, error};设置页显示
认证问题。修复:npm install -g mmx-cli 并 mmx auth login --api-key sk-…(或 export MINIMAX_API_KEY=…)。denied: true;把媒体留在工作区内
(插件已经这么做)。MiniMax-H3 返回 "TokenPlan 或 Credit 暂不支持 MiniMax-H3 系列模型"
— 账号的 MiniMax 套餐不含 H3 模型族。改用 model: "MiniMax-Hailuo-2.3"
(旧版 V1)或升级套餐。插件原样透传 API 错误,因此在工具结果中可见。duration 或不同的 model 重跑。/plugins/dsh-guide-dog/client.js 是否返回 200,并刷新页面。--quiet 会改变各命令的 JSON 形态:speech voices 打印扁平字符串数组,
text chat 只打印回复内容(因此插件不带 --quiet 跑文本对话),而
auth status / search query 保持对象形态。video generate --async 总是打印 {taskId}(原始 stdout 写入)。content.url;插件用 curl 下载。旧版 V1 任务返回
file_id,经 mmx video download --file-id 下载。image generate --out-dir、music generate --out、
speech synthesize --out、video download --out)可能打印不出可解析的
内容;插件把退出码 0 视为成功,并用 fs.stat 验证文件。自 2026-08-16 起,Guide Dog 以静态包形态挂在 web profile 中——一个
全局 host 半区 + 一个 client 半区,与已发布的 dsh-better-sidebar 完全
一致。没有动态插件、没有每会话 gdog-* 实例、没有审批卡片:DSH 重启后
工具与语音 UI 随 profile 一起恢复。
唯一事实来源仍是两个动态插件半区(plugin-host.js / plugin-client.js);
deploy/convert_bundle.py 从中重新生成 bundle/lib/:
bundle/lib/index.js,ESM name/apply):一个小型兼容
层取代动态沙箱的 harness —— 工具定义通过全局 tools 注册表注册
(每个会话可见),原来的 harness.handle RPC(guide-dog/*)变成
/guide-dog/api/ 下的 JSON POST 路由。每工作区沙箱根被全局存储
~/.dsh/guide-dog/(config、media、scripts)取代。bundle/lib/client.js):像已发布的包一样的
window.__ModuleLoader__.load({id, factory}) CJS factory;
require('react') 来自平台 seed,用自管理的 <style> 标签代替沙箱
styles,host.call 变成对 JSON 路由的同源 fetch。任何插件变更后部署一次:
python3 deploy/convert_bundle.py — 重新生成 bundle/lib/。python3 deploy/publish.py — 把包复制到 ~/.dsh/dsh-guide-dog
(工作区之外),幂等地注册到 web profile(~/.dsh/profiles/web:
依赖链接 + bundles 条目 + node_modules 符号链接),并移除被取代的
dsh-guide-dog-autoload 包(否则它会继续按会话部署动态实例)。dsh web)— 包在启动时解析,因此任何变更后都需要重启。历史沿革:早期的自动部署器(autoload/)——一个监视 agent/created 并
按会话 define+run 全新 gdog-* 动态插件的 host 包——保留在仓库中,
仍发布到 ~/.dsh/guide-dog-deploy / ~/.dsh/guide-dog-autoload 以备回滚,
但从 profile 移除后没有任何东西消费它。
Profile 坑(2026-08-15 观察):dsh web 是 --profile web 的别名——GUI
运行的是 web profile。只在其他 profile(如 cc-tui)注册包对 GUI
静默无效;deploy/publish.py 始终目标 ~/.dsh/profiles/web。
服务作用域坑(2026-08-16 观察——根因 #3):dynamicCordisRunner 与
agents 服务注册在智能体作用域上下文上,而不是包的 apply(ctx)
运行的全局/profile 上下文上。那里的 ctx.get('dynamicCordisRunner') 返回
undefined,因此 apply 里过早的 if (!runner || !agents) return 在
agent/created 监听器注册之前就退出了——包加载正常(经 dsh web --dump-default-config 验证)却从未部署。修复方案:通过事件载荷的
agent.ctx 解析两个服务(Agent 暴露 readonly ctx: Context;已用探针
验证两个服务在那里可见),并为在全局注册它们的 host 提供全局 ctx
回退。调试辅助:临时动态探针插件(inject: ['dynamicCordisRunner', 'agents'])在其(智能体作用域)apply ctx 中能看到两个服务——这种
不对称性正是该坑的特征。
包形态沿用已发布的 dsh-better-sidebar 先例:dsh.bundle.patch →
cordis.patch.yml 单 insert 行、具名导出(export const name +
export function apply(ctx))、无默认导出、仅 host(无需 dsh.client 块)。
onstop 闭包持有过期的
inputActions;转写前需重新校验录音器归属。setVoiceOverride 用可能过期的 config 重建整个
voiceMode.sessions 映射,并发会话切换可能互相覆盖;改为按键合并
(host 侧 patch)或写入前刷新 cfg。