安装
npx -y @deepseek-ai/dsh plugin --profile web add github:SilasSolivagus/dsh-plugin-multimodal此安装命令根据 GitHub 仓库地址生成,是未经验证的安装起点。
README
维护者编写的文档快照。
dsh-plugin-multimodal
给 DeepSeek Harness(dsh / Cordis)直接加上多模态能力(图 / 声 / 视频)的插件。
真实 Web 页面演示(dsh web)
以下是插件在 DeepSeek Harness Web UI 中的真实运行截图(HTML 渲染,中文正常显示,不存在终端录屏的乱码问题;截图时左侧栏已收起以保持画面干净):

图像:用户发送
这张图片里是什么?/tmp/dsh-demo/flag.png→ V4 先尝试直接读图失败(model does not declare image input)→ 自动降级调用插件read_media→ moondream 描述 → V4 回答"上半红色、下半蓝色,两条等宽水平条纹,红上蓝下"。

语音:用户发送
这段语音里说了什么?/tmp/dsh-demo/speech.wav→ 插件调whisper转写 → 注入"你好,我是校园网智能助手,请问能帮你做什么?" → V4 润色标点并逐句解释。

视频:用户发送
这个视频里有什么?/tmp/dsh-demo/bars.mp4→ 插件ffmpeg抽 4 帧 → moondream 逐帧描述 → V4 给出元数据(320×240 / 25fps / 100 帧)与逐帧表(前一半纯蓝、后一半红块,硬切换),并诚实说明"采样无法精确到每一帧"。
这些截图来自真实 dsh web 会话(模型 DeepSeek-V4-Pro,推理等级 High)。可见 V4 自身不收图 / 声 / 视频,所有多模态能力都由本插件在 agent/pre-step(自动注入)与 read_media 工具侧完成"翻译 → 文字注入",V4 作为纯文本主脑作答。
仓库内另保留
demo.gif(asciinema 终端录屏),展示 headless 模式下的三模态闭环,可作为补充参考。
它做什么
挂上这个插件后,基于 DeepSeek 的 Agent 能"看见 / 听见"图片、音频、视频。
关键实测结论(2026-08-17,真发请求验证):DeepSeek 的对话 API (
deepseek-v4-pro/deepseek-v4-flash/deepseek-chat)是纯文本的, 收image_url会返回400 unknown variant image_url, expected text。 因此"直接多模态"V4 自己做不到,必须靠插件侧做视觉翻译。本插件正是那个机制: 媒体 → 可插拔视觉 / 语音后端 → 文字描述 → 作为textblock 注入 V4, 让 V4 当"主脑(文本)"、插件当它的"眼睛"。
三条模态路径(按文件类型自动分派)
- 图片(image):路径 / URL → 视觉后端(moondream 等本地 VLM)翻成文字。
- 视频(video):
ffmpeg按videoFps抽最多videoMaxFrames张帧 → 逐帧送视觉后端 描述 → 拼成带Frame i/N标号的文本块。 - 音频(audio):
whisperCLI(本地 ASR)或外部 ASR HTTP 端点翻成文字转录。
三种接入方式:
- pre-request hook(默认开启,最"直接"):在
agent/pre-step(Harness 官方 waterfall 扩展点,决定"模型能看到什么")上注册监听器,自动扫描每条消息里的 媒体路径 / URL,按类型经可插拔后端翻译为文字后注入——模型无需主动调工具即"看见 / 听见"。 read_media工具:模型也可主动调用,显式处理媒体文件(同样按类型分派)。set_multimodal_backend工具:模型 / 用户可在会话内实时切换后端或模型 (如把vision_model换成更强模型,或中途关掉auto_inject),无需重启 dsh。- 单条消息内联提示:在媒体路径后紧跟
@vision=<backend>/<model>或@asr=<backend>/<model>,仅对这一条消息临时覆盖后端 / 模型,不动全局配置。 例:看 /tmp/x.png @vision=moondream、/a.mp4 @vision=ollama/llava、/t.wav @asr=whisper/small。
架构(契合 Cordis 范式)
- 解耦前端(核心):图 / 声 / 视频各有独立翻译器(可插拔
visionProvider), 输出统一的textcontent block。V4 原生不收图,这一步是必需的。 - 空间可组合(coeffect):
inject: ['tools']声明依赖,Cordis 等服务就绪才加载。 - 时间可组合(effect):插件卸载 / 热替换时,已注册的 hook 与工具随
ctx自动清理。
用户媒体(图/声/视频)
│ 解耦前端: 各模态独立翻译器 (可插拔后端)
▼
视觉后端 (本地 VLM / 外部 vision API) ──┐
音频 ASR (whisper 等) ──┤→ text block
视频 ffmpeg 抽帧 + 视觉后端 ──┘
│
▼
DeepSeek Harness (Cordis) ── dsh-plugin-multimodal ──► V4 (纯文本主脑)
配置可配置性(四级优先级)
同一个旋钮有四层来源,优先级从高到低:
单条消息内联提示 > 会话内 set_multimodal_backend 工具 > 环境变量 > patch.yml / schema 默认
-
profile 级静态配置(
cordis.patch.yml的config:或 schema 默认值)—— 重启 dsh 生效。 -
环境变量(启动前 export,最高优先于静态配置,适合开发 / CI 临时覆盖):
环境变量 对应旋钮 MULTIMODAL_AUTO_INJECTautoInject(true/false) MULTIMODAL_VISION_BACKENDvisionBackend MULTIMODAL_VISION_MODELvisionModel MULTIMODAL_OLLAMA_URLollamaBaseUrl MULTIMODAL_VISION_HTTPhttpEndpoint MULTIMODAL_FFMPEG_BINffmpegBin MULTIMODAL_VIDEO_MAX_FRAMESvideoMaxFrames MULTIMODAL_VIDEO_FPSvideoFps MULTIMODAL_ASR_BACKENDasrBackend MULTIMODAL_ASR_MODELasrModel MULTIMODAL_WHISPER_BINwhisperBin MULTIMODAL_ASR_HTTPasrEndpoint MULTIMODAL_VISION_MODEL=llava MULTIMODAL_ASR_MODEL=small dsh --profile headless "看 /tmp/x.png" -
会话内工具切换:
set_multimodal_backend(参数vision_backend/vision_model/asr_backend/asr_model/auto_inject,均可选),调用后影响该会话后续所有请求。 -
单条消息内联提示:
@vision=/@asr=仅作用于一条消息(见上)。
插件实现(与官方 Cordis API 对齐)
lib/index.js 是标准 Cordis 插件,导出 Config / apply / inject / name:
import "@deepseek-ai/cordis";
import z from "@deepseek-ai/schemastery";
import { defineTool } from "@deepseek-ai/dsh-tools";
import { createUserMessage } from "@deepseek-ai/dsh-llm";
import { findMediaTokens, messageText, makeVisionProvider, makeASRProvider,
detectMediaType, extractVideoFrames, describeFrames,
mergeEnvConfig, hintToOverrides } from "./core.js";
const name = "dsh-plugin-multimodal";
const inject = ["tools"];
function apply(ctx, rawConfig = {}) {
const config = mergeEnvConfig(rawConfig); // ① 环境变量覆盖最高优先
const runtime = { ...config }; // 可变运行时态(工具切换改它)
let vision = makeVisionProvider(runtime);
let asr = makeASRProvider(runtime);
// 按媒体类型分派到对应翻译器;单条消息的内联提示仅覆盖本条
async function translate(tok) {
const override = hintToOverrides(tok.hint);
const c = Object.keys(override).length ? { ...runtime, ...override } : runtime;
const v = makeVisionProvider(c); const a = makeASRProvider(c);
if (tok.type === "video") {
const frames = extractVideoFrames(tok.path, { ffmpegBin: c.ffmpegBin, maxFrames: c.videoMaxFrames, fps: c.videoFps });
return describeFrames(frames, v); // 抽帧 → 逐帧视觉 → 拼文本
}
if (tok.type === "audio") return a(tok.path); // 本地 whisper / 外部 ASR
return v(tok.path); // 图片 / URL 走视觉后端
}
// 主路径:pre-step hook 自动注入(autoInject 每次读 runtime,可被工具切换)
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
if (!runtime.autoInject) return next();
const decision = await next(); // 先拿到下游决策(含最终 messages)
if (decision.kind === "reject" || signal.aborted) return decision;
// ... 扫描 media token → translate() 按类型翻译 → createUserMessage 追加一条插件消息
return { kind: "enter", messages: [...decision.messages, injected] };
});
// 显式入口 1:read_media 工具(同样按类型分派,支持内联提示)
// 显式入口 2:set_multimodal_backend 工具(会话内实时切换后端 / 模型)
ctx.tools.register(defineTool({ name: "read_media", /* ... */ async execute(args){ /* translate() */ } }));
ctx.tools.register(defineTool({ name: "set_multimodal_backend", /* ... */ async execute(args){ /* 改 runtime + rebuildProviders() */ } }));
}
- 纯逻辑层
lib/core.js不依赖 cordis,可独立单测 (媒体令牌提取、消息摊平、视觉后端降级、extractVideoFrames/describeFrames/makeASRProvider、mergeEnvConfig环境变量覆盖、hintToOverrides内联提示解析)。 - 符合 Harness 铁律 "model-visible means logged":注入内容进入模型可见上下文,由会话日志留痕。
本地加载(已实测可用)
需要 @deepseek-ai/dsh(0.1.0-rc.6)与 Node ≥ 20,外加三个可选系统依赖:
- 视觉后端:本地 Ollama(
ollama pull moondream && ollama serve)。moondream~1.7GB、CPU 友好;llava(7B) 在本机实测会 OOM,不建议。 - 视频抽帧:系统
ffmpeg(本机 8.0.1,PATH 内即可;可用ffmpegBin覆盖)。 - 音频 ASR:本地
whisperCLI(openai-whisper,pip install whisper;whisper --model base首次会下载 ~145MB 模型,之后离线可用)。也可用asrBackend: http指向自建 / 第三方 ASR 端点。
① 让包可被 profile 解析(二选一):
- 发布后:
dsh plugin --profile web add dsh-plugin-multimodal - 本地开发(本仓库验证用过的方式):把插件与其 deepseek 依赖按真实路径 symlink 进
profile 的
node_modules(见scripts/link-local.sh):
bash scripts/link-local.sh web
bash scripts/link-local.sh headless
② 在 profile 的 cordis.patch.yml 加一行 insert(用户可编辑层,附带于
bundle 层之后):
- insert:
- id: multimodal
name: 'dsh-plugin-multimodal'
config:
autoInject: true
visionBackend: ollama
visionModel: moondream
ollamaBaseUrl: http://localhost:11434
ffmpegBin: ffmpeg # 视频抽帧
videoMaxFrames: 4
videoFps: 1
asrBackend: whisper # whisper | http | none
asrModel: base
whisperBin: whisper
asrEndpoint: "" # asrBackend=http 时填端点
③ 启动:dsh web(或 dsh --profile headless "看 /tmp/x.png 是什么")
开发 / 单测
node --test test/core.test.js # 纯逻辑层 17/17 通过(含视频抽帧、ASR 降级、环境变量/内联提示/工具切换)
端到端验证(真发请求,已实测通过)
真正的多模态闭环用 dsh --profile headless 实跑(插件 hook 会在 pre-step 自动注入):
ollama pull moondream && ollama serve # 本地视觉后端(图 / 视频帧)
whisper --model base # 首次下载 ASR 模型(之后离线)
dsh --profile headless "look at /tmp/dsh-mm-test.png and tell me what you see"
dsh --profile headless "describe the video /tmp/dsh-mm-test.mp4"
dsh --profile headless "transcribe the audio /tmp/dsh-mm-test.wav"
图片实测(2026-08-17,真实请求,EXIT=0):V4 正确回答"红上蓝下两横条"(自述不收图、靠注入描述)。
视频(ffmpeg 抽帧):插件抽 N 帧 → 逐帧 moondream 描述 → 注入 Frame i/N: ...,V4 据此回答画面内容。
音频(whisper ASR):插件调 whisper --model base 转写 → 注入文本,V4 据此回答语音内容。
配置三级动态切换(均已实测)
# ② 环境变量覆盖(最高优先于 patch.yml;改模型无需编辑配置、无需重启)
MULTIMODAL_VISION_MODEL=llava dsh --profile headless "看 /tmp/x.png"
# ③ 会话内工具切换:让模型/你中途把视觉模型换成更强的
dsh --profile headless "先调用 set_multimodal_backend 把 vision_model 设为 llava,再看 /tmp/x.png"
# ④ 单条消息内联提示:仅本条用 ollama/llava,其余消息仍走默认 moondream
dsh --profile headless "看 /tmp/x.png @vision=ollama/llava"
要点:四级来源(patch.yml → 环境变量 → 工具切换 → 内联提示)优先级递增,越靠后越临时、 越精确;内联提示只对单条消息生效,工具切换对该会话后续所有请求生效,环境变量对本次 dsh 进程生效。
四级切换均已在真实 harness 实测(2026-08-17):
MULTIMODAL_VISION_BACKEND=none启动 → 自动注入上下文显示 "vision backend disabled", V4 随之自行调用set_multimodal_backend切回ollama/moondream再描述图片 (环境变量覆盖 + 会话内工具切换在同一次真跑中同时被验证)。/tmp/x.png @vision=none→ V4 在自动上下文里直接引用 "vision backend disabled", 证明单条消息内联提示只作用于本条、覆盖全局默认。- 普通
autoInject: true图文请求仍正确返回 "红上蓝下双色旗"(回归通过,EXIT=0)。
要点:DeepSeek V4 本身不收图(实测 read_image → model does not declare image input),
但本插件的 agent/pre-step hook 自动把图经 moondream 翻译成文字注入上下文,V4 作为纯文本主脑
据此正确回答了"红上蓝下"。这说明"直接多模态"在 DeepSeek 文本 API 上只能靠插件侧视觉翻译实现,
而本插件正是该机制,且已挂进本地两个 profile(web / headless)跑通。
(可选)verify-e2e.mjs 也能单独验证"图→文字→V4"链路(生成测试图、经视觉后端翻译、发 V4)。
边界说明
- Harness 仍在开发者预览期(
0.1.0-rc.6),API 可能变。 - DeepSeek 对话 API 不收
image_url(已实测 400);图必须经视觉后端翻译为文字注入。 - 视觉后端默认
ollama(本地 VLM);也可配visionBackend: http指向自建 / 第三方 vision 端点,或none优雅降级为占位说明。 - 视频:靠系统
ffmpeg抽帧(最多videoMaxFrames张,频率videoFps),逐帧送视觉后端; 无 ffmpeg 时优雅降级为[video: no frames could be extracted]。 - 音频:
asrBackend: whisper调本地whisperCLI(默认base模型);也可配asrBackend: http指向自建 / 第三方 ASR 端点,或none降级为占位转录。
项目文件与信号
以下项目是目录快照中检测到的公开仓库信号。
仓库信息
- 开发语言
- JavaScript
- 许可证
- 未提供
- 最后更新
- 2026年8月17日 09:11
谨慎安装
请检查源代码、权限、生命周期脚本、依赖与网络访问;不受信任的插件应先在隔离环境中测试。