dsh-companion-window
已验证dsh-companion-window · v0.2.0 · MIT · Web 界面
陪聊窗口 —— 一个只在项目真的卡住时才出现的搭话窗口。它读工作区元数据(文件改动、深浅、节奏),不读文件内容;会话侧读你说过的话。逐条可核的清单见 README。
安装
dsh plugin add dsh-companion-window 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
发布到 npm 但没有公开仓库。安装前请检查包内容。
标签
作者
说明文档
dsh-companion-window(陪聊窗口)
一个挂在工作区旁边的搭话窗口。它平时不出声;当它从工作区的状态看出你可能卡住了,它会自己冒出来问一句,然后回去。
它不是心理医生,不做诊断,也不给建议清单。它的定位是搭子:说两句人话,问一个具体的、你答得上的问题。
它凭什么判断你卡住了
只看工作区的形状,不看内容:
| 它看的 | 它不看的 |
|---|---|
| 文件改动的时间间隔与节奏 | 文件内容(一个字节都不读) |
| 改动的文件数量、集中在几个文件 | 你写得好不好、对不对 |
| 目录结构与层级深浅 | 代码/论文里的任何文字 |
| 提交的时间点、提交信息、涉及的文件名 | 未提交的改动具体改了什么 |
| 会话间隔(你多久没说话) | 你说了什么(除了下面那一节) |
具体执行的命令只有这几条,都是只读的:
git rev-parse --is-inside-work-tree; git rev-parse --abbrev-ref HEAD
git log --since=120.days.ago --no-merges --pretty=format:%at%x09%s
git status --porcelain
git log --since=14.days.ago --name-only --pretty=format:%at
mkdir -p .peer
注意其中两条会读到提交信息和文件名——那是你写的字,所以在这里明说。
它会读对话原文吗
会。它读的比"只读元数据"多,所以这里逐条写清楚。
工作区那侧它确实只读元数据。会话那侧不是:
它读你在当前这一场会话里发过的消息。
它的模型如果觉得需要,可以调
read_turns展开某一段完整来回,看到双方的发言。它会看同一个目录下最近 6 场会话的标题,在你发过的话里搜下面这 16 个词,用来发现"你最近老在同一件事上卡住":
卡住 崩溃 撑不住 不想干 没意义 焦虑 失眠 熬夜 通宵 拒稿 被拒 来不及 做不完 难受 很累 坚持不下去这一项只保留命中次数和时间,不留原句——因为它会变成「要不要开口」的判据进到它的上下文里。
但你自己可以看。 设置里那张列表现在能点开:点一下会话标题,就把那场里命中的原话取回来摊在你面前(时间 + 原文)。这条路是你主动触发的,取回的文字只到界面,不进它的上下文、不落盘,记账里也只留条数。再点一下收起。
不读:思考/推理块、工具调用的参数与结果、其它目录的会话、子会话。
写这条的目的不是让你放心,是让你能核:上面每一句都对应源码里一个具体的调用点,搜 sqSvc. 就能看到。
它会往你的工作区写什么
一个 .peer/ 目录,三个文件:
.peer/state.json—— 开关偏好,和它上次开口的时间(防止它反复烦你)。.peer/notes.json—— 它的记忆。分三块,来源不同:note:它自己写的,每轮重写。lastNext:它上次给你的那个动作,由插件记录,模型改不了。pinned:只有你能写,模型永远不会覆盖它。
.peer/chat.json—— 你和它的聊天记录(双方原话 + 时间,最近 400 轮)。打开窗口就能接着往下看,不会因为切会话、刷新页面或重启 DSH 就消失。
notes.json 和 chat.json 的区别值得说清:前者是它的记忆,会进模型上下文;后者是你的记录,任何 prompt 都不读它,它也不参与「要不要开口」的判断。所以只有后者能留原话。
30 天不用自动过期。在设置面板里可以查看、编辑、清空。
删掉 .peer/ 目录不会有任何副作用——它会重新建。
安装
已发布到 npm,走官方插件渠道,和你装别的 DSH 插件完全一样:
dsh plugin --profile web add dsh-companion-window
profile 里记下的是 dsh-companion-window@^0.1.0,不依赖任何本地路径。
装完需要重启 profile:dsh.bundle.patch 声明的组合层要求新起一次进程才会挂上。
自己改代码时:打 tarball 装,不要装目录
npm run build && npm run pack
dsh plugin --profile web remove dsh-companion-window
dsh plugin --profile web add dist/dsh-companion-window-0.1.0.tgz
npm run doctor
不要用 dsh plugin add /path/to/packages/companion-window 装目录。 这不是洁癖,是 DSH 的模块解析决定的,而且失败方式很隐蔽。
DSH 的插件解析是「双锚点」的——包名从 dsh 安装目录或 profile 目录解析,但包自己内部的 import 由 Node 从它的 realpath 逐级向上找 node_modules。dsh plugin add <目录> 装成软链(link:),realpath 指向你的仓库,于是这条向上走的路走不到 $DSH_HOME/profiles/node_modules 那个安装闭包:
装目录(link:) 从注册表 / tarball 装
realpath = 你的仓库/… realpath = ~/.dsh/profiles/web/node_modules/dsh-companion-window
↑ 逐级向上 ↑ 逐级向上
桌面 → 用户 → 盘根 … ✗ profiles/web → profiles ✓ ← 安装闭包在这
后果分两种,第二种更坏:
- 直接
ERR_MODULE_NOT_FOUND,profile 起不来; - 有人为了让报错消失,在插件目录里跑了
npm install——报错是没了,但插件目录里多出一份私有的@deepseek-ai副本。这一份不报错,只是TypertRemoteService继承的Service基类和 profile 的 Cordis 不是同一个类,两套Symbol对不上,ctx.plugin(服务)静默失效:一切看起来都正常,窗口永远不出现。
装完先体检
node build/doctor.mjs # 默认体检 web profile
node build/doctor.mjs --profile xxx
它加载 profile 里真实的模块实例,完整跑一遍挂载,其中一条断言就是上面说的模块同一性。装得不对的话,它在重启之前就会告诉你哪里不对、怎么修——不用靠重启试错。
# 期望看到的最后一行
结论:这个 profile 里的安装是好的,重启后窗口会出现。
关闭
三个层次,代价从小到大。
一、让它闭嘴,但留着入口(立刻生效,不用重启)
右下角竖排按钮 → 打开 → 设置 → 「它什么时候自己出来」 → 「主动关心」点关。
之后它不再自己弹卡片;竖排按钮还在,你想说话随时点开。同一个面板里还有「翻历史对话」——关掉之后它不再去翻你其它会话。
这两项立刻生效,写在工作区的 .peer/state.json 里。
二、整个关掉,但保留安装(重启后生效)
在 ~/.dsh/profiles/web/cordis.patch.yml 里加两行:
- id: companion-window
disabled: true
行 id 是 companion-window,不是包名。加完之后 dsh --profile web --dump-config 会显示这一行被标了 disabled: true。
想只在一次启动里关掉、不动 profile:写一个临时 yml,然后
dsh --profile web --patch ./off.yml
想恢复:删掉那两行(或改成 disabled: false)。
npm run doctor 会认这个状态,不会再骗你说"重启后窗口会出现"——它会明确告诉你插件是关着的,并且继续把安装本身检一遍(关着不等于坏着,重新打开时得能挂上)。
三、卸载(彻底)
dsh plugin --profile web remove dsh-companion-window
这一条会连组合层一起撤掉。即使 profile 起不来,dsh plugin 子命令本身仍然可用(它直接调 pnpm),所以这条路是通的。
注意:卸载不会动工作区里的 .peer/ 目录。那份笔记还留在原地,要清就自己删。
自己核一遍
这个插件不要求你相信上面任何一句话。三条路,任选:
- 读源码。
lib/index.js是全部宿主逻辑,lib/client.js是全部界面逻辑,没有压缩混淆。搜sh(能看到它执行的每一条命令;搜sqSvc.能看到它读会话的每一个调用点。 - 看审计面板。 陪聊窗口的设置里有「审计」一项,它列出插件往外发过的每一次模型调用:轮次、用了多少 token、缓存命中多少。缓存命中率那一栏是给「每次说话都从零开始」这个毛病准备的证据。
- 看运行卡片。 这个插件注册了一个给 Agent 用的工具
project_state_snapshot。它只返回工作区元数据,不返回会话原文。工具卡片里写着它取哪些字段,对着源码能一条条核。
面板与设置
| 位置 | 是什么 |
|---|---|
| 右下角竖排按钮 | 手动开一次窗口。它是唯一的常驻入口。 |
| 主输入框右上角 | 它自己开口时,在这里弹一张小卡片。没事的时候这块地方是空的。 |
| 设置 → 陪聊 | 开关主动开口、开关回声、选模型、选推理档位、看审计、管理记忆 |
- 主动开口(默认开):关掉之后它就只在被叫的时候出现。
- 推理档位(默认低):调高会让它想得更久。它回答短,档位高了容易把 token 全花在思考上。
- 模型:默认跟随当前会话的默认模型。可以单独指定一个更便宜或更快的。
它明确不做的事
- 不做心理评估,不给诊断,不贴标签。
- 不做危机干预。出现自伤/伤人表述时,它没有能力处理,也不会假装能处理。
- 不建议你休息。它会给你一个身体上的小动作(喝水、站起来走两步、把窗户打开),不会说"你该歇歇了"。
- 不说"加油"「会好起来的」「我理解你」。这三句是这个插件最典型的失败方式,系统提示里被明令禁止。
DSH 升级了怎么办
先跑一次体检,再重启。
npm run doctor
它会打印本次依据的 DSH 版本,然后完整挂载一遍。版本号变了就是变了;结论不是"安装是好的",就说明契约动了。
为什么需要这一步
DSH 的模块解析升级时会自动修复——dsh-app-boot 每次启动都从当前安装重新解析依赖闭包、重建软链,这里不用管。
但内部 API 会变,而且有一半的失败是静默的:
| 变了会报错 | 变了不报错 |
|---|---|
Remote 描述符的 version: 1 |
defineTool 的 parameters 方言 |
| 服务的构造签名 | Remote 标记的格式 |
| 宿主服务的方法名 | 客户端槽位 key |
| 客户端的实参个数校验 | dsh.client.inject 里的包名 |
静默那一半的形状是:服务注册成功、界面一切正常、窗口永远不出现。所以插件启动时会自己验一遍,并把失败同时写进控制台和设置里的「审计」面板(「最近一次错误」那一行)。
界面上那句「最近一次错误」不是装饰——它就是为这类事准备的。
依赖
宿主半边需要:fs、shell、llm、agentDefaultModel、sessionQuery、workspaceRegistry、tools。
客户端半边需要:remote、slots、timer。
缺任何一个,插件会停在那等,不会半死不活地跑。
构建
lib/ 里的两个文件是生成物,不要直接改。人工编辑的是 build/ 下的规范源:
build/host.txt 宿主半边主体(15 个 Remote 处理器 + 工具 + 状态扫描)
build/client.txt 客户端半边主体(三个槽位 + 组件 + 样式)
build/build.cjs 施加"包壳"的生成器
build/doctor.mjs 启动前体检
test/ 三套运行期测试(见下)
npm run build # 重新生成 lib/index.js 与 lib/client.js
npm test # 生成之后跑三套测试(36 项)
npm run doctor # 体检当前 profile 里的安装
npm run pack # 打出 dist/dsh-companion-window-<version>.tgz
包壳只做这几件事:加 import、把处理器包成 TypertRemoteService 子类、手写 Remote
prototype 标记、把客户端主体包进 window.__ModuleLoader__ 工厂、host.call → rpc、
把样式挂进文档。
这样分工是因为:两个形态(开发用的动态插件、交付用的这个包)共享同一份主体,差异全是 机械的。手抄一份 78 KB 的源码一定会出静默错字——这个项目已经栽过三次,所以改成让脚本改。
测试
npm test
运行时的三套,都在真框架上跑,用的不是桩:
| 套件 | 验什么 |
|---|---|
| 宿主半边端到端(9 项) | 真 Context + 真 TypertRegistry + 真 TypertGatewayService。经 gateway 调 prefs 写偏好再读回;15 个方法全部可达;参数名写错与不存在的方法都被拒 |
| 开机自检(6 项) | 正向(健康时 lastError 为空)+ 反向(标记少认一个方法时必须出声,且服务照常注册——证明这确实是静默失败) |
| 客户端半边(21 项) | 伪造 __ModuleLoader__/document/react/timer。物化、样式、$mount、三槽位、四个组件渲染;遍历元素树调用所有 on* 处理器并模拟重渲染,走真实调用点穿过真实 rpc;逐条复现网关的实参校验 |
它们需要一台装过 DSH 的机器:@deepseek-ai/* 来自 DSH 的安装闭包($DSH_HOME/profiles/node_modules),不是本包的依赖。找不到就明确报错,不会静默跳过——静默跳过的测试比没有测试更糟。
测试自己搭沙箱:把 lib/index.js 复制到临时目录,在那里链到安装闭包,让 Node 的父级向上走能走到它。这就是当初踩过的那个坑——不搭沙箱,从仓库直接 import 会 ERR_MODULE_NOT_FOUND;而如果为此在仓库里 npm install 一份 @deepseek-ai/*,就会有两份 cordis、两套 Symbol,服务静默注册失败。链接指向闭包(而不是复制)正是为了保住模块同一性。
npm publish 会先跑 prepublishOnly → npm test。测试不过就发不出去。
改了 lib/ 之后必须重新打包并重装,profile 里那份是拷贝不是软链:
npm run build && npm run pack
dsh plugin --profile web remove dsh-companion-window
dsh plugin --profile web add dist/dsh-companion-window-0.1.0.tgz
npm run doctor
名字的来历
这个包本来叫 dsh-side-chat,改名是因为 npm 上已经有同名的、互不相关的插件(super-cabbage 的「并行侧边对话」)。查证之后发现撞的不止包名——他的客户端模块 id 也是 dsh-side-chat,cordis 行 id 也是 side-chat。
两个插件同时装的话:行 id 相同会让组合层重复注册(他自己的 cordis.patch.yml 里就写着「否则重复注册崩溃」),客户端模块 id 相同会让模块表重复。所以三处标识符都得换,而它们都由 build/build.cjs 里的 PKG_NAME 一处派生。
线上命名空间也从泛化的 sideChat 收窄成了 companionWindow;服务类名从包名派生为 CompanionWindowGateway。
发布
0.1.0 已于 2026-09-15 发布到 npm(maintainer gzl13,MIT)。
下次发版:
npm run doctor # 1. 确认当前 profile 里的安装是好的
npm view dsh-companion-window version # 2. 确认名字仍是自己的
npm version patch # 3. 升版本
npm publish --otp=<6位码> # 4. 发布(认证器 App 里的当前验证码)
dsh plugin --profile web remove dsh-companion-window
dsh plugin --profile web add dsh-companion-window # 5. profile 换到新版本
npm run doctor # 6. 复验
npm publish 之前会自动跑 prepack → node build/build.cjs,重新生成 lib/ —— 发出去的产物一定是规范源生成的,不可能是过期的。
一个会绊住你的东西:npm 现在强制 2FA
不带验证码直接 npm publish 会 403:
Two-factor authentication or granular access token with bypass 2fa enabled
is required to publish packages.
两条路:账号开 2FA 现场输码,或者用带 bypass-2FA 的 granular token。但后者正在被拆——npm 公告说 2027 年 1 月起它连直接发布也不给了。所以用 --otp= 最省事。
注意 npm publish(不带 --otp)在 2FA 开启后会走浏览器确认流程:它打印一个 https://www.npmjs.com/auth/cli/<uuid>,进程一直轮询 /-/v1/done。进程活着 ≠ 发布成功——得去浏览器点那一下。
发出去的包里包含 build/(规范源 + 生成器 + 体检脚本)。这不是顺手带的:README 说「源码由规范文件机械生成」,那就得让人能自己验,而不是只能信。
许可
MIT