# 更新日志 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 ## [2.2.2] - 2026-10-25 **修复版本。让「端口被占用」真正能自助解决——之前重试按钮点了只会原地转圈。** ### 修复 - **端口被占用后无法恢复。** 上一版的重试按钮本身没坏,它每次都在真的重新 绑定端口;但占用者若是一个**还活着的残留宿主进程**,它永远不会自己让出端口, 于是重试多少次都是同一个 `EADDRINUSE`,用户只能去重启 DSH 甚至手动找进程。 典型场景:上一次 DSH 退出时宿主进程没被收干净,父进程变成 `launchd`(`ppid=1`), 但它持有的监听 socket 仍然有效,继续占着 `45871` 和 `45872`。此时新开的 DSH 里的插件就永远绑不上——而且它和残留进程读写的是同一份 `identity.json`, 所以两个实例连 `peerId` 都一样,从界面完全看不出「其实有两个我在跑」。 现在会把占用者查出来显示在提示里(PID + 已运行时长 + 判定原因),并在确认 对方是残留宿主时提供「**强制释放**」按钮:`SIGTERM` 优雅退出,3 秒不退再 `SIGKILL`,随后立即重新绑定,最多再重试 2 秒等内核回收 socket。 - **误杀防护。** 结束进程是危险操作,所以只在三条判据**同时成立**时才动手: 1. 命令行里带 DSH 宿主入口(`dsh-desktop-host` / `DeepSeek Harness`); 2. 父进程是 `1`(原来的 DSH 已经没了,它已被 launchd 收养); 3. 不是当前进程本身,也不在当前进程的祖先链上(沿 `ppid` 上溯 32 层核对)。 任何一条不满足就返回 `409` 并说明原因,绝不结束别人的程序。占用者是别的软件时, 按钮根本不出现,提示仍然只建议改端口或手动关闭。 ### 新增 - `GET /lan-chat/state` 增加 `holder` 字段:`{pid, command, orphan, reason, ppid, elapsed}`, 查不到占用者时为 `null`。诊断结果按「端口 + 启动错误」缓存,不会每次轮询都起子进程。 - `POST /lan-chat/listen/release`:强制释放端口并重新监听。已监听时幂等返回 `alreadyListening`;占用者不可安全结束时返回 `409`。 - `scripts/hosttest.mjs` 新增 14 项(97 → 111):占用者查找、自身保护、空端口、PID 复用复核、 非法端口号、非残留进程时拒绝释放、`GET` 拒绝等。 ### 升级说明 - 协议 `version` 仍是 **2**,无需局域网内其他机器同步升级。 - 建议升级后重启一次 DSH,让新版宿主接管端口。 ## [2.2.1] - 2026-10-25 **修复版本。修掉「新版本文字颜色偏灰」——深色主题下整块面板糊成灰白一片。** ### 修复 - **深色主题下配色失效。** 面板一直把 `--dsw-alias-bg-l1` / `--dsw-alias-bg-l2` 当背景色,但 **DSH 主题里并没有这两个变量**(真正存在的是 `--dsw-alias-bg-base` 和 `--dsw-alias-bg-layer-1/2/3`)。`var()` 解析不到就静默回退到写死的 `#fff`, 而文字色走的是真实存在的 `--dsw-alias-label-primary`,深色主题下取值是 `#f9fafb`。 结果就是**白底配近白字**,对比度只有 1.05:1,整块面板灰成一片、几乎读不出来。 这个缺陷从 1.0.0 就存在,但之前没暴露:浅色主题下兜底的 `#fff` 恰好等于正确值, 看不出来;只有切到深色主题才会现形。 改为全部使用 DSH 真实存在的主题变量: | 用途 | 原来(失效) | 现在 | | --- | --- | --- | | 面板 / 侧栏底色 | `--dsw-alias-bg-l1` ❌ | `--dsw-alias-bg-base` | | 弹窗底板 | `--dsw-alias-bg-l2` ❌ | `--dsw-alias-bg-layer-2` | | 气泡 / 进度条底色 | `--dsw-alias-bg-l2` ❌ | `--dsw-alias-bg-skeleton` | | 强调块上的文字 | 写死 `#fff` | `--dsw-alias-label-primary-foreground` | | 危险色 / 成功色 | 写死 `#e5484d` / `#2ea043` | `--dsw-alias-state-error-primary` / `state-success-primary` | 顺带把 13 处写死的颜色(危险色半透明底、遮罩、弹窗阴影、成功提示等)改成由主题 变量派生,深色主题下不再出现「亮红底配白字」这类发虚的组合。 ### 新增 - **`scripts/themetest.mjs`(65 项)主题配色回归测试**,接进 `npm test`: - 把插件引用的每个 `--dsw-alias-*` 拿去和 DSH 主题比对,**名字写错立刻失败**。 `var()` 引用不存在的变量不会报错,只会静默用兜底值——这类 bug 功能测试全绿也 发现不了,只能靠静态校验兜住。 - 用 DSH 真实的明 / 暗两套取值算 WCAG 对比度,挡住「能解析但看不清」。 - 断言明暗两套底色真的不同,直接对应这次「深色下还是白底」的原始缺陷。 - 实测有效:把修复前的版本喂给它,失败 16 项。 ### 升级说明 - 协议仍是 **v2**,与 2.2.0 完全兼容,无需和局域网内其他机器同步升级。 - 纯界面改动,不涉及数据格式,升级后不需要迁移。 ## [2.2.0] - 2026-10-25 **功能版本。补上「删除群聊」——之前群里只能退(`/groups/leave`), 界面上连退出的入口都没有,建过的群永远留在会话列表里。** ### 新增 - **删除群聊**,按身份分成两种语义: - **群主删除 = 解散**:本机连群带消息一起删掉,并通知所有成员各自清掉。 对方那边也看不到了,确认框里写明「此操作不可撤销」。 - **非群主删除 = 只删本机这份**:相当于退群 + 清本地记录,不会解散别人的群。 确认框里写明「群主的群和别人的记录都不受影响」。 入口有两处:会话标题栏的关闭图标、会话列表行悬停时右侧出现的关闭图标。 删除会同时清掉该群的本地消息与未读标记,不留红点。 - **`POST /lan-chat/groups/remove`**:请求体 `{groupId}`,响应 `{ok:true, dissolved:boolean}`。群不存在返回 404。 - **协议层新增 `group-removed` 消息**:群主解散时推给所有成员(在线走 WS, 离线走 HTTP `POST /lan-chat/v1/group-removed`),接收方把群和消息一起清掉。 与 `group` 一样校验 `createdBy`,非群主发来的解散请求一律 403,防止有人 冒充群主解散别人的群。 ### 修复 - **WS 推送的群资料被静默丢弃**:`broadcastGroup` 会 `pushTo(member, {type:'group'})`, 但接收端 `_onSocketText` 里**没有 `group` 分支**,帧到达后直接落到函数末尾丢掉。 也就是说走 WS 的成员永远同步不到群资料,只有走 HTTP 的离线成员能收到。 已补上 `group` 与 `group-removed` 两个分支,约束与 HTTP 路径一致。 - **删掉的群会被推送复活**:本机删掉群之后,群主只要改一次群名(或改成员), `mergeGroup` 就会把这个群重新建回来——「删除」形同虚设。现在删群会在 `state.json` 里记一条 `deletedGroups` 黑名单(最多保留 500 条), `mergeGroup` 遇到黑名单里的 id 直接返回 null,重启后依然有效。 ### 升级说明 - 协议版本仍是 **2**。新增的 `group-removed` 是**新增消息类型**,不是破坏性变更: 旧版本收到不认识的类型会忽略,表现得和现在一样(群留原样),不会报错或崩。 因此新旧版本可以混跑,建议两端都升到 2.2.0 才能双向同步解散。 - `state.json` 会自动补上 `deletedGroups: []`,无需手工迁移。 - **需重启 DSH 才能加载新代码。** --- ## [2.1.1] - 2026-10-25 **修复版本。修掉全局 CSS 命名冲突,以及桌面客户端上的两个阻断性问题。** ### 修复 - **桌面客户端改不了昵称(所有写操作都报「已拒绝非同源请求」)**: DSH 桌面客户端不是从 `http://127.0.0.1:` 加载页面的,而是走自定义协议 `dsh-app://app`,所有请求由 Electron 主进程的 `forwardWebRequest()` 转发—— 它会把 `host` / `origin` / `cookie` / `sec-fetch-site` 四个请求头**主动删掉** 再重发,而 Node 的 `fetch` 不会补 `Origin`。于是这些写请求到达插件时「没有 Origin」, 正好撞上 2.0.0 引入的 F-09 规则(写操作必须带 Origin 才算同源),全部 403。 浏览器网页端因为是真正的同源 `fetch`,一直正常——所以这个 bug 只在桌面端出现。 现在写操作额外接受 `x-lan-chat-client: desktop` 标识头。**这不是放宽 CSRF 防护**: 跨站页面要带自定义头必须先过 CORS 预检,而本插件从不回 CORS 响应头、预检本身 也会被同源检查拒掉,浏览器根本发不出去;跨站表单则无法自定义请求头。 Host 白名单(挡 DNS rebinding)、`Sec-Fetch-Site`、跨站 Origin 三条检查一条没动。 - **局域网端口被占用后永久失效(`listen EADDRINUSE`)**:以前绑定失败只记一条 `startError` 就再也不试,必须重启 DSH 才能恢复。而端口占用常常是暂时的—— 旧宿主进程残留、DSH 刚退出时 socket 还没释放、或用户重启过快。 现在绑定失败后按 2s→60s 指数退避**自动重试**,恢复后通过 SSE 通知界面; 提示条也改成人话(「端口被占用」而不是裸的 `EADDRINUSE`),并带一个 「重试」按钮可立即重试。重新监听不会再重复注册路由或重复绑定 UDP 端口。 - **全局 CSS 前缀与 dsh-context 冲突**:本插件原来所有样式类和 CSS 变量都用 `lc-` 前缀, 而 dsh-context 用了一整套 `lc-*` 类名(790 个,其中 `.lc-root` 是它每个卡片的 根节点)。两个插件同时安装时,本插件的 `.lc-root{display:flex;height:100%;overflow:hidden;padding:0}` 和 `.lc-root *{box-sizing:border-box}` 会把 dsh-context 的根容器变成横向 flex 容器, 它的卡片被压成几十像素宽的竖条,文字逐字折行——**整个「上下文」「编队」页不可读**。 反过来 dsh-context 的 `.lc-root{padding:16px 20px 32px;overflow-y:auto}` 和 `.lc-empty` 也会把本插件的聊天面板挤出不该有的内边距。 现在全部 77 个类名、DOM id 和 11 个 CSS 变量统一改为 `lanchat-` / `--lanchat-` 前缀。 ### 升级说明 - 协议版本仍是 **2**,与 2.0.0 / 2.1.0 **完全互通**,可以分批升级。 - 无需迁移任何数据;如你自定义过 CSS 覆盖 `.lc-*`,需要改成 `.lanchat-*`。 - **桌面客户端必须重启 DSH 才能加载本次修复**(2.0.0/2.1.0 的写操作在桌面端一律 403)。 --- ## [2.1.0] - 2026-10-25 **功能版本。补齐三个使用上的硬缺口:联系人备注、可配文件上限、消息删除。** ### 新增 - **联系人备注**(`POST /lan-chat/contacts/alias`):可以给任何联系人起本地备注名, 会话列表和联系人列表都优先显示备注,括号里附带对方真实昵称。 备注只存在本机,**不会推送给对方**,也不影响对方自己的昵称设置。 - **单文件上限可配**(设置 → 「单文件上限(MB)」):范围 64MB ~ 64GB,默认 512MB。 上限会被应用到浏览器侧上传、节点间文件回源下载两处,发送框右下角的提示实时跟随。 文件总量配额同时支持 `maxTotalBytes` 配置(默认 2 GiB),暂未做进界面。 - **删除消息**(`POST /lan-chat/messages/delete`):单条消息悬停出现删除图标, 会话标题栏新增「清空本会话记录」。两种删除都**只删本机 JSONL 日志**, 对方那边的副本不受影响——这是刻意的,本插件没有撤回机制,伪造撤回只会误导使用者。 删除后未读标记会跟着收敛,不会留下点不掉的红点。 ### 修复 - **改名后对方看不到**(`_tokenFor`):`/hello` 的响应里其实一直带着对方的最新昵称, 但接收方只取了 token、把昵称丢了。现在握手时会把昵称(和端口)写回联系人记录, 所以即使这次是对方主动来握手,也能拿到它的新名字。 - **设置保存多推一次资料**:之前无论改什么设置都会触发一次昵称广播, 现在只有昵称真的变了才广播。 ### 测试 四个测试套件从 170 项增加到 **189 项**: | 套件 | 之前 | 现在 | 新增覆盖 | | --- | --- | --- | --- | | `selftest.mjs` | 32 | 34 | 握手回填最新昵称 | | `hosttest.mjs` | 56 | 73 | 备注增删查、上限夹取与非法值拒绝、单条删除、会话清空、新接口跨站拦截 | | `e2etest.mjs` | 35 | 35 | 无变化 | | `sectest.mjs` | 47 | 47 | 无变化 | ### 升级说明 - 协议版本仍是 **2**,与 2.0.0 **完全互通**,局域网内可以分批升级。 - `state.json` 会自动补上 `maxFileBytes` / `maxTotalBytes` 两个字段,旧数据无需迁移。 ## [2.0.0] - 2026-10-25 **破坏性安全版本。修复 2026-10-25 渗透测试发现全部 22 项问题。** 协议版本从 1 升到 2,**新旧版本无法互通,局域网内所有机器必须一起升级。** ### 安全修复 #### 网络层(局域网端口) - **鉴权**(F-01/F-02/F-06/F-16):除 `/info` 和 `/hello` 外的 7 个接口 (`/send` `/sync` `/group` `/profile` `/upload` `/file` `/ws`)全部要求 token, 否则 401。此前这 7 个接口完全裸奔。 - **身份密钥**(F-02/F-22):启用 `identity.secret` 作为节点身份密钥,首次握手时 对端记录(TOFU),此后同一 peerId 必须出示同一把密钥,否则 403。防止顶替已知节点。 - **发送者伪造**(F-04):`/send` 用 token 持有者的 peerId 覆盖 `from` 字段。 - **消息广播**(F-03):删除 `broadcast()`,消息只投递给会话参与者。此前私聊内容 会被推给所有已连接节点。 - **SSRF**(F-11/F-15):出站只连 RFC1918 私网,拒绝公网、`127.0.0.0/8`、 `169.254.0.0/16`(云元数据)。`/probe`、`/scan` 同样受限,地址数上限 1024。 - **入站来源过滤**(F-13):来自公网或链路本地地址的请求直接 403。 默认仍监听 `0.0.0.0` 以便局域网使用,但不再对公网来源提供服务。 - **WS 资源上限**(F-17):分片累计上限(超限 `close(1009)`)、单连接 256 条、 60 帧/秒、单帧 8 MiB。 - **群成员注入**(F-16):只有群主能修改群,`createdBy` 强制改写为 token 持有者。 - **mime 净化**(F-21):`safeMime()` 校验 `Content-Type`,非法回落 `application/octet-stream`。 - **信息泄露**(F-19/F-20):`/info` 不再返回主机名;`/diag` 只返回目录名。 #### 浏览器层(`/lan-chat/*`) - **同源检查**(F-08/F-09/F-10):全部 21 条路由统一过 `isTrusted`。此前只有 11 条 受保护,且缺少 `Origin` 时直接放行。现在要求 Host ∈ 回环白名单、 `Sec-Fetch-Site` ∈ `same-origin`/`none`/空、带 Origin 时必须与 Host 一致; **写操作缺 Origin 一律拒绝**,堵住表单型 CSRF 与 DNS rebinding。 - **访问码泄露**(F-07):`/state`、`/settings` 不再回显明文 `accessCode`, 改为 `hasAccessCode: boolean`。前端相应改为「留空表示不修改」+ 显式清除勾选。 - **SSE**(F-12):`/events` 接入同源检查,加并发上限(32)与空闲超时(30 分钟)。 #### 存储层 - **文件配额与回收**(F-18):上传总配额 2 GiB,7 天 GC,被消息引用的文件不回收。 - **死代码**(F-22):`autoAccept` / `autoDiscover` 从「只写不读」变为真实生效。 ### 新增 - `scripts/sectest.mjs`:把渗透测试用例改写成环境无关的回归测试(47 项断言), 覆盖上述全部发现。CI 已接入。 - `npm test` 一键跑四套测试。 ### 变更 - 版本号 1.0.0 → 2.0.0;协议版本 1 → 2。 - 测试基线 109 项 → 170 项(selftest 32 + hosttest 56 + e2etest 35 + sectest 47)。 - `README.md` 安全说明按修复后行为重写。 ### 已知限制 - **不加密传输**(F-14):局域网内可被同网段的人抓包。不要用它传敏感凭据。 未做 TLS 是刻意的取舍——零依赖、免证书配置;如有需要可后续加端到端加密。 - 访问码只作为「入场券」,真正防越权的是 token。 ## [1.0.0] - 2026-10-25 首个公开版本。局域网聊天:单聊 / 群聊 / 文件传输 / 网段扫描加好友。