有些画面只有几秒,却让人想反复回看:海风吹起头发的那一刻,孩子突然笑起来的瞬间,或是旅行途中刚好遇见的一束光。视频记录了过程,照片留下了定格;而 Live Photo,让这两种记忆可以在同一张照片里相遇。
Vimemo 实刻是一款面向 iPhone 和 iPad 的视频转实况照片工具。挑选一段视频,选好想保留的片段和封面,就能把熟悉的动态瞬间整理成 Live Photo。

有些画面只有几秒,却让人想反复回看:海风吹起头发的那一刻,孩子突然笑起来的瞬间,或是旅行途中刚好遇见的一束光。视频记录了过程,照片留下了定格;而 Live Photo,让这两种记忆可以在同一张照片里相遇。
Vimemo 实刻是一款面向 iPhone 和 iPad 的视频转实况照片工具。挑选一段视频,选好想保留的片段和封面,就能把熟悉的动态瞬间整理成 Live Photo。

DeepSeek Harness(下称 DSH)的 Provider schema 其实相当完整:重试、超时、请求头、图片预算、思考档位、二十多个兼容性开关……但其中一多半在界面上没有入口,只存在于 profile 的
cordis.patch.yml里。我做的 Advanced Provider Settings 插件把这些字段做成了看得见、点得到的设置面板,并且把「模型」变成了面板的主角。这篇主要讲三张截图对应的三块:按模型配置、带退避曲线的重试策略、视觉与推理预算,其余面板最后也一并过一遍。当前版本 v0.4.5,已在 DeepSeek Harness 0.1.7-alpha.1 上验证。
DSH 开箱最不擅长的场景,恰恰是把 Harness 指向一个第三方 DeepSeek 端点——云上的聚合网关、公司代理、nginx 后面自建的 vLLM。DSH 的兼容性是按 Provider id 与 baseURL 探测的,只有 URL 里含 deepseek.com 的主机才会被认成 DeepSeek,其余一律拿到一套 OpenAI 形状的默认值。于是你选的思考档位被打包成端点看不懂的形状、该发 max_tokens 的地方发了 max_completion_tokens、厂商根本没实现的 store 字段照发不误。
最要命的是,这一类配置失败的时候一声不响:配置时没有任何报错,只是请求并没有按你选的档位去做。而这些真正决定行为的字段——重试怎么退、图片多大要拦、思考方言是哪一种——原本都只能手改 ~/.dsh/profiles/<profile>/cordis.patch.yml。
这个插件不改变这些字段的归属,只是把它们呈现出来:不改 Harness 一行源码、不 patch node_modules、不保存第二份配置,读写走 DSH 公开的 settings API,写入是按路径的最小操作——你的 YAML 保留注释,没动过的字段原样保留。

「按模型配置」一次只编辑一个模型。顶部一排模型胶囊,带「图」标记的表示它声明了图片输入,右侧的小圆点表示这个模型身上已经有覆盖——不点开也知道哪里配过。截图里的 opencode.ai/deepseek-v4.1-flash 就是一个被我手动声明过覆盖的模型。
第一行声明这个模型接受什么输入:继承、仅文本、文本 + 图片,这里声明的类型会盖过模型目录的声明。目录里没说它能收图,就在这里手动打开「文本 + 图片」。
往下是模型档位映射,它回答的问题是:这个模型管某一档思考叫什么。阶梯是 off → minimal → low → medium → high → xhigh → max,每一格里填的字符串,就是这一档实际发到线上的值。截图里 low、中、高都映射到同名档位,关闭、极简、极高留空——留空代表该模型不支持这一档,请求点名它会得到一个带名字的错误,而不是被悄悄丢掉。如果这个模型根本不推理,点右侧的「无推理(false)」,Harness 就不再为它提供任何推理控件,也不发任何相关字段。
另外,路由用内置模型目录时,按模型配置走的是 modelOverrides.<id> 通道,同一个编辑器还负责改这个模型的显示名称、上下文窗口与最大输出 token——因为那条通道没有别的编辑器。

