dsh-temptask
aeroscis/dsh-temptask
DSH 临时任务模式:为 DSH 增加「任务」分组——不绑定任何长期工作空间,自动在任务根目录下创建隔离的临时工作目录并开启会话,即开即用、用完即弃。Temporary task mode for DeepSeek Harness.
安装
dsh plugin --profile web add github:aeroscis/dsh-temptaskREADME
dsh-temptask — DSH 临时任务模式
给 DeepSeek Harness 增加「临时任务」:一键在 DSH 自己的目录下开一个隔离的临时工作目录, 并在里面开会话。实验、临时调试、一次性会话——即开即用、用完即弃。
- 任务目录就是工作区:
<DSH_HOME>/dsh-temptask/<年-月-日-时-分-秒>/,会话 cwd 就是它 - 因此侧边栏长成 DSH 原生的树:任务节点 → 它的会话,展开 / 归档 / 重命名 / 搜索 / 拖拽全部是官方能力
- 不用起名:节点名就是时间戳目录名(DSH 对工作区的默认行为,本插件不改写它);会话标题由 DSH 自动总结,显示在它下面的会话行上
/temptask list | new | open | clean | help命令(补充入口,日常点侧边栏即可)- 清理有二次确认,删除前做路径穿越校验,只删任务根目录下的子目录;节点与记录一起清理
1. 安装
# 默认 profile
dsh plugin add @aeroscis/dsh-temptask
# 指定 profile
dsh plugin --profile <profile> add @aeroscis/dsh-temptask
# 从本地源码目录安装(尚未发布到 registry 时)
dsh plugin --profile <profile> add /path/to/dsh-temptask
然后重启 DSH。重启后侧边栏顶部(「新建会话」按钮下方、工作区列表上方)多出一行 「+ 临时任务 │ 🗑 ⧉ ? ⚙」——主区整块可点 = 新建临时任务(不是一个小图标了), 右侧竖线分隔出四个次要动作:清理 / 打开(或复制)根目录 / 说明 / 设置; 点标签不出任何页面;窄栏时官方把这一行收成 36×36,只剩主区那个 + :
- 点
+(或/temptask new)→ 目录<DSH_HOME>/dsh-temptask/<时间戳>/被创建,并在其中打开一条会话; - 侧边栏出现对应的任务节点,展开就是它的会话;给它发第一条消息后会生成会话标题(显示在会话行上), 而节点名始终保持那个时间戳目录名——想换个名字就在节点上右键重命名(那是 DSH 自己的能力);
⚙里选「删除任务时怎么处理它的会话」,?里是这份说明。
卸载:dsh plugin --profile <profile> remove @aeroscis/dsh-temptask。
任务目录与 tasks.json 不会被卸载流程删除,用菜单里的「清理临时任务」或手工删除。
支持的 DSH 版本
- 声明:
engines.dsh与peerDependencies里的@deepseek-ai/dsh都是^0.1.5-rc.2 || ^0.2.0-rc.1。市场的兼容性判定读的就是这些字段(@deepseek-ai/schemastery另有版本线,不参与 DSH 版本判定),它判的是"范围是否满足",不是"版本是否相等"—— 插件的版本号是它自己的迭代序号,与 DSH 版本无关。 - 硬门槛:DSH 0.2.0-rc.1 起,宿主在挂载前会先核对插件的
@deepseek-ai/dsh*peer 范围, 不满足就整条 bundle 直接跳过(日志:skipping profile bundle,界面上那一行直接消失)。 本插件声明了双范围,两条线都能过这道门。 - 开发与验证环境(Windows 上实测,Linux 由 GitHub Actions 复跑全部检查):
0.1.5-rc.2(三方桌面 App DSH Desktop 2.0.13 内置的那条线):settings 命名空间 +sessions.open;0.1.7-rc.2(官方 DeepSeek Harness 早期桌面版):同上,界面差异由本插件自行吸收 (图标自绘、order取负值);0.2.0-rc.1(官方桌面版当前线,核心包与桌面版同号):插件自身的 Config schema +configEditor+uiWorkspace.openSession(新形态,见 §5)。
- 依赖的宿主能力(缺失时逐项降级,任何一种缺失都不会让插件加载失败):
sessionController(create/cancel/openWorkspacePath)、workspaceRegistry、commands、webServer;配置侧在 0.1.x 用settings命名空间、在 0.2.0-rc.1 起用configEditor(两者都没有时退回<dataDir>/config.json);客户端用configForms(0.2.0-rc.1)或settingsScope(0.1.x),插槽sidebar.panellist/main/shell.overlay/settings.section两条线同名。这些服务全部按可选读取。界面侧不依赖任何宿主 Client 包(行内图标自绘,见 §3)。 - 怎么自查:
?气泡底部显示本插件的两半版本(插件后端 v… · 插件界面 v…)—— 这两个数是插件自己的,不要拿去和 DSH 版本比较;DSH 自身版本在 DSH 的关于页或市场里看。
升级 DSH 之后如果出现异常,先看 ? 气泡里的插件版本,再用 engines.dsh 对照本插件的发布说明。
2. 项目结构
dsh-temptask/
├── package.json # 插件元数据 + dsh 清单 + 构建/测试脚本
├── cordis.patch.yml # 补丁层:把本插件行插入 profile 的组合
├── tsconfig.json # host + 共享代码(strict,NodeNext)
├── tsconfig.client.json # client(strict,DOM + JSX)
├── src/ # ★ 类型化源码(唯一事实来源)
│ ├── host.ts # Cordis 插件入口:name / inject / apply,装配全部部件
│ ├── types.ts # host/client 共享类型(零运行时依赖)
│ ├── config.ts # 配置归一化、settings schema、原子写 JSON
│ ├── paths.ts # 路径/时间戳目录名/唯一化/路径穿越校验
│ ├── store.ts # tasks.json 持久化(原子写 + 串行锁 + 跨进程重读 + 损坏备份重建)
│ ├── workspaces.ts # ★ 临时工作区:建/查(**不改写标题**)/删 + 归档会话
│ ├── sessions.ts # 会话创建(sessionController 主路径 + agents 降级)与挂载
│ ├── tasks.ts # 业务核心:建/开/删/清理/自动登记/自动清理
│ ├── hooks.ts # session/created、session/disposed、session/event(user|title)
│ ├── commands.ts # /temptask 命令与参数解析
│ ├── routes.ts # /dsh-temptask/api/* HTTP 桥(同源围栏 + JSON)
│ ├── client.tsx # ★ client 侧:侧边栏面板行 + 动作页 + 清理对话框 + 设置卡片
│ ├── index.ts # 包入口(重导出 host half)
│ └── dsh.d.ts # DSH 接口的最小环境声明(见 §8)
├── lib/ # 构建产物:host 运行时(ESM,DSH 加载的就是它)
├── client/client.js # 构建产物:client 的 ModuleLoader 包
├── scripts/ # build-client / prepare / check-pack / check-registration / smoke / smoke-client
├── README.md
└── LICENSE
3. 界面(几乎全是原生能力)
| 位置 | 谁提供 | 内容 |
|---|---|---|
| 侧边栏任务节点 + 展开后的会话 | DSH 官方 WorkspaceBrowser | 展开/收起、归档、重命名、搜索、拖拽排序、删除——本插件一行 UI 都不写 |
侧边栏那一行(sidebar.panellist) | 本插件 | 紧贴「新建会话」下方(注册 order 取负值,排在官方「插件」面板行 order: 0 之前),一个 .5px border-l3 + button-elevated-fill + r12 的框(取值抄自官方 .newSession):左边是主区「标记 + 临时任务」,整块可点 = 新建(role="button" + Enter/Space,hover 用 button-floating-hover);中间 1px 分隔线;右边四个 28×28 次要图标(清理 / 打开或复制 / 说明 / 设置),清理 hover 转危险色。行内所有字形都是本插件自绘的 SVG(「临时任务」标记 = 文件夹轮廓 + 右下角时钟徽标;四个动作 = 垃圾桶 / 右上箭头 / 纸张 / 问号 / 齿轮,创建中是缺口圆环 + 旋转)。点主区不出任何页面;窄栏时官方把行收成 36×36,只剩那个标记 |
| 齿轮 → 精简设置(浮层) | 本插件 | 只放一个策略:删除任务时如何处理它的会话 —— 归档会话(推荐) / 留在未分组。写入优先走客户端配置通道(configForms,0.2.0-rc.1 起;settingsScope,0.1.x),被拒或缺失时退回 /api/config —— host 侧最终写的是同一处(profile 补丁或配置文件) |
说明气泡 / 清理确认框(shell.overlay) | 本插件 | ? = 锚在该行下方的小气泡(任务数量 / 根目录 / 配置来源 / 记录文件 / 版本(插件后端 + 插件界面) / 会话在哪 / 与 dsh-side-session 的区别 / 更多设置入口)。版本那一行会说清两半的区别:插件后端(Node 侧,只在 DSH 启动时加载 → 改插件要重启 DSH)与插件界面(按页面加载 → 刷新页面即更新);两半不一致时会直接提示"重启 DSH"。它是悬浮详情:鼠标在气泡上就不动,离开约 0.6s 后自动收起——没有手动关闭按钮(再点一次 ? 也会收起);垃圾桶 = 居中确认框(勾选列表 + 二次确认);齿轮 = 操作面板(不会自动收起);另有创建失败时的小提示。都渲染在浮层里——不会被侧边栏裁掉,也不会嵌套进官方那个 <button> |
兜底空面板(main,key=dsh-temptask) | 本插件 | 渲染 null 并立刻切回 conversation。那一行在官方源码里是导航按钮(onClick: selectPanel(id),键盘 Enter/Space 也只走它)——有这个兜底,误触只会闪一下,绝不会出现空白页或新页面 |
| 清理对话框 | 本插件 | 任务勾选列表(默认勾选已关闭的)、全选/全不选、二次确认 |
| 设置 → 插件 → 临时任务 | 本插件(settings.section) | 根目录、自动清理天数、删除策略、配置来源、清理入口、与 dsh-side-session 的区别 |
常驻守护(shell.overlay,渲染 null) | 本插件 | 轮询 host 的 pendingOpen:/temptask new、/temptask open 之后自动把会话切到前台 |
为什么点标签没有页面:任务列表就是侧边栏里的树,入口只需要一行标签 + 几个按钮。
DSH 的树按「工作区归属」分组,而会话归到某个工作区当且仅当 session.cwd === workspace.path
(见 §8 证据),所以把任务目录本身做成工作区,就能白拿官方的整棵树;本插件只在那行里放四个动作,
不替换任何官方 Slot、不新增任何导航目的地。
行内图标全部自绘,不再 require 宿主图标包(@deepseek-ai/dsh-client-ui-primitives)。原因是那个包
在 0.1.7-rc.2 里把图标导出名从 <Name>Outline<尺寸>(IconTrashOutline16)改成了
<Name>OutlineRegular|Medium:老名字取不到时旧版代码会静默降级成 🧹 📂 ? ⚙ 这类文本/emoji 字形,
于是同一份插件在 0.1.5 宿主上是线框图标、在 0.1.7 宿主上变成 emoji(官方自己的插件文档也要求
「不要 require 宿主 Client 包」)。自绘的代价只是观感要自己对齐官方规格:viewBox 16、
stroke="currentColor"、1.4px 线宽。因此清单里也不再需要客户端依赖声明:
"dsh": { "client": { "platform": "web" } }
任何时候都不会因为宿主少一个图标或改了图标名而变形、也不会因此挂掉(data-tt-mark / data-tt-icon
是这些自绘字形的记号,可在开发者工具里直接认出是哪一枚)。
已知边界(来自官方插槽的约束,不是疏忽):那一行官方是按「导航按钮」渲染的,行内内容落在
aria-hidden 的字形槽里——所以主区和四个小按钮对屏幕阅读器不可见(鼠标可用,每个都有 title 与
aria-label;主区用 role="button" + tabIndex 接键盘,因为官方那一行本身已是 <button>,
再嵌一个是真的非法 HTML);同样的动作在「设置 → 插件 → 临时任务」与 /temptask new | clean 命令里
都可键盘到达。
另一个细节:官方 PanelRow 除了渲染字形槽,还会用注册的 label 再渲染一次标题
({wide && <span className="panelTitle">{label}</span>}),而这一行的可见文字由本插件自己渲染
(因为要「标签在左、按钮在右」)。两者叠加会出现两遍「临时任务」,所以本插件注入了一条限定作用域的样式:
[class*="panelRow"]:has(.__tt_row) [class*="panelTitle"] { display: none }
只隐藏本行那一个重复标题,其它面板行不受影响;label 仍保持非空——官方用它做 tooltip 与无障碍名。
4. 命令
/temptask list 列出全部任务(ID / 目录 / 节点 / 状态 / 时间)
/temptask new 新建任务并打开会话(目录名=时间戳,节点名就是它)
/temptask open <ID|目录名|标题> 打开任务;支持片段匹配,多个匹配会列出候选
/temptask clean <ID|目录名|标题> 删除任务目录与节点(**先预览**,加 --yes 才执行)
/temptask clean --all --yes 删除全部任务
/temptask help 帮助
- 标题含空格用引号:
/temptask open "修复登录 bug"、/temptask clean "修复登录 bug" --yes clean的二次确认语义:第一次执行只打印将要删除的任务,必须追加--yes才真删/temptask new、/temptask open登记pendingOpen,前端在 2.5s 内自动切到该会话
5. 配置
配置的所有权按 DSH 版本走,但对使用者都是同一件事:在「设置 → 插件 → 临时任务」里改。
| DSH | 主通道 | 存放位置 |
|---|---|---|
0.2.0-rc.1 起 | 本插件的 Config schema(lib/index.js 导出 Config,四个字段都标了 volatile) | profile 的 cordis.patch.yml 里本插件那一行的 config: |
0.1.x | settings 命名空间 dsh-temptask(ctx.settings.register) | DSH 的 settings 文档 |
四个键都一样:
| 键 | 默认 | 说明 |
|---|---|---|
rootDir | <DSH_HOME>/dsh-temptask | 任务根目录(Windows 即 C:\Users\<你>\.dsh\dsh-temptask)。留空 = 默认 |
autoCleanDays | 0 | >0 时,启动清理「已关闭且最后打开超过 N 天」的任务(会话按下面的策略处理) |
onDeleteSessions | archive | 删除任务(含自动清理)时如何处理它的会话:archive = 归档(从侧边栏消失,日志保留)/ keep = 留在「未分组」。见 §6 |
dataDir | <DSH_HOME>/plugin-data/dsh-temptask | 插件数据目录(改它需要重启 DSH 才切换记录文件) |
也可以在 cordis.patch.yml 的插件行里给初始值(0.2.0-rc.1 起设置表单改的就是这一行):
- insert:
- id: dsh-temptask
name: '@aeroscis/dsh-temptask'
config:
autoCleanDays: 7
为什么 0.2.0-rc.1 换了形态:新宿主的 ctx.settings(SettingsForms)不再提供
register(namespace, schema),那个被 settingsNamespace 品牌化的命名空间注册表已经删掉了;
取而代之的是「插件用自己 Config 里的 volatile 字段声明设置」,读取时字段是活引用
(config.rootDir.get()),写入统一走 ctx.configEditor.edit(entry, …)。本插件因此:
- 导出
Config(schemasteryz.object,rootDir/autoCleanDays/onDeleteSessions/dataDir全部.volatile()), 官方「设置 → 插件」表单据此渲染; apply(ctx, config)里读字段时兼容两种形态(引用取.get(),普通值直接用);- 写入优先走
configEditor(与官方表单同一处),其次是 0.1.x 的 settings 命名空间, 最后才退回config.json; - 监听加载器的
loader/volatile-update事件:官方表单改完配置,插件当场重读引用、 必要时重新载入清单(改rootDir立即生效,不用重启 DSH)。
注意:@deepseek-ai/schemastery 3.18.4 才有 .volatile();0.1.x 宿主附带的是 3.18.2,
那里会自动退化成普通字段(配置仍可用,只是表单少了热更新语义)。
降级通道:宿主既没有 configEditor、也没有 settings 服务、或取不到 schemastery 时,
插件不会加载失败,而是改用 <dataDir>/config.json(设置卡片会显示「配置来源」)。
6. 数据与文件布局
<DSH_HOME>/dsh-temptask/ ← 任务根目录(启动时自动重建;删除操作永不碰它本身)
├── 2026-09-27-14-30-00/ ← 一个任务 = 一个目录 = 一个临时工作区
│ └── …(会话在这里读写文件)
├── 2026-09-27-14-31-12/
└── …
<DSH_HOME>/plugin-data/dsh-temptask/
├── tasks.json ← 任务记录(原子写:先写 .tmp 再 rename)
├── tasks.json.bak ← 仅当记录损坏时生成
└── config.json ← 仅在没有 settings 服务时使用
- 目录名 = 本地时间戳
YYYY-MM-DD-HH-mm-ss;同一秒内重复创建会自动追加-2,绝不覆盖 - 任务目录被手工删除 → 启动对账会丢弃对应记录并告警(目录是任务的实体)
- 记录损坏 → 备份为
tasks.json.bak,重建空清单并在设置卡片显示告警 - 节点被手工删除 → 再次「打开」该任务时会按路径把工作区建回来(自愈)
- 删除任务 = 目录 + 侧边栏节点 + 记录;命名/删除策略的处理见下一节
删除任务时,它的对话去哪了(onDeleteSessions)
一个任务先后可能开过好几条会话(会话结束后再打开,会在同一目录新建一条)。删除任务时:
- 先读会话账目 ——
WorkspaceEntity.sessionIds(按sessionPath(id) === path过滤, 正好等于「cwd == 该任务目录」的全部会话)。工作区记录一删这份账目就没了,所以必须在删除前读; 只记着"最后一条 sessionId"是不够的; - 运行中的会话先中止当轮 ——
sessionController.cancel(UI 上「停止」按钮的同一个入口), 免得 agent 继续往一个即将消失的目录里写。插件无法销毁会话对象(那归创建它的 agent 工厂), 所以确认框会给这类任务打上「运行中」标记; - 删目录 → 删节点 → 删记录;
- 按策略处理会话:
| 策略 | 行为 | 代价 |
|---|---|---|
archive(默认) | 逐条 workspaceRegistry.archiveSession() —— 就是官方右键「归档会话」的同一个 API。官方浏览器会把已归档会话排除在列表外,所以它们从侧边栏消失 | 日志仍在 $DSH_HOME/sessions 里占空间;DSH 没有反归档 API、也没有"已归档"列表,所以从 UI 上找不回来 |
keep | 什么都不做 → 会话落进官方的「未分组」桶(DSH 删工作区的原生行为) | 聊天记录会留在侧边栏,需要你自己收拾 |
为什么不能"真删":sessionPersistence 只暴露 create / open / flush / stat / list,文档明写会话日志是
append-only、"never rewritten"——没有删除 API。绕过去直接删 ~/.dsh/sessions/** 会跳过
session-query 的内存缓存与 session_projcache 的 sqlite,属于碰私有存储,本插件不做。
策略在齿轮 → 精简设置或设置 → 插件 → 临时任务里改;手动删除与自动清理(autoCleanDays)
走同一条 disposeTask 路径,所以不会出现"自动清理归档、手动删除不归档"这种不一致。
并发:同一个 DSH_HOME 下多个 profile 同时运行
tasks.json、配置、rootDir 都在 DSH_HOME 级,所以多个 profile 同时运行(例如桌面版与 dsh web)时共用一份记录。
两个进程同时写时的处理:
- 写入前在锁内重读磁盘,以磁盘内容为基准做变更(按 id 增删改),不拿本进程的内存副本整份覆盖;
- 幂等写:变更前后内容一致就不落盘(例如
session/disposed落在非任务会话上); - 界面与命令读到的都是磁盘现状:
GET /state与/temptask list会先重读一次; - 「是否已登记」这类判断也放在锁内做,避免两个进程把同一目录登记成两条记录。
残留窗口:没有跨进程文件锁,理论上仍有「读盘 → 变更 → rename」这段亚毫秒级窗口内的互相覆盖。 彻底消除需要 OS 级文件锁或每任务一个文件;对人工点按钮的频率不值得,故明确记录而不是假装没有。
7. 架构
┌──────────────── client half (浏览器) ────────────────┐
│ sidebar.panellist → 「临时任务」那一行 + 四个动作 │
│ shell.overlay → 说明气泡 / 清理框 / 齿轮 / 守护 │
│ settings.section → 配置卡片 + 清理入口 │
│ (任务列表本身由官方 WorkspaceBrowser 按工作区分组渲染)│
└───────────────┬──────────────────────────────────────┘
│ fetch("/dsh-temptask/api/*") ← 同源 + 回环围栏
┌───────────────▼──────────── host half (Node) ────────┐
│ index/host.ts 导出 Config(官方设置表单)+ apply 装配 │
│ routes.ts 状态 / 新建 / 打开 / 删除 / 清理 / ack / 配置 │
│ tasks.ts 目录 + 工作区 + 会话 + 登记 + 清理 │
│ workspaces.ts 工作区建/查/改名/删(侧边栏节点就是它) │
│ store.ts tasks.json(原子写 + 串行锁 + 写前重读) │
│ hooks.ts session/created · disposed · event │
│ commands.ts /temptask │
│ sessions.ts sessionController.create({ cwd }) │
└───────────────────────────────────────────────────────┘
配置写入的三条路(按宿主能力选,见 §5):configEditor.edit(0.2.0-rc.1)→
0.1.x 的 settings 命名空间 → <dataDir>/config.json;官方设置表单与插件自己的齿轮面板
最终都落到同一处。
创建一个任务时的顺序(不能改):
mkdir <rootDir>/<时间戳>——workspaceRegistry.create要求目录已存在;workspaceRegistry.create(dir)→ 拿到 realpath 归一化后的路径;sessionController.create({ cwd: <归一化路径> })—— 只传 cwd,不传 workspaceId;workspace.attachSession(sessionId)—— DSH 会硬校验 cwd === workspace.path;- 写入
tasks.json。
第 2 步回传的路径必须在第 3 步使用:如果自己拼路径(含 Junction/符号链接时与 realpath 不同), 第 4 步会直接抛错。冒烟测试的桩复刻了这条校验,所以顺序写反会被测出来。
- 不注册任何全局服务,host 与 client 之间只走插件自己的 HTTP 路由(社区插件 dshmarket 的既有做法)
- 所有副作用都挂在 Cordis fiber 上(
ctx.effect/ 注入子上下文),插件停用或替换时自动回收 - 所有服务都按可选读取(
ctx.get+ 缺失即降级),任一服务缺失都不会让插件加载失败
8. 设计取舍(以及为什么)
早期设计里假定的部分 API 在真实 DSH 中并不存在;另外有两条设计目标在 DSH 的数据模型下互相冲突。 本插件按「能真正在侧边栏长成树」的形状实现,逐条说明:
| 早期设计 | 实际实现 | 说明 |
|---|---|---|
session.create({ cwd }) | ctx.sessionController.create({ cwd }) | DSH 没有公开的 session 服务;sessionController 就是 ctx.remote.session 的 host 实现,其 create 明确接受「只给 cwd」。降级:ctx.agents.create({ sessionId, meta: { cwd } }) |
session.created / closed / focused | session/created / session/disposed / session/event 的 user/message | 真实事件名用斜杠;没有 focused 事件,用户发消息等价于「正在用这个任务」 |
| 任务不进工作区列表 | 任务 = 一个临时工作区,会出现在侧边栏的工作区列表里(都在 <DSH_HOME>/dsh-temptask 下,可整体清理) | DSH 的树按工作区归属分组:WorkspaceEntity.attachSession 要求会话 cwd 与工作区路径一致(否则抛 cannot attach session '…': its cwd resolves to '…'),且工作区的 sessionIds 按 sessionPath(id) === path 过滤——即会话归到某工作区当且仅当 session.cwd === workspace.path。所以「一个叫『任务』的容器 + 每个子会话各带自己的临时目录」在 DSH 里无法表达;把任务目录本身做成工作区,才能直接获得「任务节点 → 子会话」这棵树与全套原生操作 |
| 侧边栏新增「任务」分组,与项目同级 | 任务以工作区节点的形态出现在官方树里 + 侧边栏一行「临时任务」标签与 +/🧹/⧉/? 四个小按钮(位置在「新建会话」与工作区列表之间) | 侧边栏没有给「同级第二个浏览分组」留增量插槽:sidebar.workspaces 是 single 且标记 shadows-shipped-ui,占用它会遮蔽官方工作区浏览器,并让它的子插槽 sidebar.workspaces.directoryFlow 失效——该洞由官方 WorkspacePickFlow 在「添加工作区」流程里渲染(flowAvailable = useDirectoryFlow(occupied => occupied)),占了它反而会弄坏官方的按目录添加工作区;而官方浏览器组件只导出 apply/inject,无法复用。那一行的点击语义是 selectPanel(id)(键盘激活同样只走它),所以本插件同时注册同名 main 面板作为空兜底(渲染 null + 立刻回会话),保证不出现任何页面 |
任务名称(输入框、nameTemplate、重命名显示名) | 整条删除 | 工作区标题默认取目录 basename,DSH 只在显式 rename 时才改它 —— 会话标题属于会话行,不属于节点。所以本插件不碰工作区标题(早期版本曾把 DSH 总结的会话标题写回节点,结果每轮对话都可能给节点改名、还会覆盖手工改的名字,已删除,并留了一条回归用例)。要改名就在节点上右键重命名(官方能力) |
| 任务根目录放在用户自选路径 | <DSH_HOME>/dsh-temptask | 放在 DSH 自己的目录下,不掺进用户的工作目录;目录名 = 时间戳,一个任务一个目录,天然不重名、可排序 |
| 命令解析支持参数带空格 | /temptask open "2026-09-27" | 支持单/双引号包裹(目录名是时间戳) |
| 右键菜单里的「复制任务路径」 | 那一行改为 打开任务根目录(官方 sessionController.openWorkspacePath,它接受任意路径);宿主不能打开文件夹时自动退回"复制"。复制始终留在齿轮里(次要动作不跟主按钮抢位置) | 「打开」是目的地动作,「复制」是它的权宜替代——但粘进终端/编辑器仍有用,所以两者都留,只是分层:能用就用打开,不能用才复制 |
| 删除的二次确认 | UI 弹确认对话框 + 命令必须 --yes + 服务端强制 confirm: true | 三层都拦,绕过 UI 也删不掉 |
| 插件数据目录 | <DSH_HOME>/plugin-data/dsh-temptask | 可用 dataDir 覆盖(重启生效) |
| TypeScript 严格模式 | src/ 全部 strict 编译通过 | lib/、client/ 是构建产物(DSH 加载的是 JS)。官方 @deepseek-ai/dsh-* 包不带 .d.ts,所以 src/dsh.d.ts 用逐条抄自 live Inspect 目录与官方源码的最小声明固定了本插件真正用到的契约 |
兼容 dsh-side-session | 不冲突 + 设置卡片提示区别 | 不强制禁用任何一方 |
其他实现选择(需求未指定但影响体验):
- 打开任务的语义:会话还活着就复用;已关闭或进程重启过,就在同一目录里新建一条会话 (同一任务因此可以有多次会话,节点下会列出它们的历史——与官方工作区行为一致)
- 失败即回滚:目录刚建好但工作区/会话没建成时,目录会被删掉、记录不留,界面给出原因
- 根目录不可用:不再自动换目录(默认值就在 DSH_HOME 内),而是如实报错并提示去改设置
9. 开发
pnpm install
pnpm typecheck # host + client 两套 tsconfig,strict
pnpm build # src/ → lib/ + client/client.js
pnpm check:pack # 发布形态自检 6 项(入口/源码/脚本是否都被 files 覆盖)
pnpm smoke # 65 项端到端/渲染冒烟测试(40 host + 25 client,不需要 DSH 进程)
pnpm check:registration # 用宿主真实的 dsh-client-modules 校验 ModuleLoader 注册契约(需要 DSH 桌面应用)
pnpm check # typecheck + build + check:pack + smoke + check:registration
check:pack 也挂在 prepack 上,所以 npm pack / npm publish 之前会自动跑一遍:
files 少写一条(例如漏了 src)在源码目录里跑测试是发现不了的,只有装包的人才会撞上。
scripts/smoke.mjs(50 项)用桩 Cordis 上下文挂载 lib/index.js 直打 HTTP 路由。
桩里的 workspaceRegistry 刻意复刻 DSH 的语义(create 走真实 realpath、attachSession
硬校验 cwd === workspace.path),覆盖:时间戳目录、任务=工作区、会话只传 cwd、挂载、
会话标题变化不改动节点名(回归用例:发 session/title 事件后节点标题仍是目录名、且一次
workspace.setTitle() 都没调用)、命名接口确实已移除、MISSING_DIR、CONFIRM_REQUIRED、
删除策略 archive:连旧会话一起归档(断言归档 2 条,而不是只归档最后一条)、
删除策略 keep:一条会话都不归档、删除运行中的任务前先 cancel 当轮、
"目录/节点/记录三样同步清"、命令预览语义、session/disposed → closed、外部会话自动登记、
同名消歧、路径穿越防护(伪造 dirName=../ 的记录必须被拒绝且根目录外文件毫发无损)、
记录损坏备份重建、启动对账丢弃幽灵记录、autoCleanDays 清理+归档会话、
缺 workspaceRegistry 时拒绝建任务并回滚目录、/open-root 走官方 openWorkspacePath 打开任务
根目录、宿主不能打开文件夹时能力位为 false 且接口如实返回原因、
数据安全:在任务根目录里放未登记的目录/文件,clean --all 与 autoCleanDays 都不能碰它们、
两个 host 进程并发、服务全缺失时优雅降级;
以及 0.2.0-rc.1 的配置模型:行配置是 volatile 引用时取到引用里的当前值、
配置来源标为 plugin-config、/api/config 走 configEditor.edit(且不落 config.json、
未改动字段原样保留)、改完根目录立刻生效、loader/volatile-update 后重读引用、
没有 configEditor 的宿主仍写 config.json、以及
Config schema 契约(4 个字段;schemastery 3.18.4 上标 volatile,3.18.2 上不标)。
scripts/smoke-client.mjs(27 项)按 DSH 的方式装载 client/client.js
(window.__ModuleLoader__),断言工厂只 require("react")(任何对宿主包的 require 都会让这条桩
直接失败)、注册了「那一行 + 兜底空面板 + 浮层×2 + 设置卡片」、面板行的 order 取负值
(排到官方「插件」面板行 order: 0 之前,紧贴「新建会话」)、行的 label 是 i18n thunk:切换语言后再取必须
得到另一种语言(官方 PanelRow 每次投影都会重读它)、宽栏那行自己的标签文字只出现一次、
五个自绘字形(断言 data-tt-icon / data-tt-mark,且产物里不再出现宿主图标包的 require、
不再出现 emoji 兜底字形、没有外链资源)、构建产物里带着限定作用域的隐藏规则、窄栏只留图标、
第三号按钮按宿主能力在"打开根目录 / 复制根目录"之间切换(含能力未知时按复制渲染)、
兜底面板渲染 null(点那一行不会出现任何页面)、浮层各态渲染正确
(help / notice / clean / 齿轮设置的两档策略与选中态)、
清理确认框按策略如实描述并标出「运行中」、settingsScope 与 locale 缺失时各自降级,
并把各组件真渲染一遍(react-dom/server)。
scripts/check-registration.mjs(3 项)补上前面那些桩测不到的一环:它加载宿主真实的
@deepseek-ai/dsh-client-modules,按浏览器的顺序装好 window.__ModuleLoader__ 队列门面,
然后走一遍宿主那条判据(loadBundle(url) → factories.has(row.id))。这条判据要求
ModuleLoader 的注册 id 必须等于包名(row.id 就是加载器条目名),所以注册 id 由
scripts/build-client.mjs 从 package.json 的 name 现读,不再手写。
宿主代码的位置按版本换过地方,脚本按顺序找:DSH_APP_ROOT 环境变量 →
C:\Program Files\DSH Desktop\resources\app\node_modules\…(0.1.x 三方桌面版)→ 本包依赖。
官方 DeepSeek Harness 桌面版 0.2.x 起把 dsh 运行时打包进了 app.asar,普通 Node 读不到
asar 内部的文件,所以要在新宿主上跑这一项得先解包,再指过去:
# 把 app.asar 里的 dsh/ 树解包到任意目录(asar 头是标准的,解包工具用什么都行)
$env:DSH_APP_ROOT = '<解包出来的 dsh 目录>'
npm run check:registration
脚本会自动适配两代 makeRequire 签名(0.1.x 是 (edges),0.2.x 是 (ownerId, edges)),
本机没有 DSH 时该项自动跳过(打印原因并以 0 退出),不影响 npm run check。
10. 排错
| 现象 | 处理 |
|---|---|
界面弹「部分插件加载失败 / Failed to load plugins」,文案里有 loaded without registering "@aeroscis/dsh-temptask" | 已在 0.3.2 修复(0.3.0 / 0.3.1 的客户端注册 id 写成了短名)。升级到 ≥0.3.2 后重启 DSH 即可;pnpm check:registration 能在发版前拦住这类不匹配 |
| 侧边栏没有「临时任务」这一行 | 确认 profile 名对不对、插件是否装上;看 DSH 日志里有没有 [dsh-temptask];窄栏(折叠)时它只剩图标,悬停有提示。DSH 0.2.0-rc.1 起还有一道兼容性门槛:插件声明的 @deepseek-ai/dsh* peer 范围不满足时,宿主会整条 bundle 跳过并在日志里写 skipping profile bundle "@aeroscis/dsh-temptask",此时升级到 ≥0.4.0 即可(0.3.3 声明的是 ^0.1.5-rc.2,过不了 0.2.x 的门) |
| 设置里看不到「临时任务」这一节 | 0.2.0-rc.1 起这一节由插件自己的 Config schema 生成(设置 → 插件);若整个「插件」页都没有,说明该宿主没挂 dsh-settings/config-editor(例如 dsh CLI 自建 profile 上没有 profileContext),此时配置走插件的齿轮面板(写入落到 <dataDir>/config.json) |
| 改了插件代码,行为却还是旧的 | 插件后端(Node 侧)只在 DSH 启动时加载——必须重启 DSH;插件界面(页面)刷新即更新。点 ? 看「版本」那一行:插件后端 v… · 插件界面 v…,两半不一致时会直接提示「重启 DSH」。启动日志里也有 [dsh-temptask] v<版本> 已就绪…。三处版本号都应与 package.json 一致 |
| 点了「新建」但没看到节点 | 节点是工作区,刷新/展开侧边栏;设置卡片里若提示「未提供 workspaceRegistry」,说明该宿主无法建节点(任务目录仍会创建) |
| 节点名一直是时间戳 | 这是预期行为:节点名 = 目录名(DSH 默认语义),本插件不改写它;DSH 总结的会话标题显示在该节点下的会话行上。想改节点名就在节点上右键重命名 |
| 建任务报「无法创建任务根目录」 | 改设置里的 rootDir,或检查 <DSH_HOME> 的写权限 |
| 设置卡片出现「记录损坏…已备份重建」 | 看 <dataDir>/tasks.json.bak 找回旧记录 |
/temptask open 说「任务目录不存在」 | 目录被手工删了:用清理移除记录后重新新建 |
| 打开了任务但页面没跳 | 前端每 2.5s 轮询 pendingOpen;直接点侧边栏对应节点即可 |
| 删了任务,对话却出现在「未分组」 | 默认策略会把它归档(从侧边栏消失)而不是留下;若你把策略切成了 keep,那就是预期行为。见 §6 |
| 两个 profile 同时开着,列表对不上 | 界面每次都会先重读磁盘,正常 1 个刷新周期内一致;长期不一致见 §6「并发」的残留窗口 |
11. 发布前检查清单
自动化覆盖边界:host 测试全部跑在桩 Cordis 上下文上,client 测试全部是 react-dom/server 的
静态渲染。也就是说:业务逻辑与各条降级路径已覆盖,真实 DSH 与真实 DOM 下的表现需要人工验收一遍。
需要人工验收(按出错后果排序)
- 数据安全(唯一"一错就丢数据"的地方):在
~/.dsh/dsh-temptask/里手动放一个没被登记的 目录 + 文件,再跑/temptask clean --all --yes,并让autoCleanDays清一次 → 那个目录必须毫发无损。 (自动化里已有等价的桩用例,但真文件系统上值得再确认一次。) - 那一行的真实外观:页面上「临时任务」是否只出现一次、边框是否只有一圈、hover 颜色是否正确、
+ 临时任务 │ 🗑 ⧉ ? ⚙在侧边栏宽度里是否挤。这两处 CSS 是按读到的官方类名做的:has()手术, 只有真 DOM 能证伪。 - 不导航:鼠标点主区 = 新建;键盘 Tab 到那一行按 Enter = 闪一下回到会话,中心列不能变空。
- 打开根目录:点下去资源管理器应弹出;若宿主不支持,应给提示而不是静默。
- 重启 DSH:任务节点/会话/
tasks.json都还在,?里插件后端 v… · 插件界面 v…一致。 - 切语言:英文界面下那一行 tooltip、
?气泡、齿轮文案要跟着变。 ?悬浮收起:鼠标进出气泡;以及"点完?不碰它、直接移开鼠标"也要关。- 窄栏:侧边栏折叠后只剩 +,点它应能新建。
- 设置表单(0.2.0-rc.1):「设置 → 插件 → 临时任务」里能改根目录 / 自动清理天数,
改完不重启就该生效(改
rootDir后新建任务落到新目录);?气泡里的「配置来源」应显示 「DSH 插件配置」,profile 的cordis.patch.yml里本插件那一行应该长出config:段。 - 两半版本:0.2.0-rc.1 上重启后
?里两半都应显示0.4.0(后端是 Node 侧的编译产物, 改完必须重启 DSH 才会换)。
发布前需要人工确认
package.json的repository/homepage/bugs/author与仓库一致;- 真安装形态:开发时用
link:安装即可,发布形态是 tarball —— 建议npm pack后在临时 profile 里dsh plugin --profile <profile> add ./aeroscis-dsh-temptask-<版本>.tgz启动验证一次; - 卸载:
dsh plugin --profile <profile> remove @aeroscis/dsh-temptask之后 profile 仍能正常启动 (语义是保留任务目录与tasks.json)。
已知限制
- 不支持真正删除会话:只能归档(DSH 未提供删除 API,见 §6);
- 跨进程并发只保证「写前重读 + 按 id 合并」,仍存在亚毫秒级残留窗口(自动化用例是同进程双实例模拟);
- 无障碍:那一行内部控件位于官方
aria-hidden字形槽内,屏幕阅读器无法读取; 等价动作在「设置 → 插件 → 临时任务」与/temptask命令里都可键盘到达。
12. 兼容性说明:dsh-temptask vs dsh-side-session
| dsh-temptask(本插件) | dsh-side-session(EAC 桌面版自带) | |
|---|---|---|
| 管理对象 | 「临时任务」:<DSH_HOME>/dsh-temptask/<时间戳> 目录 + 它的临时工作区 | 侧边/并行会话 |
| 在侧边栏的形态 | 官方工作区节点(原生展开/归档/搜索/拖拽) | 依附既有工作区/会话体系 |
| 生命周期 | 可整体列出、清理、自动过期清理 | 跟随其宿主会话 |
| 是否冲突 | 不冲突:两者使用不同的插槽与服务,可同时启用 | — |
License
MIT