Chuyển đến nội dung chính

dsh-companion-window

Đã xác minh

dsh-companion-window · v0.2.0 · MIT · Giao diện web

陪聊窗口 —— 一个只在项目真的卡住时才出现的搭话窗口。它读工作区元数据(文件改动、深浅、节奏),不读文件内容;会话侧读你说过的话。逐条可核的清单见 README。

Cài đặt

dsh plugin add dsh-companion-window

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Tác giả

Readme

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  ✓ ← 安装闭包在这

后果分两种,第二种更坏:

  1. 直接 ERR_MODULE_NOT_FOUND,profile 起不来;
  2. 有人为了让报错消失,在插件目录里跑了 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/ 目录。那份笔记还留在原地,要清就自己删。


自己核一遍

这个插件不要求你相信上面任何一句话。三条路,任选:

  1. 读源码。 lib/index.js 是全部宿主逻辑,lib/client.js 是全部界面逻辑,没有压缩混淆。搜 sh( 能看到它执行的每一条命令;搜 sqSvc. 能看到它读会话的每一个调用点。
  2. 看审计面板。 陪聊窗口的设置里有「审计」一项,它列出插件往外发过的每一次模型调用:轮次、用了多少 token、缓存命中多少。缓存命中率那一栏是给「每次说话都从零开始」这个毛病准备的证据。
  3. 看运行卡片。 这个插件注册了一个给 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