重试永远是路由级的——既没有按模型的重试,也没有全局重试。插件给了 Harness 默认 / 保守 / 激进三档预设,也可以完全自定义:模式(按错误码 / 始终重试)、最大重试次数、可重试错误码、初始延迟、最大延迟、抖动比例。错误码可以任意增删,因为 Harness 接受任意错误码。
截图里是我的实际配置:只重试 EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT 这五类错误,最多重试 10 次,初始延迟 1 秒、封顶 5 分钟、抖动比例 0.2。底下那根退避曲线把这些数字画成形状——每次重试一根柱子,长度对应真实等待时间,右上角给出累计等待 13 分 31 秒。「重试 4 次、500 毫秒、翻倍、上限 8 秒」是一种形状,看成一张图更容易判断合不合理。

视觉能力是 Provider 级的,共三项预算加一个默认值:未声明输入类型的模型的默认输入模态(截图里是「文本 + 图片」)、单请求图片总字节上限(20 MiB)、单请求像素预算(4 MiB ≈ 2048 × 2048 像素)、单张图片字节上限(1 MiB)。单位全部用 MiB 说话,差 1000 倍这种错误一眼就能看出来。某个模型能不能收图,则回到「按模型配置」里逐个开——给一个并不支持图片的模型声明图片能力,只会让 Provider 报错。
推理卡片配的是路由级默认思考档位(截图里是 high)与各档 token 预算。xhigh 与 max 虽然 schema 接受,但在请求发出前会被归并到 high 的预算上,所以真正要填的只有 high 一档,卡片直接用文字说明这一点,而不是给出一档实际不存在的粒度。预算还会被钳到这次请求留给答案的空间:输出上限只有 8000 的请求,配 32000 的预算最终就是 8000。
有一个值得注意的边界:路由默认档位会拿去跟实际收到请求的模型校验,模型不支持的档位不会被降级,而是直接抛 UNSUPPORTED_REASONING_EFFORT。所以在同时挂着推理与非推理模型的混合路由上,请把这一档留在「继承」,让每个模型自己的映射去决定。
user-agent 会被 Harness 剥掉、再发它自己的归属标识(只有全局层能真正生效);自己配的 authorization 会顶掉 API Key。sse / websocket / websocket-cached / auto)、请求超时、流空闲超时、WebSocket 连接超时、缓存保留策略。compat 开关,并按该路由协议实际会读取的字段过滤——包括思考方言(deepseek、openai、openrouter、qwen、chat-template 等)与承载思考预算的字段名。每个开关三态:继承 ≠ 关闭。需要 DeepSeek Harness 0.1.7 及以上。Web UI 用 CLI 安装:
dsh plugin --profile web add github:misswell/dsh-advanced-provider-settings
桌面版在应用的「插件市场」里安装 misswell/dsh-advanced-provider-settings,重启即用。两个 profile 跑的是同一个产物,机器上不需要构建。
项目在 GitHub 开源(MIT):misswell/dsh-advanced-provider-settings。更多细节——按模型的第二条通道、思考方言的具体行为、安全设计——都在 README 里;遇到配不出来的形状,欢迎带上诊断面板的报告提 issue。
如果你平时在用 Claude Code Router,可能经常需要打开终端启停服务。CCRBar 是一个原生 macOS 菜单栏应用,把这些操作和运行状态放到菜单栏里,随时可以查看和使用。
环境:DeepSeek Harness Desk 0.3.37(运行时 dsh 0.1.5-rc.2,内置 Node 24.19.0),底层 LLM 库
@earendil-works/pi-ai0.85.1,接入 OpenCode Go(Console Go)网关。
一天之内踩了两个坑:先是所有请求返回 400MissingSessionID,修好之后又发现自定义接入的deepseek-v4.1-flash不能看图。两个问题的根因都不在报错指向的地方。这篇把排查路径、验证手法和最终配置完整记录下来。
OpenCode Go 是 OpenCode 提供的订阅制网关,端点统一在 https://opencode.ai/zen/go/v1/,按模型分三类协议:/chat/completions(DeepSeek、GLM、Kimi 等)、/messages(MiniMax、Qwen)、/responses(Grok、GPT 等)。
在 Harness 里,模型不由内置列表决定,而是写在用户配置 ~/.dsh/settings.yaml 的 llm-pi-ai.providers 段里。每个 provider 的路由键(下文的 opencode2)是配置字典的 key,模型列表是它的 models 数组。
因为 Harness 内置的 opencode-go catalog 里没有 deepseek-v4.1-flash(三个 DeepSeek 条目分别是 deepseek-v4-flash、deepseek-v4-flash-vision-exp、deepseek-v4-pro),所以我们用一个自定义路由键 opencode2 手动声明它。
这个「自定义路由键」的决定,是后面两个问题共同的伏笔。
对话请求全部失败:
{
"type": "MissingSessionID",
"message": "Error from provider (Console Go): Request is missing x-opencode-session and cannot be routed efficiently."
}
先去查官方文档,得到一个关键线索:OpenCode 把使用方分成 validated clients 和 known problematic 两类,而 DeepSeek Harness 被明确列在后者,备注是「session info missing on some model paths」——也就是说这是已知缺陷,不是我们配置错了。
社区讨论帖(deepseek-ai/deepseek-harness#5495)把机制讲得更清楚:网关从某日起要求每个推理请求带一个稳定的 x-opencode-session 头,用于把同一会话的请求路由到同一后端,以保持 prompt cache 热着。而 Harness 的请求链路
dsh-agent-loop → dsh-llm → dsh-llm-pi-ai
直接调用 pi-ai 的 streamSimple,绕过了上游 pi-coding-agent 里负责合并归因头的 mergeProviderAttributionHeaders,于是任何会话身份都没能到达 adapter,自然也没进请求头。
讨论帖里列了 6 种解法,按口碑排序,社区插件 dsh-opencode-session 是其中最干净的一档:它监听 Harness 的 llm/stream 事件,在匹配的 provider 上把一个进程内 AsyncLocalStorage 存起来,然后 patch 全局 fetch,在请求真正出网时把头合并进去,值复用 Harness 本来就带着的会话 ID。
检查本地发现插件其实已经装好了——但我们还是 400。原因出在插件的默认配置上:
// 插件源码
const DEFAULT_PROVIDERS = ['opencode', 'opencode-go']
插件默认只对路由键 opencode / opencode-go 注入头,而我们的路由键是自定义的 opencode2,根本不在匹配范围内。装了等于没装。
Harness 的插件树由 Cordis 加载器按 patch 层组合,每层是一个 YAML 数组。用户层在 ~/.dsh/profiles/web/cordis.patch.yml。我们在这一层按 id 定向覆盖插件那一行的配置,把路由键补进去:
- id: opencode-go-session-header
config:
providers:
- opencode
- opencode-go
- opencode2 # 我们自定义的路由键
mode: session-id
debug: false
这里有个容易踩的坑:读加载器源码(applyEntryPatches)后发现,id 定向 patch 的合并语义是
for (const [key, value] of Object.entries(overrides)) {
target[key] = value // 整字段替换,不是深合并
}
所以 config 是整块替换的。如果只写 providers,原本的 mode / debug 会连同整块 config 一起消失。必须写全。
这次没有靠「改完重启试试」,而是顺着数据流一路验证:
composeEntries(而非自己写个 YAML 合并)跑一遍「插件 bundle 层 + 我们的用户层」,确认最终行的 providers 里确实有 opencode2。dsh --dump-config --profile web,输出里能看到加载器自己标注的 # ... patched by /Users/.../cordis.patch.yml,并且 providers 已含 opencode2。debugFile,改动落盘后该文件出现了 36 条记录,全部是 provider: opencode2 的真实调用,每条都带上了 x-opencode-session,值就是会话 ID。顺带确认了一件省事的事:cordis.patch.yml 是被 Cordis HMR 热监听的(watchUserPatches),文件一改,加载器会重组整棵 patch 栈并事务性重新应用——不需要重启服务。
deepseek-v4.1-flash 接进来了,但传图片时模型看不到内容,附件也像是不可用。
Harness 判断一个模型能不能看图,靠配置里的 input 字段(PiAiModality,取值 text / image)。而模态的解析有三层优先级:
模型条目的 input → 该路由的 defaultInput → 兜底默认
关键在于:第一层和第二层的「继承」只对 pi-ai 内置 catalog 里的 provider 生效。我们的路由键 opencode2 不是 catalog provider,模型条目又没有声明 input,于是直接落到第三层兜底 defaultInput: [text]——视觉能力就这么被关掉了。
实测最能说明问题。我们用一个模块探针(后面细说)对同一份真实配置做前后对比:
声明 input 前:deepseek-v4.1-flash | input: ["text"]
声明 input 后:deepseek-v4.1-flash | input: ["text","image"]
动手改配置之前,先要回答一个前提问题:这个模型到底支不支持图片? 因为如果网关本身不支持,声明 input: image 只会让模型在拿到图之后中途报错,比看不到图更糟。
官方文档说 deepseek-v4.1-flash 是纯文本,四个 DeepSeek 模型里只有 deepseek-v4-flash-vision-exp 处理图片。但文档可能滞后,于是直接实测。
第一次测试是错的。我发了一张纯色图问「什么颜色」,三个模型(含公认的纯文本模型)全都答对了 "Red"。这说明这个测试没有区分度——纯色图靠猜都能中,所以结论无效。
于是设计了一个猜不中的对照实验:在白色背景上画一个具体的数字(用点阵字体渲染),问「图里是什么数字」,并加一组不带图的对照组。
结果非常明确:
deepseek-v4.1-flash 带图 → "7"
reasoning_content: "The image shows a pixelated digit that looks like a 7.
The top horizontal bar and the diagonal descending
stroke are characteristic of the digit 7."
换一个数字 4 再测一次,同样答对,推理过程还准确描述了「左侧竖笔画、横笔画」的结构。
结论:网关实际上是支持视觉的,官方文档过时了。 问题纯粹在 Harness 侧的配置声明。
在 ~/.dsh/settings.yaml 的模型条目下补上模态声明:
opencode2:
displayName: Opencode
apiKeyEnv: OPENCODE2_API_KEY
api: openai-completions
baseURL: https://opencode.ai/zen/go/v1
models:
- id: deepseek-v4.1-flash
name: deepseek-v4.1-flash
input: # 新增
- text # 新增
- image # 新增
改完后用 dsh-llm-pi-ai 导出的真实 Config schema 校验通过;再用适配器内部的 resolveRouteModels 对真实 settings 做物化,确认输出是 ["text","image"]。
settings.yaml 由 dsh-settings-file 用 chokidar 监听(watch 默认开启,源码注释写明「external edits hot-publish through the seam」),所以同样是热生效,不用重启。
复盘下来,两个 bug 本身都不复杂,但有几点思路值得单独拎出来。
400 是网关抛的,但根因在客户端的请求构造路径;「没有视觉」表现为功能缺失,但根因是一行配置声明。如果盯着报错的地方找,会一直在错误的方向上打转。
这次能快速收敛,很大程度是因为没有靠猜:
composeEntries,不是自己写的近似实现;resolveRouteModels,直接看物化结果。自己复刻一套逻辑去验证另一套逻辑,只会验证你的复刻。
内部函数没导出时,有个干净的办法:把模块复制到临时目录(软链运行时的 node_modules),在副本末尾追加一行导出,然后 import 这个副本。既拿到了内部函数,又不碰原始安装目录。
这是本次最有价值的一条教训。第一版视觉测试(纯色图)看起来通过了,但没有任何信息量——因为对照组(公认的纯文本模型)也「通过」了。
有效的测试必须满足:如果是阴性结果,它不可能碰巧通过。改成「读一个具体的数字」之后,纯文本模型无法靠猜满足条件,结论才立得住。
任何时候测试全绿都值得再问一句:这个测试有没有可能因为太简单而全绿?
官方文档白纸黑字说那是纯文本模型。实测证明它支持视觉。文档描述的是某一时刻的状态,而网关在持续迭代。涉及「某个端点到底行不行」这类事实问题时,一次实测胜过十页文档。
cordis.patch.yml 和 settings.yaml 都支持热更新,省去了重启。但反过来,没有重启这个明确的分界点,就需要更主动地去确认「改动真的被读进去了」。这次用的办法是:
debugFile 有没有新记录;--dump-config 看加载器的实际组合结果;lsof 检查进程持有的文件句柄 inode——如果磁盘上文件的 inode 和进程打开的 inode 不一致(原子写会换 inode),说明进程的缓存副本可能还是旧的,需要触发一次变更事件让它重读。input 字段的本质是声明,不是探测。源码注释把取舍说得很清楚:没有接口能问网关「你支持哪些模态」,所以只能靠配置声明。而两种猜错的代价不对等——
所以正确处理是:对确实验证过支持视觉的模型精确声明,而不是图省事给整个 provider 加宽泛的 defaultInput。
改动只涉及两个文件,都在用户目录下,没有碰 App 安装目录。
~/.dsh/profiles/web/cordis.patch.yml(修 400)
# 给 dsh-opencode-session 插件补上自定义路由键 opencode2。
# 注意:id 定向 patch 整块替换 config,mode/debug 必须重写。
- id: opencode-go-session-header
config:
providers:
- opencode
- opencode-go
- opencode2
mode: session-id
debug: false
~/.dsh/settings.yaml(修视觉)
llm-pi-ai:
providers:
opencode2:
displayName: Opencode
apiKeyEnv: OPENCODE2_API_KEY
api: openai-completions
baseURL: https://opencode.ai/zen/go/v1
models:
- id: deepseek-v4.1-flash
name: deepseek-v4.1-flash
input:
- text
- image
dsh plugin --profile web remove dsh-opencode-session 卸掉插件并清空 patch 文件。opencode2 这种自定义路由键依赖配置里显式声明,内置 catalog 一旦收录了 deepseek-v4.1-flash,就可以改用 catalog 默认值、省掉模态声明。grep -r "KEY" 配置文件 这类命令很容易把密钥整行带进终端历史或日志里。排查认证问题时用 grep -oE "^[A-Z_]+:" 之类只取字段名,或者干脆打码。如果已经发生了,记得轮换密钥。最近,我重新整理并更新了个人网站首页:www.liuguofeng.com。
如果你每天在邮件、聊天、浏览器、截图、输入法和窗口之间来回切换,MacPilot 想解决的不是再增加一个复杂的工具,而是把这些高频操作收进一个原生 macOS 菜单栏 App:减少干扰、自动处理重复动作,并让常用工作流始终触手可及。
CCRBar 是一款 macOS 原生 SwiftUI 菜单栏应用,专为 Claude Code Router(CCR)提供轻量、直观的日常管理入口:不用反复打开终端,就能查看服务状态、启动或停止 CCR,并快速进入 Dashboard。
如果你经常处理接口返回、调试网页、转换编码,或者只是想在浏览器里随手完成一项开发任务,XFeHelper 可以把这些零散工具集中到一个浏览器扩展中:34 个常用工具,开箱即用,默认在浏览器本地运行。
如果你喜欢 DeepSeek Harness 的工作方式,却不想每次都打开终端、配置运行环境,DeepSeek Harness Desk 提供了一个更顺手的选择:把 Harness 放进一个简洁的桌面窗口里,并补上菜单栏/系统托盘、通知和运行状态管理等日常使用体验。
Continue reading DeepSeek Harness Desk:更轻松地在桌面上使用 DeepSeek Harness
做 macOS 开发久了,磁盘空间总会在某个早上突然变少。Xcode 的缓存、模拟器、项目里的 node_modules 和 target,再加上 Docker 镜像和 Build Cache,平时不太注意,攒起来就是几十 GB。真正麻烦的不是找不到几个大目录,而是不确定哪些能删,删了会不会影响手头的项目。