集中重命名、合并、重建和删除工作区,并跨区移动会话
29.8/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/11/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add QIUZAIYOU/dsh-workspace-control该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:仅索引
是什么
dsh 原生插件 · browser
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 3 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件
查看扫描证据(共 25 处,展示前 20 条)
- 写入本地文件index.js:96
appendFile(LOG_PATH, line, 'utf8').catch(() => {}); - 读取本地文件index.js:100
fsStat(LOG_PATH) - 写入本地文件index.js:103
return rename(LOG_PATH, `${LOG_PATH}.1`).catch(() => rm(LOG_PATH, { force: true })); - 删除本地文件index.js:103
return rename(LOG_PATH, `${LOG_PATH}.1`).catch(() => rm(LOG_PATH, { force: true })); - 读取本地文件index.js:116
const raw = await readFile(PENDING_PATH, 'utf8'); - 删除本地文件index.js:131
await rm(PENDING_PATH, { force: true }); - 写入本地文件index.js:134
await writeFile(PENDING_PATH, `${JSON.stringify(entries, null, 2)}\n`, 'utf8'); - 读取本地文件index.js:264
if ((await fsStat(candidate)).isDirectory()) existing.push(candidate); - 读取本地文件index.js:288
projects = await readdir(root, { withFileTypes: true }); - 读取本地文件index.js:297
entries = await readdir(directory, { withFileTypes: true }); - 删除本地文件递归index.js:312
await rm(directory, { recursive: true, force: true }); - 读取本地文件index.js:314
const rest = await readdir(project); - 删除本地文件index.js:315
if (rest.length === 0) await rmdir(project); - 写入本地文件index.js:712
await writeFile(script, `${lines.join('\r\n')}\r\n`, 'utf8'); - 删除本地文件index.js:747
await rm(launcher, { force: true }).catch(() => {}); - 写入本地文件index.js:766
await writeFile(target, body, 'utf8'); - 执行外部命令来自配置/变量index.js:772
child = spawn(command, args, { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true }); - 执行外部命令固定命令index.js:819
const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], { - 写入本地文件index.js:2001
await rename(sessionDirectory, parkedDirectory); - 删除本地文件递归index.js:2010
await rm(sessionDirectory, { recursive: true, force: true });
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
DeepSeek Harness 的工作区管理:在侧边栏面板里移动、重建、合并、重命名和删除工作区。
README
由 DeepSeek 最新模型翻译生成dsh-workspace-control
DeepSeek Harness 插件,用来做工作区层面的维护:重命名、删除、重建到另一个目录、合并,以及在多个工作区之间移动单个会话。
一个面板管好 DeepSeek Harness 的工作区:重命名、删除、重建到另一个目录、合并,以及在多个工作区之间移动单个会话——不丢任何东西。会话的工作目录写死在存储的头部里,也写死在会话存储的目录名里,所以界面在创建之后改不了;这个插件通过官方支持的持久化接缝来完成这件事。
它能做什么
面板放在独立的中央视图里,从侧边栏面板行的图标打开(工作区管理 / Workspaces):
| 操作 | 会发生什么 |
| 重建… / Rebuild… | 为整个工作区挑一个新目录。它持有的每个会话都跟着搬过去,新路径上的记录继承标题和侧边栏位置,旧记录一旦清空就删除。 |
| 合并… / Merge… | 挑其他工作区。它们的会话搬进这个工作区的目录,来源记录一旦什么都不剩就丢弃。 |
| 重命名 / Rename | 只改侧边栏标题。目录不动。 |
| 删除 / Delete | 删掉工作区记录。它的会话先归档,目录和里面所有会话文件留在磁盘上——之后还能在同一路径把工作区加回来。
展开完整文档(README 剩余 1 节)
dsh-workspace-control
DeepSeek Harness 插件,用来做工作区层面的维护:重命名、删除、重建到另一个目录、合并,以及在多个工作区之间移动单个会话。
一个面板管好 DeepSeek Harness 的工作区:重命名、删除、重建到另一个目录、合并,以及在多个工作区之间移动单个会话——不丢任何东西。会话的工作目录写死在存储的头部里,也写死在会话存储的目录名里,所以界面在创建之后改不了;这个插件通过官方支持的持久化接缝来完成这件事。
它能做什么
面板放在独立的中央视图里,从侧边栏面板行的图标打开(工作区管理 / Workspaces):
| 操作 | 会发生什么 |
| 重建… / Rebuild… | 为整个工作区挑一个新目录。它持有的每个会话都跟着搬过去,新路径上的记录继承标题和侧边栏位置,旧记录一旦清空就删除。 |
| 合并… / Merge… | 挑其他工作区。它们的会话搬进这个工作区的目录,来源记录一旦什么都不剩就丢弃。 |
| 重命名 / Rename | 只改侧边栏标题。目录不动。 |
| 删除 / Delete | 删掉工作区记录。它的会话先归档,目录和里面所有会话文件留在磁盘上——之后还能在同一路径把工作区加回来。 |
每一行还会在点任何按钮之前报告:
- 目录里实际有多少个会话;
- 占用 N / N held —— 其中有多少个现在搬不了,因为有一轮正在运行(running),或者因为应用还占着会话日志(open,这会让存储拒绝发布后继产物)。这类行上的重建、合并、删除保持禁用,被占用的会话会在行里点名,所以拒绝是提前可见的,而不是事后才冒出来。
单个会话仍然有自己的入口:会话行 ... 菜单里的 移动到工作区… / Move to workspace… 只把那个会话移到某个目录。如果目标已经是工作区,会话就并进去;如果不是,就创建工作区。
移动会保留会话 id、标题、每一个已提交事件(历史、工具调用、审批)和格式版本,然后把会话重新归到拥有目标目录的工作区下。如果那个工作区还不存在,就创建它。
移动是怎么做的
会话存储是只追加的,拒绝改写历史,所以移动是通过官方支持的持久化接缝生成一个后继产物,而不是改字节:
1. sessionPersistence.stat(id) —— 读头部(id、版本、cwd、血缘)。
2. sessionPersistence.open(id, 'read') —— 复制每一个已提交事件。读句柄从不取得写所有权,所以界面开着会话时也能这么做。
3. 把旧会话目录挪到一边(同卷原子重命名),让 id 在后继产物认领它之前先离开存储。
4. sessionPersistence.create({ ...header, cwd }, { inheritedEventCount }) +
append(events) + flush() —— 后端按当前格式写入正确的 header 帧,其中带上新的 cwd。
5. 回读 stat(id),核对存下来的 cwd;然后更新工作区注册表:resolveByPath(target) ?? create(target)、attachSession(id),并在旧工作区上执行 detachSession(id)。
失败处理:如果第 4 步失败,就把暂存目录改回原名,结果里说明这次移动已回滚。重建时,会把已经移动过的每个会话都退回旧目录,并删掉它在目标位置创建的记录。
有两种不同状态会在开始前就被拒绝,区别很关键:
- running —— 有回合正在执行。活着的智能体仍把旧目录当作进程 cwd 和权限根,此时在它脚下移动目录是错的。判断信号是会话日志里的回合边界(turn/start 之后没有 turn/end),不是 agents.get(id):应用只是打开着的空闲会话同样有活着的智能体,所以侧边栏可能显示「空闲」,而天真的检查却报 "running"。
- session-open —— 会话在应用里打开着,日志在内存中,store 也占着这个 id。此时 create 会拒绝后继者("already exists"),所以不关掉标签页,移动就不可能成功;在这里直接拒绝,能点明真正的原因,而不是让重建做到一半才失败。
还有两道防护,避免半途而废的移动吃掉历史:
- 只有 header、没有已提交事件的会话会被拒绝,因为 create 什么都不写,只有第一次 append 才会发布 —— 移动它只会删掉原件,却产生不了后继者。
- 如果目录暂存之后 store 仍然报出这个 id(基于索引的解析),就恢复暂存副本,移动结果报 store-cached,而不是抛出一个误导性的写入错误。
移动完成后,注册表在同一进程内完成收尾。WorkspaceRegistry 在 [Service.init] 里一次性建好规范 cwd 的 header 索引,attachSession 依据该索引做校验(readSessionHeader 优先读活的 Session,其次读缓存的 header,两者都不存在时才重新读 store)。因此插件在 attach 之前,先删掉被移动会话的过期索引项 —— 这些字段就是服务实例上的普通 Map —— 这样下一次校验会从 store 重新读取重写后的 header。这就是重启后会得到的状态,只是不用真的重启。随后 Client 调用 sessions.refreshProjections(id) 重建派生视图,其他仍列出该会话的工作区会被解绑。
如果 attach 仍被拒绝(比如会话在标签页里打开着,活的 Session 仍返回旧 header),这次移动会记为待处理,并在下次启动时自动收尾。
工作区归属会校验,也可修复
attachSession 以规范 cwd 索引为准,该索引由 harness 在启动。如果某个会话的头部在本次运行期间被改写,它可能被拒绝,直到 harness 重启。Host 从不把这种情况报告成普通的成功:
- moved —— 头部已改写,回读匹配,工作区已接收该会话。
- moved-unattached —— 文件已移动,但工作区还没接收。
- attached —— 会话本来就在目标目录里(早先的移动把成员关系落下了);本次运行只补完了成员关系。
重复执行同一次移动就是修复动作:存储的 cwd 已经等于目标时,插件跳过文件操作,只处理工作区成员关系,所以一次没做完的移动,用发起它的同一条命令就能收尾。workspace-unattached 表示连这一步也失败了;重启 DSH,等索引刷新后再重复一次。
未挂接的情况在启动时也会自愈:Host 把待处理的 (sessionId, target) 对记录在 .remaker-pending.json 里,下次启动两秒后——注册表已重建索引——它补完成员关系并清掉该条目。所以文件已到位、成员关系没到位的移动,只需要重启一次,不用再点第二次。
这两半也都不会拖垮应用启动:Client 工厂抛异常时降级为插件缺席,而不是让 Web 启动关卡失败(那会让整个应用以 1 entry did not activate 失败);Host 的 apply 抛异常时记到 .workspace-control.log,而不是让条目失败。
工作区标题跟随目录,除非你改过名
create(path) 从路径最后一段推导标题,所以仍等于自身 basename 的标题从来不是用户输入的:重建会把这类标题带到新目录名上(ce 重建到 D:\Github\workspace test 后变成 workspace test)。与 basename 不同的标题视为有意改名,原样保留。
早期构建创建的记录忽略了这条规则,保留了重建前的标题。管理器里的工作区行针对这种形态提供一键修复——只有存储的标题与文件夹名不同时按钮才出现——它调用 retitleWorkspace(workspaceId, title),用的是受支持的 Workspace#setTitle,无需重启。
从面板重启 DSH
应用打开的会话由存储持有:它的写句柄属于加载它的智能体,没有 API 能交还。所以移动(或重建拥有它的工作区)在应用放手之前会拒绝,所以释放一切的唯一可靠方式是退出并重新启动 DSH。
因此面板上有 Restart DSH 按钮,这一步不必再碰托盘图标:
- 桌面版把该 host 作为 Electron 子进程运行,窗口属于父进程,没有 API 跨越这条边界。按钮把一个小 .cmd 辅助脚本写到临时目录,先应答 Remote。
helper 先等一会儿,停掉应用,等这个进程(持有监听套接字的那个)消失,再重新启动可执行文件,然后删掉自己。如果那个进程不肯退出,helper 就直接退出,不会启动第二个实例。
- helper 通过 WMI 服务商(Win32_Process.Create)创建,不是作为这个进程的子进程:普通子进程会和它即将重启的应用一起死掉。启动器以隐藏方式运行,所以不会弹出控制台窗口。
- WMI 调用由 cscript 发出,而不是 PowerShell:两者都隐藏运行,也都让 helper 留在本进程树之外,但 PowerShell 启动要 ~1.2 s,cscript 只要 ~0.1 s。VBScript 不可用时才回退到 PowerShell。在这台机器上实测点击到重启:≈1.2 s,而第一版实现(.ps1 helper 经 PowerShell 启动,固定等两秒和三秒)是 ≈8.7 s。
- 加载进 Electron 主进程的插件(浏览器构建)改用文档里的 app.relaunch() 路径。
- 其他任何地方,该能力都会报告 restart-unsupported,按钮保持禁用;不做任何猜测。
- 重启会打断进行中的回合,所以面板先统计这些回合,确认后才继续。
安装
从 npm 安装 —— 包名、仓库名和插件入口都用同一个名字 dsh-workspace-control:
dsh plugin --profile web add dsh-workspace-control
或者从本地检出安装:
plugin_manager install_bundle target=D:\Github\dsh-workspace-control
然后确认加载器记录存在:
cordis_inspect_query Config.listConfigs name=dsh-workspace-control
替换旧安装:这个插件曾以 dsh-workspace-remaker 发布,后来又改名为 dsh-workspace-manager,所以把列出的那个删掉:plugin_manager remove_bundle target=dsh-workspace-manager。
面板报告的活动与侧边栏一致
只有当会话确实处于回合中时,工作区行才显示 N running,而这个答案来自内置侧边栏所用的同样两个来源:
useSessionStatus().get(sessionId)?.running ?? useSessions().byId[sessionId]?.running
应用只是保持打开的会话显示为 N open · idle,其按钮保持可用,宿主在动任何文件之前会先释放它。两个来源都不认识该会话时,面板回退到宿主自己的摘要,而不是编一个答案。
面板
面板是卡片网格,不是表格:每个工作区是一个 article.wsm-card,带状态点、名称、画布路径、活动徽标和四个操作(重建 / 合并 / 重命名 / 删除)。只有会话运行中时操作才会禁用 —— 只是打开的工作区仍可操作,因为空闲持有者会在移动前被释放。
样式跟随宿主主题,而不是硬编码颜色,所以面板跟随 Harness 的浅色/深色切换:
.wsm-card {css
border: .5px solid var(--dsw-alias-settings-card-stroke);
border-radius: var(--dsw-radius-xl);
background: var(--dsw-alias-settings-card-fill);
}
.wsm-card:hover { background: var(--dsw-alias-interactive-bg-hover); }
首次读取还在进行时,网格会显示三个脉冲骨架屏,面板因此不会闪出空白状态。prefers-reduced-motion 会关掉脉冲动画。
测试 bash
node --test
测试套件不依赖 harness 运行:test/fakes.js 顶替存储、工作区注册表、sessionController 和智能体注册表,把真实文件发布到一个临时 DSH_HOME 里。
- paths.test.js —— samePath / titleFollowsPath / encodeId 的边界情况。
- move.test.js —— 完整的移动事务:成功、会话被占用、有一轮正在执行、日志为空、发布失败、后继读回偏短、存储仍列出已停放 id,以及重复移动时的修复路径。
- contract.test.js —— 在 vm 里加载 client.js,断言 Client 挂载的描述符与 Host 注册的完全一致(id、命名空间、参数接线),并断言两个语言字典携带相同的键。
说明与限制
- 移动会话不会移动磁盘上的文件。只有会话记录的工作目录会变。
- 当前 DSH 版本允许嵌套工作区,所以嵌套不会被拒绝;只有无法承载工作区的目标才会被拒绝(不存在、不是目录、不可写)。
- 旧格式存储的会话会报 migration-required;先打开一次让 harness 升级,再移动。
- 分叉会话保留它的 parentSession 链接和继承的事件偏移;父会话留在原地,所以想把整棵树放进一个目录,要先移动父会话。
- 会话运行期间拒绝移动。先停止,再移动,然后重新打开。
- 应用已打开但空闲的会话过去会被拒绝:存储保留它的写所有权,因此无法为它发布后继产物。插件现在会先释放空闲持有者(#releaseHeld 销毁智能体自己的 Cordis 作用域,并重读注册表确认),只在释放没有发生时拒绝。会话执行到一半仍会被拒绝:它还有真正的工作要完成。
- 每个席位都从 listWorkspaces 绘制相同的占用标记,所以按下按钮之前就能看到被阻塞的工作区。
- 删除 / Delete 只删除记录;目录及其会话文件留在磁盘上,会话会被归档,方便再次找到。
- 插件从不修改已提交的字节:它通过 sessionPersistence 发布后继,存储的禁止重写规则和分帧检查因此保持完好。
验证 bash
1. the loader row exists and the bundle applied
cordis_inspect_query Config.listConfigs name=dsh-workspace-control
2. the Client half registered its three seats (live Slots tree)
cordis_inspect_query Slots.listSubTree root=sidebar.panellist
cordis_inspect_query Slots.listSubTree root=main
cordis_inspect_query Slots.listSubTree root=sidebar.workspaces.session.menu.item
sidebar.panellist 和 main 都带 id workspace-control;前者是侧边栏按钮,后者是它派发的面板。同一个面板也能从 settings.section 进入(id 为 workspace-control-settings)。
已完成的结果会在几秒后自动退场,所以操作成功不会在面板里留下横幅。
离线检查(不需要 harness)覆盖拒绝路径、inspect,以及针对假注册表的启动自愈:
node .scratch/smoke.mjs
智能体也能通过 workspace_control 工具触达同一个操作,所以不用 UI 就能演练移动:
workspace_control { "action": "inspect", "sessionId": "session-<uuid>" }
workspace_control { "action": "move", "sessionId": "session-<uuid>", "target": "D:\\target" }
action=listWorkspaces 会报告每个工作区,带上真实会话数和阻塞项(running / open),面板绘制占用标签用的就是这份数据。action=inspect 只读,报告记录的目录、所属工作区、会话是否在运行,以及存储格式。action=move 执行移动。每一步都会往本包旁边的 .workspace-control.log 追加一行,移动被拒时先看这里。
缓存的标题跟随头部
会话列表用 sessionProjectionCache.cachedSnapshot(header) 构建投影列,这次读取只匹配生命周期标识(formatVersion、createdAt、cwd、isSeeded)与传入头部相等的存储记录。移动会重写头部里的 cwd,于是移动前写入的记录不再匹配,侧边栏就把这一行渲染成「未命名」,尽管缓存的标题行完好无损。
所以移动结束时要做一次冷投影读取,基于完整日志(sessionProjectionCache.coldSnapshot(header, inheritedEventCount, events)),把日志按新标识折叠,再写回一份新检查点。action=move 把结果报告为 projections: { asOfSeq, keys };把会话重复移动到它已所在的目录,只会重跑这一步修复。
不重启 harness 重载插件
宿主那一半通过插件管理器切换条目来重载——plugin_manager 的 set_plugin,target: "include:workspace-control",先 enabled: false 再 enabled: true——这会原地重新导入 index.js。客户端那一半随页面刷新。两者都不需要重启 DSH。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
QIUZAIYOU/dsh-workspace-control
集中重命名、合并、重建和删除工作区,并跨区移动会话
相关插件 · 浏览器
智能体插件框架deepseek-ai/deepseek-harness
DeepSeek Harnessdsh是由 DeepSeek AI 开发的开源 agent harness智能体框架。
★ 270,422已汉化DSH
71.9
多设备 Agent 看板YaoApp/yao
✨ 所有 Agent 和工作区集中管理,覆盖你的每一台设备。任务看板追踪进度,桌面端、移动端、浏览器、API…
★ 8,147已汉化DSH
71.2
可扩展侧边栏工作台omdsh-dev/DSH-better-sidebar
DSH 的双工作台:原生右侧栏 + 自绘底部面板,并把 ctx.betterSidebar 服务开放给所有插件
★ 4,761已汉化DSH
71.1
浏览器自动化桥Tencent/BrowserSkill
你的 Agent。你的浏览器。连接,就能协作。
★ 9,967已汉化DSH
71
AI 小说创作工作台EthanYoQ/AI-Novel-Writer
AI 小说作家 / AI Novel Writer
★ 1,591已汉化DSH
70.7
智能体浏览器引擎platonai/Browser4
DeepSeek Harness 集成
★ 1,175已汉化DSH
70.4
这是你的仓库?认领条目