前言¶
DSH(DeepSeek Harness)的思路是一切皆插件。落到 web GUI 上,一个问题很快出现:Chat 模式插件和 Code 模式插件都想占据会话栏,谁来提供那个「切换模式」的开关?侧边栏里一排会话长得都一样,哪些属于哪个模式、你现在看的是哪一个?
如果注册表随某一个模式发行,其他模式就得依赖那个模式的整个包,层叠关系就反了。下面介绍的 omdsh-basemode,做的就是把这个「席位」从各个「姿态」里拆出来。
这是什么¶
@omdsh-plugins/omdsh-basemode(插件中心显示名 Base Mode / 基础模式)是 DeepSeek Harness web GUI 的会话模式系统,由 omdsh-plugins 维护,MIT 许可证,当前版本 0.2.5,在插件中心的分类为 system(来自 package.json 的 dsh.plughub.category)。
一句话定位:它是每个模式插件注册 segment 的注册表,是渲染这些 segment 的切换器,也是给侧边栏会话按模式上色的圆点。
要强调的是:它自己不发明任何模式。Chat 来自 omdsh-chatmode,Code 来自 omdsh-codemode。它唯一贡献的姿态是 Work——harness 本身的会话栏,作为基线注册进去,让切换器有地方可以切回去。
核心功能¶
注册表与切换器¶
sessionModes服务通过ctx.provide提供,是每个模式插件注册 segment 的入口。- 模式切换器是
shell.overlay(ui-layout 的全框架浮层)里的一个条目,居中于会话栏,无人使用时自动隐藏。 - 注册表强制同一时刻恰好一个 segment 处于 active:标记一个 active 会清除其余,贡献者通过自己的条目变 false 得知失去了会话栏。
基线姿态 Work¶
- 通过
registerBaseline注册,就是 harness 自身的会话栏,使用与 omdsh-chatmode 相同的名称、文案、颜色与图标。 - 任何声明
fallback或占用其 id 的注册会让它让位;那个插件卸载后,基线恢复。 - 它的 active 标志是派生值:恰好在其在位且没有贡献者接管时持有会话栏。
- 基线单独存在时不渲染切换器——单 segment 的控件无从切换。因此只有模式系统而没有模式插件的 profile,什么都不显示。
侧边栏标记¶
- 通过
row-marks.ts在侧边栏每行绘制模式色圆点:直接画在既有的行上,由注册表的tone与owns驱动。 - 在会话栏正在展示的会话上绘制侧边栏高亮,仅在会话栏与选中项不一致时写入。
New Session 的去向¶
- 覆写
workspaces.startSession:New Session 请求先交给持有会话栏的活动 segment。 - 无人接手时公告,并新建一个真正的新会话,而不是复用 workspace 旧的空白会话。
sessionModes.column报告会话栏实际显示的内容:活动 segment 声明了 scope 就用它,否则是选中的会话。
对 harness 本体的边界¶
- 不修改 harness 本体:注册槽位是公开席位,覆写是被自有属性遮蔽的原型方法,撤回行时两者都归还。
- 不注册 settings namespace:segment registry 没有可配置项,插件中心卡片没有表单。这是刻意的,不是遗漏。
为什么拆成独立的包¶
它曾属于 Chat mode,后来拆分为独立包,以消除层叠倒置。模式插件都需要注册表;如果注册表内置于某一个模式,其他模式就被迫依赖那个模式的整个包。拆分之后依赖是诚实的:每个模式插件依赖本包,本包不依赖任何模式。
安装与启用¶
本次核对的资料中没有官方安装命令原文,本文不做拼凑。安装方式请以仓库 README 与目录页为准:
- GitHub 仓库:https://github.com/omdsh-plugins/omdsh-basemode
- 目录页:https://www.skillhub.cn/plugins/omdsh-plugins/omdsh-basemode
从 package.json 可以确认的环境信息:node ^22.19.0 || >=24.0.0,包管理器 pnpm@11.7.0;客户端侧 platform 为 web,依赖注入 @deepseek-ai/dsh-client-ui-layout、@deepseek-ai/dsh-client-locale、@deepseek-ai/dsh-api-session-controller、@deepseek-ai/dsh-api-workspace-controller。
需要说明:本文依据的 README 与 package.json 在「The contract」一节附近被截断,其后可能存在的安装与配置章节未能核对,以仓库为准。
模式插件如何接入¶
这一节面向要写模式插件的读者。先拿到服务,再注册 segment:
// 不要写在顶层 inject 里——见规范第 9 条
ctx.inject(['sessionModes'], (mctx) => {
const modes = mctx.get('sessionModes') as SessionModes | undefined
if (modes === undefined) return
mctx.effect(() => modes.register({
id: 'code',
order: 20,
label: t('mode.code'),
hint: t('mode.code.hint'),
tone: 'var(--dsw-alias-state-error-primary)',
icon: createElement(IconCodeOutline16, { size: 14 }),
owns: isCodeSessionId,
available: true,
enter: () => { /* 按下后执行的导航 */ },
newSession: (workspaceId) => { /* 该模式开启了新会话时返回 true */ },
}))
})
经过上面的步骤,segment 就进入了注册表。有四条契约值得逐条记住:
1、类型只用 type 导入。SessionModes 从 @omdsh-plugins/omdsh-basemode/client 以 import type 导入,绝不作为值导入。跨插件的 VALUE 导入要么把本包运行时内联进你的 bundle,要么向 shell 的冻结模块表索要它答不上的 specifier,client bundle purity gate 会因此使构建失败。所以上面的服务用字符串 'sessionModes' 解析,而不是本包导出的 SESSION_MODES 常量——服务名是和运行时共享的线上名字,不是和包共享的符号。
2、同一时刻恰好一个 segment active。这是注册表强制的,不靠贡献者自觉:你标记自己 active 会清除其余;你看到自己的条目变 false,就是失去了会话栏。
3、文案带进来就是本地化的。切换器按原样渲染 label、hint、unavailableHint,自身只拥有两个词(switch.aria,以及模式不可用且未说明原因时的 fallback)。在 locale/change 时重新 update segment 以保持文案本地化。
4、enter 是一次导航,不是状态写入。按下之后由 segment 把世界变为真——打开会话、开始一个,或接管会话栏——active 标志由它自行上报。任何东西都不跨 reload 记忆模式。
两个字段的语义:owns 回答「这个会话是不是我的」;newSession 返回 true 表示该模式已开始一个新会话。
适用场景与注意¶
适合谁:
- 要向 DSH web GUI 贡献会话模式的插件作者,这里是必经的注册入口。
- 想自由组合模式的 profile 使用者。注意本包不发明模式,只装它不会出现任何模式按钮。
注意事项:
- segment registry 没有可配置项,不要在插件中心卡片里找表单。
- 插件以当前 dsh 进程权限运行,安装前请检查源码与许可证(本包为 MIT)。
- 星标数与官方安装命令在本次核对中缺失,README 亦有截断,最新契约说明以仓库为准。
结尾¶
omdsh-basemode 做的事很克制:一个公开的注册席位,一个居中且自动隐藏的切换器,侧边栏的圆点与高亮,外加一条 New Session 的分流规则。模式本身不归它管——Chat 与 Code 各自来自自己的插件,Work 是 harness 原有的会话栏。需要给 DSH web GUI 增加会话模式时,从这里注册。
- GitHub 仓库:https://github.com/omdsh-plugins/omdsh-basemode
- 社区目录页:https://www.skillhub.cn/plugins/omdsh-plugins/omdsh-basemode (独立站点,与 DeepSeek / 幻方无官方从属关系)