DSH WorkBuddy Connect
corrinehu/dsh-workbuddy-connect将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness,在 DSH 对话窗口里零配置使用。
为第三方模型配置推理强度档位,并在输入栏提供模型与强度选择器。
为 DeepSeek Harness(DSH)的第三方模型补充可配置的思考强度档位。
UI 分两处:
conversation.input.model 插槽,priority: -10 顶替内置选择器——该槽位升序取最小者渲染,内置为默认 0):点击输入框工具行的「选择模型」按钮,弹出 290px 宽的浮层(按 model-strength 原型):顶部胶囊(档位名 + 模型名 + ›)、底部推理强度滑块(轨道 23px、旋钮 30px,档位圆点与旋钮全部为 SVG 几何:<rect rx> / <circle>,不使用 border-radius、clip-path、transform 或百分比宽度,因此在桌面端与网页端渲染一致);点击胶囊以 150ms 淡入展开模型下拉(30px 选项行、按压实色、选中 ✓,外部按下/Escape 收起,Ctrl+Shift+M 切换,档位名 150ms 淡入淡出)。下拉覆盖在卡片上、左右与卡片对齐,底边与推理强度浮层的底边对齐(对话中从该底边向上长,不是胶囊正下方;新会话 hero 布局下改为向下长)。下拉高度固定为推理强度卡片高度的 2 倍(height: 200%,自动跟随卡片实际高度;另有 100vh - 24px 的屏幕兜底),模型放不下才出现滚动条;打开时整块浮层会按其真实高度重新摆放,因此永远完整可见、不会被视口切掉。浮层定位两态:输入框上方空间足够时相对触发按钮水平居中、置于上方;新会话 hero 布局下右对齐挂到输入框卡片下缘,卡片与模型下拉的顶边与输入框底边完全贴齐(不留缝);两种模式都夹在视口内,并随滚动/窗口变化重定位。面板崩溃时自动退位、内置选择器无缝接管。数据与 /model 命令共享同一份会话模型目录(ctx.modelDirectories),当前选中状态处处一致。settings.models.footer 插槽):「思考强度档位」卡片,集中配置每个 Pi-AI 路由模型提供的档位与思考开关格式。只有 Pi-AI 路由的模型可编辑,其余路由(档位由适配器内置提供)折叠成一行只读汇总,不再各占一行。llm-pi-ai 配置中对应模型的 reasoningEfforts(档位 id → reasoning_effort 线级拼写;off 留空 = 不发送参数)。providers 是 volatile 配置,写入后立即生效,无需重启。
off / minimal / low / medium / high / max(xhigh 已停用,见「注意事项」)DeepSeek 兼容 会为该模型写 compat.thinkingFormat: deepseek(off 发送 thinking: {type: disabled},其余档位发送 thinking: {type: enabled} + effort)。directory.select(provider + model + reasoningEffort),也就是内置 Effort 菜单的同一通道;插件本身不做任何持久化——「作为新会话默认值保存」是 session.selectModel 自带的宿主行为。任选其一(把 <路径> 换成插件目录的绝对路径):
<路径>。dsh plugin --profile <profile> add <路径><路径> = 本目录(含 package.json 与 cordis.patch.yml)。本 bundle 零外部依赖,可离线安装。
model-strength-picker/
├── package.json dsh.bundle.patch + dsh.client(web 客户端模块)
├── cordis.patch.yml 插入宿主插件行(id: model-strength-picker)
├── index.js 宿主半边:空实现(全部逻辑在客户端模块)
├── client.js Web 面板(React,挂 conversation.input.model + settings.models.footer)
├── icon.svg / locale/ 插件卡片图标与展示文案
├── model-strength.html 静态设计原型(对照用,不参与运行,也不在 package.json 的 files 白名单内)
└── README.md
llm-pi-ai 路由的模型可编辑;DeepSeek 官方路由(llm-deepseek 适配器)自带 Off/Low/High/Max,无需也不可在此配置。xhigh 已停用:编辑器不再提供该档位,自动声明与「默认思考强度」也不会再写入它。但 xhigh 在 llm-pi-ai 的 schema 里依然合法,所以如果某个模型的配置里已经写了 xhigh,它会被原样保留——打开再保存不会丢(该模型行仍会显示 X-High 标签),只有点「清除」才会连同它一起清空。若确实需要重新启用这个档位,把 client.js 里的 RETIRED_LEVELS 改成 [] 即可。high → reasoning_effort: "high");网关自有词汇(如 max: xhigh)可手动编辑 profile 的 cordis.patch.yml 微调。llm-pi-ai 时会把读取到的该路由 models 列表整体回写(数组整体替换语义),因此对同一模型条目上其它字段是保留式编辑;与 Models 页共用同一配置层。tool-subagent 行的 agentOptions.reasoningEffort,不需要时请在 $DSH_HOME/profiles/<profile>/cordis.patch.yml 中手动移除该键。settings / session remote 的形态可能变化;若更新后失效,按新版本调整 client.js。桌面端(Electron 壳)曾经把滑块画错:档位圆点渲染成方块(border-radius: 50% 被丢弃)、旋钮渲染成方圆角(border-radius: 9999px 解析异常),而同样的写法在浏览器里完全正常。逐步排查后确认:
3px / 50% / 9999px / SVG 四种写法在桌面端全部正常);滑块几何相关的 border-radius、clip-path、transform、百分比宽度已全部移除——请勿用它们画轨道、填充、刻度点或旋钮。唯一例外是 Max 档的星空动效(.msp-nebula-* / .msp-max-stars / .msp-max-sweep / .msp-burst-* 的 @keyframes),它用 transform 做位移与缩放、skewX 做扫光倾斜,且已被 prefers-reduced-motion 关闭;这些动画不参与任何几何布局。
拖动旋钮时不跟手连续移动,只在刻度点上停驻;换档时旋钮从旧刻度点滑到新刻度点,不瞬移。
逻辑层 —— 立即生效
idx = clamp(round(frac × (n-1)), 0, n-1),写进 dragFraction 的是 idx/(n-1) 而非连续的 frac;round 的 .5 上取语义)才换档;越过端点钳在首/末档,不越界;shownIndex、预览回调、松手提交、Max 特效的触发都用这一层,所以档位一换就生效,不等动画跑完。视觉层 —— requestAnimationFrame 补间
knobX = cy + inner * visual、fillW、showFill 全部由 visual 推导;visual 由 rAF 以 ease-out(1 - (1-t)³)补向 fraction,时长约 TWEEN_MS = 120ms;showFill 判的是 visual,所以滑到 Off 时填充是随旋钮一起缩没,不是瞬间消失;cx / width):不要改用 CSS transform 或 transition 实现动画。那既破坏本滑块「几何一律 SVG 原语」的约定,transform 还会动错盒子(transform 的参照框与 cx 不同)。注意 SVG 的 cx / width 本身是可 transition 的 CSS 属性,误加一行 transition 就会变成双重动画。fraction:换档会取消上一段 rAF 并从当前位置续滑,不会闪回。保持原样:点击轨道吸附、拖动阈值(10px)、键盘方向键 / Home / End、松手提交的档位算法。不要把连续 frac 写进 dragFraction,否则旋钮会重新跟手滑动。
桌面鼠标专属,触屏自动跳过:所有悬停处理器都检查 pointerType === 'mouse',touch / pen 一律忽略,按下/拖动行为不受影响。
grab,旋钮 r 15→16.5(外环同步 14.5→16,始终紧贴旋钮边缘),投影增强到 drop-shadow(0 2px 6px rgba(0,0,0,0.28)),约 120ms;按下拖动中光标 grabbing、投影保持既有 dragging 规则的最深一档(0.32),松开还原。拖动期间 pointer capture 会吞掉 over/out,所以 endDrag 松手时按旋钮最终落点重算一次悬停态,并直接驱动 r 补间——不能只切类等下一次 pointerover 来补,否则松手停在旋钮上会出现「grab 光标 + 悬停投影但旋钮还是小号」的错位。r 3→4.5 并提亮(100ms 数值补间)——露在灰轨道上的点把 fill 换成深灰(--dsw-alias-border-l2,比点基色 l3 高一档、明暗主题自适应;想换别的深浅改 DOT_HOVER_FILL 一处即可),被填充盖住的点把半透明白点的 opacity 0.4→0.75(渲染时白点写的是纯白 fill + opacity 属性,与原 rgba(255,255,255,0.4) 同效,补间的就是 opacity 本身);移开还原(fill 也复原)。点上方浮出该档档位名小标签(常驻一枚、靠 .on 淡入淡出,pointer-events: none,纯 HTML 不属于滑块几何)。Max 档刻度点整体淡出后,悬停提示与点击跳档仍然可用。tweenSvgAttr 数值补间直接写 SVG 属性(ease-out rAF,按「元素+属性」记账、重复触发先取消上一段),不用 CSS transform 缩放几何,也不给几何属性挂 CSS transition;投影走 filter 的 CSS 过渡(120ms),filter 不是几何属性。旋钮与刻度点各有一层透明命中圆(旋钮 r=17 盖在外环之上、半径 ≥ 悬停视觉半径以免放大/缩回抖动;刻度点 r=8 盖在可见点之上而旋钮之下)——悬停不再要求精确瞄中 3px 的小点,旋钮压住的点也不会被误报悬停。.msp-burst 补了 pointer-events: none,迸发小点从旋钮上起飞时不会盖住旋钮命中区。悬停投影规则必须放在 dragging 投影规则之前(同为 (0,3,0) 特异性靠源顺序让拖动的最深投影胜出)。焦点环、点击吸附、拖动阈值、位移动画一概未动。静态分层(自下而上,全部 SVG 原语,由 clipPath 裁在填充区的圆角矩形内):
| 层 | 内容 | 出现时机 |
|---|---|---|
1 渐变底 msp-max-base | 左→右 #2f6fed → #5a4bf0 → #7c3ff0 → #8b5cf6(右端亮堇紫,不是深紫) | Max 档 |
2 星云 ×2 msp-nebula-layer | 径向渐变圆 r=14 / r=11,白核 → rgba(200,180,255,0.45) → 透明,feGaussianBlur stdDeviation=3 | 只在 Max 档(与其余层一起);一团悬在旋钮左侧附近,一团在轨道中部偏右 |
3 星点 ×28 msp-max-stars | 白色 <circle>,半径 0.5–1.6px,其中 4 颗稍大并带 drop-shadow(0 0 2px) 光晕 | Max 档 |
4 高光 ×2 msp-max-sweep-layer | 两条 35% 宽的白色高光带,两端透明,skewX(-12deg),峰值不透明度 0.22 | 只在 Max 档(整层由 .level-max 门控),两条相位错开半周期交替扫过 |
5 旋钮光晕 .msp-svg-thumb.halo | 白旋钮外一圈 drop-shadow(0 0 6px) 柔光 | Max 档 |
迸发 .msp-burst | 12 颗淡紫小点(r 1.5–4px,#a78bfa / #c4b5fd)从旋钮边缘偏上偏右飞散 30–80px | 一次性:旋钮落到最高档的那一瞬 |
进入 Max 时,轨道上原有的刻度点(灰轨道灰点与填充下的白点,即 .msp-svg-dots / .msp-svg-dots-on 两组)随 250ms 一起淡出,星空接管轨道;离开 Max 时同步淡入。悬停提示与点击跳档不受影响。
动画(只动 transform / opacity):星星闪烁 2.2–5.5s 随机负延迟错峰、星群漂移 ±5px/16s、星云漂移 9s 与 12s、两条白色高光带每 7s 扫过(相位错开半周期);进出各 250ms 淡入淡出。
颜色全部走 CSS 变量(--max-grad-stop-* / --max-nebula-* / --max-star-* / --max-knob-halo / --max-sweep-opacity / --max-burst-*),便于主题化。
.msp-max-fx 只在最高档可见,但它在 Off 以上的所有档位都会渲染 —— 父级 opacity: 0 不会停掉子元素的动画。因此有一道冻结门:.msp-slider:not(.level-max) .msp-max-fx * 统一 animation-play-state: paused。新增动画元素时务必落在 .msp-max-fx 之内,否则这条规则盖不到它。opacity,.msp-max-fx 自身不能带。四层现在都是「最高档才出现」,阈值一致;分开写只是为了将来某层想单独提前或延后时不必再拆结构。新增图层要同时做两件事:类名加进那条共享的 opacity: 0 规则,并接进 .level-max 的 opacity: 1 列表——漏掉后者它会在所有档位都可见。animation-delay。mspSweep 的前 55% 是「屏幕外 + opacity: 0」;动画被冻结后从 0 恢复的话,到达 Max 会有约 3.9 秒完全看不到高光。第一条 -3.9s 让它停在即将入场的相位上;第二条 .second 用 -0.4s,与第一条错开半个周期,否则两条会锁在一起只显得一条。高光层的 opacity 由 .level-max 门控,这是「只在最高档出现」的唯一保证——把 opacity 加回 .msp-max-fx 或给高光层单独放开都会破坏它。clipPath 之外(在旋钮之后)。小点要飞出轨道,裁在填充区里就没了。它是一次性的:靠 key: burstKey 重挂来重播,改结构时别丢掉这个 key。prefers-reduced-motion 下只留静态渐变:动画全关,扫光与迸发 display: none,星云与星点一并隐去——留下的只有渐变底和旋钮光晕这两层完全静止的图形。注意扫光的位移与透明度只写在 keyframes 里,动画一停会回落成轨道左侧一块白斑,所以必须 display: none 而不是只停动画。filter 是有意保留的:星云的 feGaussianBlur、光晕星的 drop-shadow(0 0 2px)、旋钮光晕的 drop-shadow(0 0 6px) 都是规格指定的视觉。这类滤镜作用在被 transform 动画驱动的元素上时会每帧重新栅格化,是已知的性能代价。若日后要换回零滤镜近似,可用多段 radialGradient 顶替(历史实现见 client.js.bak-before-maxspec-*)。