🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

会话存档 chinacaa/dsh-session-save

DeepSeek 客户端
chinacaa1 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

把 DSH 会话存成一个 .dsproj 工程文件:像 Word 那样保存,像工程文件那样拷走。

31.5/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/chinacaa/dsh-session-save.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 6 处,展示前 6 条)
  • 发起网络请求
    client.js:442
    const response = await fetch(`${ROUTE}?${search.toString()}`, {
  • 发起网络请求
    client.js:731
    const response = await fetch(`${route}?${search.toString()}`, {
  • 写入剪贴板
    client.js:1685
    await navigator.clipboard.writeText(prompt);
  • 执行外部命令来自配置/变量
    dialog/linux.mjs:95
    execFile(
  • 执行外部命令固定命令
    dialog/macos.mjs:92
    execFile(
  • 执行外部命令来自配置/变量
    dialog/windows.mjs:97
    execFile(
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

把任意 DSH 会话存成单个 .dsproj 文件,像 Word 那样支持保存、另存为、自动保存,换台机器打开就能接着聊。

README

由 DeepSeek 最新模型翻译生成
dsh-session-save 把 DSH 会话存成一个 .dsproj 工程文件:像 Word 那样保存,像工程文件那样拷走。 换个 DSH、换台机器,打开这个文件就能接着上次的话题继续。 已上架 · listed on English · 中文 点「保存」弹出的原生另存为窗口 核心功能与使用场景 DSH 把每条会话都记在 ~/.dsh/sessions/ 里,session.v4.jsonl.zstd 是追加写的 zstd 帧。这样做很稳,但带来三个麻烦: - 想带走一条会话,得整目录搬。 ~/.dsh 里有会话、记忆、附件、索引、插件状态,几个 GB 起步,搬一次等于换整台机器。 - 想存档一条对话,只能手动拷文件。 拷完是个没有名字的 UUID 目录,过两周就想不起来它是哪次讨论。 - 两台机器轮流用,谁覆盖谁说不清。 直接同步 ~/.dsh 会让索引、状态、日志互相打架。 dsh-session-save 把这堆东西收成一个文件:一个会话 = 一个 .dsproj,名字是会话标题,里面是这条对话的全部历史加它所在工作区的记忆与引用附件。发给别人、丢进网盘、归档进项目仓库,都只是"一个文件"的事。
展开完整文档(README 剩余 1 节)
dsh-session-save

把 DSH 会话存成一个 .dsproj 工程文件:像 Word 那样保存,像工程文件那样拷走。
换个 DSH、换台机器,打开这个文件就能接着上次的话题继续。

已上架 · listed on

English · 中文

点「保存」弹出的原生另存为窗口

核心功能与使用场景

DSH 把每条会话都记在 ~/.dsh/sessions/ 里,session.v4.jsonl.zstd 是追加写的 zstd 帧。这样做很稳,但带来三个麻烦:

- 想带走一条会话,得整目录搬。 ~/.dsh 里有会话、记忆、附件、索引、插件状态,几个 GB 起步,搬一次等于换整台机器。
- 想存档一条对话,只能手动拷文件。 拷完是个没有名字的 UUID 目录,过两周就想不起来它是哪次讨论。
- 两台机器轮流用,谁覆盖谁说不清。 直接同步 ~/.dsh 会让索引、状态、日志互相打架。

dsh-session-save 把这堆东西收成一个文件:一个会话 = 一个 .dsproj,名字是会话标题,里面是这条对话的全部历史加它所在工作区的记忆与引用附件。发给别人、丢进网盘、归档进项目仓库,都只是"一个文件"的事。

装上后,标题栏右侧多出四个图标:

| 图标 | 动作 | 说明 |
| 📁 | 打开 | 选一个 .dsproj → 导入 → 自动切到那个会话 |
| 💾 | 保存 | 第一次弹「另存为」,之后每点一次覆盖同一个文件 |
| 🔄 | 自动保存 | 开着的话,每步结束自动写回那个文件 |
| 📄✏️ | 另存为 | 每次都弹窗口,可以换名字或换位置,原文件保留 |

空会话页面的新会话行右侧还会多一个「打开」胶囊按钮。刚进 DSH、还没有对话时,也能直接开一个工程文件。

标题栏图标

快速上手

1. 聊到一半点 💾(或者直接点 🔄 开自动保存,它会先替你存一次)。
2. 弹窗里确认文件名,默认是「会话标题 + 会话创建时间」,比如 把会话存成工程文件-20261025-1043.dsproj。
3. 之后每点一次 💾 就覆盖它;想留一份快照就点 📄✏️。

在另一台机器上:点 📁 选这个文件,会话会出现在列表里并自动切过去,接着上次的话继续说。

文件命名与保存保护

把会话存成工程文件-20261025-1043.dsproj └── 会话标题 ──┘ └─ 创建时间 ─┘

- 标题取日志里最新的 session/title(DSH 自己起的那个,通常是第一句话或 LLM 的概括)。
- 时间取会话创建时间,不是点保存的时间。同一个会话每次另存为都得到同一个名字,不会越存越乱。
- \ / : * ? " < > | 和控制字符换成空格,连续空白折叠,最长 60 字。
- 还没起标题的会话退回 20261025-1043-session-7f3a2c91.dsproj(时间 + id 前 8 位)。
- 目录用上次保存的目录,第一次是桌面。
- 覆盖保存不改名:记住的是完整路径,标题后来变了也不会突然冒出一个新文件。

保存时的存在性保护

保存的目标是「那个路径上的那个文件」,不是「那个路径」。所以每次写盘之前都会先看它还在不在:

- 手动保存发现不在了(被删、被移走、磁盘或网盘没挂上)→ 弹窗把原来那个路径摊给你看,三个选择:就在这里新建(原路径上重新起一份) / 另存到别处(换个位置,原路径不碰) / 取消(什么都不写)。默认什么都不写,不会在空位上放一个同名空壳。
- 自动保存发现不在了 → 自己关掉自动保存,并在界面告诉你原因(带原路径),这一轮不写盘。
- 目标文件夹整个没了(比如网盘没挂上)也一样:报「目标目录不存在」,不再悄悄 mkdir -p 把目录和文件一起造出来。

打开与导入流程

点「打开」之后,Host 侧按这个顺序判断(importPackage()):

flowchart TD
A[读 .dsproj] --> B{本机有这个会话 id 吗}
B -- 没有 --> C[问:打开到哪个工作区]
C --> D[导入,并把 header 的 cwd 改写成本机路径]
B -- 有 · createdAt 相同 · cwd 相同 --> E[直接切过去,不复制不写盘]
B -- 有 · createdAt 相同 · cwd 不同 --> F[就地再开一份,换新 id 落地]
B -- 有 · createdAt 不同 --> G[冲突面板:四选一]
D --> H[切到该会话]
E --> H
F --> H
G --> H

导入时的交互与落盘行为:

- 本机没有这个会话:弹窗询问「打开到哪个工作区」,确认后改写 header 里的工作区路径并切过去。打开成功后,这个 .dsproj 会被记成该会话的「保存位置」——接着点 💾 就是存回同一个文件。
- 本机已有且完全一致(createdAt 相同且 cwd 相同):直接切过去,不复制也不写盘。
- 同会话换工作区落地(createdAt 相同但 cwd 不同):换新 id 落地(relocate),就地再开一份。
- 会话冲突(createdAt 不同):弹出冲突面板,把两边并排列出来(标题、创建时间、体积、工作区 / 导出机器),四个按钮供选择:

| 按钮 | 做什么 |
| 取消 | 什么都不做 |
| 另存为副本 | 换新 id 落盘,两条历史都留着 |
| 用文件覆盖本机 | 先给本机那份留 .bak-<时间戳>,再写回同一目录 |
| 用本机的 | 只切到本机那份,不写盘 |

设计考量与实现细节

- id 必须全机唯一:所以「同一会话落到别的工作区」会换新 id(relocate),不会出现两份同 id 的会话,两条历史都留着。
- 落点不对时先自己想办法:打开时要把会话落到包里的工作区路径上(这个路径来自包本身,换台机器后经常已经不在)。分三种情况处理,都不需要你自己去资源管理器里折腾:①目录不存在 → 面板上直接给「在这里新建文件夹并继续」,点了才建;②那个位置被一个同名文件占着 → 给「把那个文件改名备份,在原处建文件夹」,文件改名成 <原名>.bak-<时间戳> 留着,一个字节都不删;③两种情况都不适用(相对路径、盘符根、没权限)→ 退回工作区面板,选一个已有的工作区,或「选其他目录…」指定。没有界面确认时(直接调接口)一律不建、不删、不写。
- 换工作区落地时只重写 header 那一帧里的 cwd,其余 zstd 帧原字节照搬。
- 记忆文件按 sha256 比对:相同跳过;不同则写成 xxx.from-dsproj-<日期>,从不覆盖本机已有记忆(详见后文记忆机制)。

多机同步与并发保护

这是最容易出事的地方,所以有三道防线。

第一道:文件里的锁。 每个包都带 LOCK.json:

{ "format": "dshproj-lock/1", "sessionId": "…", "host": "…", "pid": 1234, "at": "2026-10-25T09:00:00.000Z", "writes": 7 }

manifest.json 里镜像一份,SUMMARY.md 里写明「写入者 / 锁时间」。writes 是这份文件的累计写入次数。

第二道:系统里的侧车锁。 写盘瞬间用 <file>.lock 以 wx 独占创建;抢不到就读持有者,判断是否过期。同机 pid 还活着 → 挡;异机且 at 在 15 分钟内 → 挡;其余可写。被挡时先自动等一小会儿再判一轮(对方的写入通常这时已经写完了),仍然占着才返回 409 locked,消息里点名占用者,并给一个「仍然保存」按钮——锁只说明「可能有人在写」,要不要盖过去由你判断。

第三道:内容指纹(CAS)。 每次写完记下文件的 sha256;下次写之前重算,不一致说明这个文件已经被别处改过:

- 手动保存遇到 → 409 stale,弹「这个文件已被别处改过」,三个选择:覆盖磁盘上的 / 另存为新文件 / 取消。
- 自动保存遇到 → 自己关掉自动保存,这一轮不写盘。绝不用本机内容盖掉对方。
另外每次写盘都是原子写(先写 <file>.tmp-<pid>-<时间戳> 再 rename),同步盘不会抓到写了一半的包。路径落在 OneDrive / Dropbox / Google Drive / iCloud / 坚果云 / Box / Syncthing / Seafile / 百度网盘 时,界面会提示「两台机器同时开着自动保存会互相覆盖,建议只在一台机器上写」——只提示,不阻止。

一句话:真要两台机器轮流用同一份文件,只在一台机器上开自动保存;另一台手动保存时,会被内容比对拦下来问你。

包格式与离线工具

.dsproj 是 dshproj/1 格式的 ZIP,和同名的 dshproj 命令行工具双向兼容(同一个包格式,工具本体不在这个仓库里):

manifest.json 会话元数据 + 每个条目的 sha256 + 锁镜像 LOCK.json 谁写的、什么时候、写了几次 session/session.v4.jsonl.zstd 会话日志原字节(zstd 帧一个不动) memory/workspaces/<slug>/** 该工作区的项目记忆(笔记 / 交接 / 反思) attachments/objects/<前2>/<sha256> 会话引用到的图片(按内容哈希存,去重) attachments/files/<前2>/<sha256> 会话引用到的其它文件(同上) SUMMARY.md 人类可读摘要(ID / 工作区 / 帧数 / 行数 / 写入者)

不开 DSH 也能用同一个格式离线处理:

dshproj verify 会话.dsproj # 校验帧结构与条目 sha256 dshproj info 会话.dsproj # 看标题、帧数、记忆文件数 dshproj import 会话.dsproj --cwd D:\elsewhere # 导进另一个工作区(换新 id)

图片与附件打包机制

会话日志里只留 sha256: 引用,图片和文件的字节在 ~/.dsh/attachments/v1 下的独立对象库。两者不同步就会出「附件丢失」——而且重试不会自愈,因为引用指向的是一段固定哈希,重试还是同一段。所以打包时按内容哈希把会话真正引用到的对象一起塞进 attachments/,读出来先重算哈希,对不上宁可不带。

打开时按同样规则回填:目标已存在就跳过(内容寻址天然幂等,两个包带同一张图不会打架);条目名与哈希前缀不符、或内容哈希对不上,一律拒绝写入。本机本来就没有的对象记进 manifest.attachments.missing 并写进 SUMMARY.md——接包的人能提前知道「这个包天生缺几张图」,而不是打开时炸一脸。

不打包工作区里的代码文件——.dsproj 只带会话、记忆与附件。要把代码一起带走用 dshproj export --files。

安全防御与数据体检

打开别人的 .dsproj 等于让别人给你递一份压缩包,所以这个插件按「包可能是敌人」来做:

- 条目名穿越:包里的路径逐段校验,../../x、..\..\x、盘符、UNC 一律在读包阶段拒绝;记忆文件写入时每条落点重新拼一遍并复核仍在记忆目录内。
- 落盘文件名:manifest.json 里声明的会话文件名只接受 session[.tag].jsonl[.zstd] 形态并只取 basename,落点必须仍在 ~/.dsh/sessions 里,已存在就不覆盖。
- 解压炸弹:单条目上限 64 MB、整包上限 512 MB、压缩比上限 200×(非附件条目 4 MB 以下不算比例;attachments/ 是已压缩的二进制、合法包接近 1:1,所以 64 KB 起就要算,不给豁免),并且先校验中央目录里的长度再解压。会话日志本身是「多个 zstd 帧拼接」,所以每一帧解压后另有 64 MB 上限、整卷累计 512 MB——zip 那层只算「压缩数据解出来多少」,帧里再套一层就能绕过去(实测一个 29 KB 的包解出 900 MB)。
- 写去哪儿:保存只允许写到上次用的目录、已记住文件的目录,或原生对话框选的位置。写到别处、或者要覆盖一个不是本插件产出的文件,都要先确认。
- 路由入口:四条 /api 路由只接受回环地址(127.0.0.1 / localhost / ::1)、同源 Origin、非 cross-site 请求;sessionId 形态校验;失败信息不回显绝对路径。
- 包完整性:manifest.parts 里每条 sha256 与字节数逐条复核,不匹配就拒绝导入。反过来说,内容条目(session/、memory/workspaces/、attachments/)必须都在 manifest.parts 里声明过——没声明的一律拒绝,这样就没法只声明会话、偷偷夹带一份 MEMORY.md 塞进你的项目笔记。
- 附件回填:一次导入最多写 2000 个附件对象、合计 128 MB,超出部分不写,并在打开提示里告诉你少写了几个。附件的落点是 DSH 自己的对象库(~/.dsh/attachments/v1),那里没有回收机制,写进去就一直占着磁盘——所以这条额度比整包上限更紧。

打开前体检与自动修复

包可能是从别的机器来的,中间也可能有文件被删过。所以导入落盘之前先扫一遍,能修的直接修好再存:

- 图片的字节本机没有 → 那个位置换成一行可读的文字([图片已丢失:page-2.png])。留着一个打不开的引用只会让你打开时看到一排 Attachment object is missing.,还重试不好。
- 图片在,但缺宽高/字节数 → 按图片文件头(PNG / GIF / WebP / JPEG)把尺寸补齐。
- 某一行日志解析不了 → 丢掉那一行,同一条记录里的其余事件照常留下。
- 某一帧解压不开 → 原样保留它的字节,只记一笔,不替你丢数据。

改完之后界面会逐条告诉你动了什么(缺几张图、补了几个尺寸、丢了几行)。只重压真的改过的那一帧,没动的帧逐字节不变——所以体检过的包和原来的包是同一份会话,不是重新生成的。想只看不改,可以调 repairSession(buf, { keepMissing: true })。

包坏了先试着救,救不了给一段能直接交给 AI 的说明

.dsproj 是别人打包发来的,传输、网盘同步、手动改动都可能让它坏掉一层。校验器一发现不对就整包拒绝,用户拿到的只有一句「包校验失败」——既不知道坏在哪,也没法自己修。

现在分两步:

1. 先试宽松模式(点「丢掉坏条目后打开」,接口上是 ?repairPackage=1)。校验器不再整包拒绝,而是逐条判:哈希对不上、体积对不上、没在 manifest.parts 里声明、条目名不在白名单——这四类条目就地摘掉,其余照常导入,并告诉你摘掉了哪几条、为什么。只要 session/ 那条还在,对话历史就能救回来;记忆和图片坏了只是少几张。
2. 救不了就给说明(返回 package-broken)。界面上弹一个只读文本框,里面是一整段可以直接复制的说明:包在哪、多大、插件版本、校验器的报错原文、包内条目清单(前 60 条)、包格式约定、以及五步修法(重算 sha256 → 修 manifest.parts → 删坏条目 → 重新打包 → 说明改了哪些)。拿着这段去问任何 AI,它都能照着修——不用你自己读懂 zip 结构。

两条边界:session/ 那条要是坏了或被摘掉,就没有可救的了,只会给说明不会导入;「这个包来自更新版本的插件」是另一种情况(见下一节),不会被当成坏包硬修。

版本感知与升级提示

包里有这一版插件不认识的条目时,以前只能回一句「包里有不认识的条目,拒绝打开」。那个提示把人往错的方向推——你会以为文件坏了,去翻备份、重存一份,而真实原因只是插件旧了。

现在包会记下写它的插件版本(manifest.plugin),打开时先比一比:

- 包比你的插件新 → 直接说清楚「写它的是哪个版本、你本机是哪个版本、先升级再打开」,并且你一定知道包本身没坏。
- 同一个版本却带了陌生条目 → 仍按「不认识的条目」拒绝。这种情况多半是包被别的工具拼过,不是版本问题,不能拿「你旧了」糊过去。
- 老包没写版本 → 按条目前缀反推(attachments/ 是 0.3.0 起才有的),照样能给出「该升级了」而不是「文件坏了」。

提示里不会出现你机器上的路径。

记忆保护:默认不带;带了默认落副本,合并才覆盖

- 默认不打包记忆。~/.dsh/memory 里有你的项目笔记、日志和白板,默认一个字节都不进包。要连记忆一起带走,按住 Alt 点保存/另存为,或请求里带 ?memory=1。
- 包里带着记忆、在别的机器上打开,记忆默认只落成 xxx.from-dsproj-<日期> 副本,DSH 不会把它当记忆读。打开后界面会报「几个文件落了副本 / 几个和本机重名」,并给一块带「合并记忆」「打开记忆目录」的提示面板。
- 「合并记忆」(?memory=restore)只覆盖本机已存在、且内容不同的文件,覆盖前备份成 xxx.bak-<时间>;本机没有的一律一个都不写,只在弹窗「将新建」段里列给你看。
- 要把记忆铺到一台全新机器,得在同一个弹窗里额外点「合并并新建」(?memory=new),这时才允许落原名。
- 落点只认「路径逐段相同」。 记忆目录按工作区路径算出来(projectKey),查已有目录时只比两级精确候选,不做大小写 / - / 尾分隔符的归一化模糊匹配——...\ws 和 ...\ws\ 是两个工作区,各自的记忆各归各的,防止模糊匹配导致 restore 顺着路径覆盖别人的 MEMORY.md。合并弹窗里会直接列出这次要落的 memoryDir,并写明「落点由包里的 cwd 决定,不是你当前打开的工作区」。
- 为什么这么保守:parts 里的 sha256 是打包方自己算的,把投毒的 MEMORY.md 也登记进去、哈希算对,前面所有校验都过得去——自洽不等于可信。所以「会不会被 DSH 当记忆读」只能靠改落点名来断。于是投毒最多落在两种位置:你本机本来就有、且备份看得见的那份,或者你亲手点过「合并并新建」的新文件。

安装与跨平台支持

从 npm 装
dsh plugin --profile web add dsh-session-save

或直接从 GitHub 装
dsh plugin --profile web add github:chinacaa/dsh-session-save

装完重启 DSH:插件在启动时组合,热装不会生效。

跨平台原生窗口

Host 侧逻辑与平台无关;另存为/打开窗口按平台挑实现:
- Windows:走 powershell.exe 的 WinForms 面板。
- macOS:走系统自带的 osascript 的 Finder 面板(choose file name / choose file)。
- Linux / BSD:按顺序探测 zenity → kdialog → qarma → matedialog,使用找到的第一个的 GTK / KDE 面板。

三者都没有时(无头服务器、纯远程会话、精简镜像)自动退回页面内的路径输入框,功能完整不丢。

手动安装(开发时用)

1) 把仓库链接进 profile New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-session-save" -Target "<repo>"
2) profile 的 package.json → dependencies 加 "dsh-session-save": "link:<repo>"
3) profile 的 package.json → dsh.profile.bundles 追加 "dsh-session-save"
4) 重启 DSH

别只往 profile 的 cordis.patch.yml 追加 - id/name 行。那份文件只用于覆盖已有 id,新增插件走 dsh.bundle.patch + dsh.profile.bundles。

常见问题

保存的是我这句话吗? 保存的是此刻已落盘的内容;正在生成的这一步可能还差最后一帧,等它结束再点一次就齐了。

文件被我删了,再点 💾 会怎样? 会停下来问你,不会静默新建。详见上文「保存时的存在性保护」,弹窗会提供就在这里新建 / 另存到别处 / 取消三选一;自动保存发现文件丢失会自动关闭并告警。
自动保存会不会很占磁盘? 每步都整包重写(会话日志是追加写,但包要重新打包),1.5 秒去抖把频率压到每步最多一次。会话特别大时建议只手动保存。

能在 Linux / macOS 上用吗? 能。Host 侧逻辑与平台无关,支持 WinForms、Finder、zenity/kdialog 等原生面板,无图形环境退回 Web 输入框(详见「跨平台原生窗口」一节)。

会覆盖我的项目记忆吗? 默认连记忆都不打包;带记忆打开时默认落 xxx.from-dsproj-<日期> 副本;点「合并记忆」才会覆盖本机已有文件(覆盖前留 .bak-<时间>),点「合并并新建」才在本机没有的地方落原名。

打开后为什么换了 id? 因为 id 必须在一个 DSH_HOME 内唯一:同一个会话换工作区落地会重新发一个 id,两条历史都在。

能只导出不导入吗? 能。.dsproj 就是 ZIP,dshproj verify/info 都能离线读。

兼容与版本

| 项 | 版本线 | 什么时候动 |
| 包版本 | package.json 的 version(SemVer) | 0.x 时 MINOR 可破坏,PATCH 只修 bug |
| 包格式 | dshproj/1(index.js 的 FORMAT) | 包内布局变了才动,要照顾 dshproj 命令行 |
| 锁格式 | dshproj-lock/1(LOCK_FORMAT) | LOCK.json 字段变了才动 |
| 宿主范围 | engines.dsh、peer @deepseek-ai/cordis | 用了新 API 或官方包抬了下界 |

各版本对外声明过的宿主范围(npm view dsh-session-save@<版本> engines --registry=https://registry.npmjs.org 可复核):

| 插件版本 | engines.dsh | 说明 |
| 0.2.3 | >=0.2.1-rc.1 | 没有上界 |
| 0.3.0 | >=0.2.1-rc.1 | 没有上界;起包内带附件对象 |
| 0.4.0 | >=0.2.1-rc.1 <0.3.0-0 | 补上界 |
| 0.4.1 | >=0.2.1-rc.1 <0.3.0-0 | 同上 |

下界四个版本一致,因为 0.3.0 起依赖的附件对象库布局(attachments/v1 与 objects/file-objects 分片)在 @deepseek-ai/dsh-attachment-local@0.2.1-rc.1 里就已经存在(实测该包内命中 10 处),没有把下界推高。上界是 0.4.0 才补的:更早的版本在 dsh 0.3.x 上不会被 engines 挡住,装了也不报错,但那些接口还在不在没人验过。在 dsh 0.3.0 及以上请用 0.4.x。

改版本用脚本,别手改(它会同步 CHANGELOG.md):

node scripts/bump.mjs patch # 修 bug
node scripts/bump.mjs minor # 新功能
node scripts/bump.mjs major # 破坏性

改完提交、打 tag、推 tag,发布就全自动了:打 tag 之后 publish.yml 会做完整条链路:

git commit -am "发布 vX" && git tag -a vX -m "vX"
git push origin main && git push origin vX

发布到 npm(带 provenance)→ 从 registry 取回那份 tarball 核对 sha1 → 用 CHANGELOG.md 里这一版的小节自动建 Release → 把 tarball 传成 Release 附件。不用手工建 Release,也不用另写一份发布说明:发版说明的真源只有 CHANGELOG.md,node scripts/release-notes.mjs <版本> 负责把它摘出来。

重跑同一个 tag 是安全的:版本已在 npm 上就跳过发布、Release 已存在就跳过创建、附件直接覆盖。所以「上次附件没传上」这种事故,重跑一次就补齐了。

界面语言

界面文案不在代码里,在 locales/:

locales/
├── zh-CN.json 源语言,114 个键,新文案先加这里 └── en-US.json

启动时插件把语言包发给前端(/api/session.locales),前端按浏览器的 navigator.language 挑一份,挑不到就用中文。client.js 里仍留着一份中文表做首屏兜底——语言包是异步拉的,等它回来再渲染等于白屏。

想加一门语言:复制 zh-CN.json 改名成 xx-XX.json,把值翻掉,跑一遍

node scripts/check-locales.mjs

它会告诉你少了哪些键、哪个占位符对不上。占位符({n}、{path}、{name})必须原样留着,翻译时改掉就会显示成 undefined。细则见 locales/README.md。

开发与测试

零运行时依赖,纯 ESM,只用 node: 内置模块;客户端只依赖 react。

node --check index.js client.js # 语法
node test/client.mjs # 客户端接线:五个插槽项、图标指纹、点击打到的路由(含英文词表)
node test/smoke.mjs # 打包冒烟:夹具会话 → 打包 → 回读 ZIP → 二次保存复用路径
node test/open.mjs # 打开链路:复用 / 换 cwd 落地 / 问工作区 / 逐帧比对
node test/conflict.mjs # 锁、四种冲突、CAS、自动保存(约 30 秒)
node test/dialog.mjs # 三平台原生窗口:选择器判据 + 参数拼装 + 中文路径往返
node test/security.mjs # 安全回归:条目名穿越 / 落点文件名 / 解压炸弹 / 路由闸 / 包校验
node test/pathkey.mjs # 路径键:与 DSH 官方的目录名算法逐字比对(升级 DSH 后第一件事)
改客户端后想验英文词表:DSH_TEST_LOCALE=en-US node test/client.mjs。

十一套测试全部跑在自己的临时 DSH_HOME(test/fixture.mjs 在 os.tmpdir() 下建的夹具目录)里,跑完自动删掉,不碰真实的 ~/.dsh,也不要求本机「恰好有会话」。

三套例外:test/client.mjs 是纯内存接线测试,不建夹具目录也不读写磁盘;test/pathkey.mjs 只调两个纯函数,同样不落盘。test/dialog.mjs 里「选择器判据 / AppleScript 与 zenity 参数拼装 / 脚本 BOM」这几组是纯静态校验、跨平台可跑,只有真拉 powershell.exe 验中文编码的几条在非 Windows 上跳过(会打印 ⏭ 跳过,不算通过)。其余八套跨平台。

想改代码先读 AGENTS.md:红线、四条路由的契约、importPackage 的分支顺序、测试怎么写,都在那里;CLAUDE.md 是最短入口,docs/TEST-MATRIX.md 是功能面与测试的对照表。

如何贡献

报一个能修的问题

带上这五样,否则大概率只能来回问:

1. 插件版本与 DSH 版本(node -p "require('dsh-session-save/package.json').version");
2. 平台(Windows / macOS / Linux),以及能不能弹出系统自带的另存为窗口。这决定走的是原生对话框,还是页面里的路径输入框;
3. 复现步骤,从点哪个按钮开始;
4. 报错原文:界面上的提示,或控制台里 [dsh-session-save] 开头那一行,原样贴,别转述;
5. 如果是包打不开:这个 .dsproj 换台机器、换个盘符是否同样打不开。

提一个 PR

git clone https://github.com/chinacaa/dsh-session-save
cd dsh-session-save
node test/pathkey.mjs # 先确认基线是绿的,十二套逐个跑见 AGENTS.md

- 零运行时依赖是硬约束:不要往 dependencies 里加东西,客户端只许 require('react')。
- 改了行为 → 在 CHANGELOG.md 的 [Unreleased] 加一条;改了断言或功能点 → 同步 docs/TEST-MATRIX.md。
- 改了文案 → 中文 README、英文 README、locales/*.json 一起改,node scripts/check-locales.mjs 会查漏。
- commit message 用中文,一句话说清改了什么;一个 PR 只做一件事。
- 提交前跑 node --check index.js client.js 和全部十二套测试。红色的 PR 不看。

加一门语言

界面文案全在 locales/,加一份 <语言标签>.json 就行,不用改代码:

1. 复制 locales/zh-CN.json 成 locales/ja-JP.json(标签按 BCP 47 写,中文请用 zh-TW 这种带地区的);
2. 逐条翻译,占位符 {n}、{path} 原样保留,键一个不多、一个不少;
3. 跑 node scripts/check-locales.mjs,它会指出少翻哪几条、哪个占位符对不上;
4. 提 PR。少翻几条也能合,缺的部分自动回落到中文。

细节见 locales/README.md。

不用写的

- 不需要签 CLA,也不用先开 issue 打招呼。
- 不要动 package.json 的 version:发版由维护者打 tag 触发。
- 不用提「换成 TypeScript」「加一条构建链」「引入某个 UI 库」这类 PR。零依赖、无构建是刻意的:插件要在 DSH 启动时直接组合,装完重启就能用。

已知边界

- 打开只认本插件与 dshproj 产出的 dshproj/1 包;包里没有 session/ 条目会直接报错,不会造一个空会话。
- 切会话依赖 uiWorkspace.openSession;某个前端没有这个服务时(例如纯 TUI),会话仍会被导入并出现在列表里,只是不会自动跳转。
- 跨机锁只按 15 分钟时间窗判断,不是实时心跳:另一台机器刚写完但没来得及清锁,最多挡 15 分钟。
- 单机多实例共用一个 DSH_HOME 时,storages/session-save.json 会被后写的覆盖,但每个会话记住的路径仍各自独立。
- 「打开到哪个工作区」的列表来自 storages/workspace.json;全新机器上该文件不存在,列表为空,只能走「选其他目录…」。
- 空会话页面的「打开」按钮只出现在还没有消息的新会话上;进入对话后要打开别的工程,用标题栏那个 📁。
- 默认不打包工作区记忆;带了记忆的包打开后默认落副本,要用户显式点「合并记忆」/「合并并新建」才会被 DSH 读到(见上一节)。
- 只在回环地址上工作:通过反向代理或另一台机器的浏览器访问本机 DSH 时,四条会话路由会被 403 挡掉(这是有意的)。

许可

MIT © 2026 chinacaa · 见 LICENSE
预览分享卡片长什么样 ↓chinacaa/dsh-session-save
分享 · DeepSeek Harness Hub
chinacaa/dsh-session-save

把 DSH 会话存成一个 .dsproj 工程文件:像 Word 那样保存,像工程文件那样拷走。

这是你的仓库?认领条目

回到顶部