dsh-session-folders
romantic123fly/dsh-session-folders
为工作区里的每个新对话在对应目录下创建以对话标题命名的文件夹,把该对话的工作目录切到其中;侧边栏会话菜单新增删除入口,文件夹内仍有文件时会先询问是否一并删除。它与 DieJob/dsh-session-folders 使用同一个 npm 包名、功能相近,两者只启用一个;它的 HTTP 路由没有 DSH 的访问检查。
安装
dsh plugin --profile web add github:romantic123fly/dsh-session-foldersREADME
会话文件夹(dsh-session-folders)
在工作区里新建会话时,自动在该工作区路径下创建一个专属文件夹,并把新会话的工作目录切换到该文件夹。 会话在侧边栏里仍然归属原来的工作区,但它读写文件、执行命令、访问相对路径时都只发生在这个文件夹里 —— 相当于每个对话一个文件夹。
行为
- 你在左侧新建工作区并指定路径(例如
E:\项目资料)。 - 在这个工作区下新建会话时,插件监听会话创建事件(
session/created),确认该会话的原始目录正好等于某个已注册工作区的路径。 - 在
E:\项目资料下创建文件夹,默认就用会话标题命名,例如体检报告分析与健康趋势。- 创建的那一刻标题还没生成,所以先建成会话 id 的短名(例如
9f3a1c2b);标题一落库就原地改名为标题,会话的工作目录跟着改。 - 之后你在界面上重命名会话,文件夹也会跟着改(同一次运行内)。
- 创建的那一刻标题还没生成,所以先建成会话 id 的短名(例如
- 插件向会话写入标准事件
working-directory/change,把该会话的有效工作目录改为这个文件夹。- 会话头部(header)里记录的仍是工作区路径,所以分组不会被破坏,会话不会掉到"未分组"。
- 这个变更发生在模型第一轮之前,系统提示里的
Current working directory直接就是新文件夹。 - 直接写事件、不经过
workingDirectory.set(),因此不会在对话里插入一条"工作目录已更改"的可见提示。 - 另外每轮开始前(
agent/pre-step)还会再校验一次:即使创建那一刻没建成(例如工作区注册表还没就绪),也会在第一轮工具执行前把文件夹补上。
- 该会话之后创建的每个文件、每次
cd的起点都在这个文件夹内;换一个会话就是另一个文件夹,互不干扰。
命名
默认模板 {title}(只用会话标题),支持以下占位符:
| 占位符 | 含义 | 示例 |
|---|---|---|
{title} | 会话标题(还没生成时渲染为空) | 体检报告分析与健康趋势 |
{date} | 本地日期 | 20261110 |
{time} | 本地时间(时分秒) | 102345 |
{id} | 会话 id 去掉 session- 后的前 8 位 | 9f3a1c2b |
{session} | 完整会话 id | session-9f3a1c2b-... |
- 名称里 Windows 不允许的字符会被替换为空格;重名时自动追加
-2、-3。 - 标题最多取 60 个字符,整个文件夹名上限 100 个字符。
- 想让文件夹名带日期,就把模板改成
{title}{date};想要唯一性可加{time},例如{title}-{time}。 - 只有在本次运行内由插件创建过文件夹的会话才会跟着标题改名;重启后不会再追溯改名(避免误改你手动命名的目录)。
配置
在插件的 cordis.patch.yml 行里(安装后位于 Harness 配置的用户补丁文件中):
| 键 | 默认值 | 说明 |
|---|---|---|
enabled | true | 关掉整个行为但保留插件。 |
folderName | '{title}' | 文件夹名模板。 |
includeDerivedSessions | false | 是否也给分支会话(fork)和子代理会话单独建文件夹。默认关闭:它们沿用父会话的文件夹,保持文件上下文连续。 |
isolateSessions | [] | 需要补充隔离的会话 id 列表。列在这里的会话即使已经有对话内容,也会在下次被打开/恢复时建文件夹并切过去(用来补建历史会话的文件夹)。补完可以从列表里删掉。 |
debug | false | 往 ~/.dsh/session-folders.log 写诊断行(每个决定一行)。排查问题时临时打开。 |
判定规则(为什么有的会话不建文件夹)
session/created 不只在"新建会话"时触发,打开旧会话、DSH 启动恢复会话、在会话里点"新对话"清空重开时也会触发(清空重开的会话会保留原来的 createdAt,所以不能用"创建时间新不新"来判断)。插件按下面顺序判定,任何一条不满足就跳过:
- 会话必须有
header.cwd; - 该
cwd必须恰好等于某个已注册工作区的路径(忽略大小写/分隔符差异); - 会话日志里还没有模型的回复(有
assistant消息就说明这段对话已经跑过,属于旧会话,不动它); - 会话还没有被隔离过(当前工作目录仍等于工作区根目录);
- 不是子代理/分支会话(除非
includeDerivedSessions: true); - 例外:会话 id 出现在
isolateSessions里时,跳过第 3 条,强制补建。
同一套判定还会在每轮开始前(agent/pre-step)再跑一次:如果创建那一刻因为注册表还没就绪、或工作目录指向了别处而没建成,这一步会补上,保证第一轮工具执行时文件夹已经存在。已经隔离过的会话在这一步直接跳过,不会二次搬家。
不会做的事
- 不会改动已经有对话内容的旧会话(除非把它的 id 写进
isolateSessions)。 - 在会话里点"新对话"清空重开时,会当成一个新对话,另建一个文件夹(旧文件夹保留旧对话的文件)。
- 不会重命名升级前就已经建好的文件夹(例如
20261110-141707-…);那些文件夹是旧会话的工作目录,改名会让旧会话找不到自己的文件。 - 不会修改会话头部的
cwd,也不会改动工作区注册信息;工作区路径和文件本身都不动。 - 不会处理工作区之外的会话(例如默认目录里新建的会话),它们没有对应工作区,插件直接跳过。
- 新建后从未发过消息的空会话会留下一个空文件夹;这个空文件夹会在你删除对话时一起清掉(见下一节)。
删除对话与清理文件夹
左侧会话列表的 ··· 菜单里多了一条 「删除对话(含工作区文件)」(原来的「删除」菜单项仍然保留)。点击后的流程:
- 插件先向宿主查询这个对话的文件夹:路径、是否存在、里面有几个条目;
- 没有文件夹,或文件夹是空的 → 直接删掉对话(空文件夹一并删掉),不弹窗;
- 文件夹里有内容 → 弹窗「是否删除对应工作区文件?」:
- 是(连文件一起删) → 删除文件夹及里面所有文件,再删对话;
- 否(只删对话) → 只删对话,文件夹和文件留在工作区里;
- 取消 / Esc / 点背景 → 什么都不做;
- 查询失败时弹窗会说明原因,并提供「仅删除对话」作为保底选项。
删除对话优先走本机已安装的 @michengai/dsh-archive-manager 提供的 workspaceRegistry.deleteSession(sessionId)(它会同时清理会话日志、投影缓存和子会话);如果它不可用,插件退回去自己删会话存储目录、投影缓存并从工作区里摘除该会话。
安全限制:只有当目标文件夹严格位于某个已注册工作区之内、且不等于工作区根目录时才会被删除;任何越界路径都会被拒绝并记入日志。
删除接口是宿主上的两个本机同源 HTTP 路由(/api/dsh-session-folders/inspect、/api/dsh-session-folders/delete),要求请求头 x-session-folders: 1、回环地址与同源 Origin;浏览器半侧在 client.js 里,只负责菜单项和弹窗。
排查
插件把每个决定写进 ~/.dsh/session-folders.log(需要 debug: true):
| 日志行 | 含义 |
|---|---|
module loaded (build N) | 模块被进程加载了(重启后应出现一行)。 |
apply build=N ... | 插件已激活且配置生效;forced=[...] 是 isolateSessions 当前列表。 |
created id=... seq=... cwd=... derived=... forced=... | 收到会话创建/打开通知;forced 表示命中了 isolateSessions。 |
workspaces=[...] | 读取到的工作区路径列表。 |
folder=... / committed ... | 建目录成功、工作目录已切换。 |
skip: ... / error: ... | 跳过原因或失败原因。 |
inspect id=... managed=... content=... count=... | 删除前查询文件夹:是否由插件管理、是否有内容、有几个条目。 |
delete id=... folder=... via=... deleted=... | 删除结果:是否连文件夹一起删、走的是 workspaceRegistry.deleteSession 还是内置存储清理、对话是否删掉。 |
refused folder delete not-inside-workspace ... | 目标文件夹不在任何已注册工作区内,拒绝删除(安全保护)。 |
兼容性
- 面向 DeepSeek Harness(DSH)桌面版;Host 半侧依赖
workspaceRegistry、webServer服务与session/created、agent/pre-step、session/event、agent/turn-stopping事件,以及核心的working-directory/change事件投影。 - 删除对话优先复用本机可能安装的
@michengai/dsh-archive-manager(它给workspaceRegistry补了deleteSession);没装也能工作,会走插件自带的清理逻辑。
安装
本插件是一个 DSH bundle(Host 半侧 index.js + Web 客户端半侧 client.js),用 Harness 的插件管理器安装:
- 把本仓库克隆或下载到本地,例如
D:\dsh-plugins\dsh-session-folders。 - 在 DSH 里让模型执行
plugin_manager action: install_bundle target: <上一步的绝对路径>。 - 重启 DSH,然后新建一个工作区并在其下新建会话,即可看到专属文件夹。
安装后 profile 的 package.json / pnpm-lock.yaml 会以 link: 指向这个源码目录,node_modules/dsh-session-folders 是指向它的目录联接(junction)。因此:
- 移动或删除源码目录会让插件失效:移动后需要把 profile 里的
link:路径和那个联接一起改到新位置,或先移除再重新安装。 - 修改
index.js/client.js/cordis.patch.yml后,在插件管理里把本插件关掉再打开即可热加载(debug打开时日志里会出现module loaded (build N));改动涉及 Host 路由时可能需要重启。 - 本目录只是插件源码,不是"会话文件夹";插件只会给新建会话在工作区根下建一个以会话标题命名的文件夹(标题就绪前临时用会话 id 短名,如
9f3a1c2b)。
开发
npm test
# 等价于依次运行:
node tests/run3.mjs # 创建 → 命名 → 跟随标题改名
node tests/run4.mjs # 旧会话判定、isolateSessions 补建
node tests/run5.mjs # 删除接口(inspect / delete 路由)
node tests/run6.mjs # agent/pre-step 兜底
四套仿真都不需要真正启动 DSH:用假的 ctx、临时工作区和临时 HOME 跑真实文件系统,全部输出 ALL CHECKS PASSED 即通过。
许可
MIT