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

远程工作区 yh4922/dsh-workspace

DeepSeek Harnessv0.12.4
yh49224 天前有提交已提供中文文档扫描:含敏感能力

DeepSeek HarnessDSH的远程工作区插件:在 DSH 里管理 SSH…

40.6/100
综合分
13
GitHub 星标
403
npm 周下载
—/5
用户评分

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @yh4922/dsh-workspace
npm 源 · @yh4922/dsh-workspace 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · vision
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 4 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@yh4922/dsh-workspace @ 0.12.4
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/28 14:38:44

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

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

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 225 处,展示前 133 条)
  • 写入本地文件
    lib/index.js:101
    rename(tmp, target);
  • 删除本地文件
    lib/index.js:110
    rmSync(tmp, { force: true });
  • 读取本地文件
    lib/index.js:277
    if (!existsSync(file)) {
  • 读取本地文件
    lib/index.js:283
    const parsed = JSON.parse(readFileSync(file, "utf8"));
  • 写入本地文件
    lib/index.js:299
    mkdirSync(path.dirname(file), {
  • 写入本地文件
    lib/index.js:304
    writeFileSync(tmp, JSON.stringify(this.data, null, 2), {
  • 写入本地文件
    lib/index.js:813
    mkdirSync(pluginRoot(), {
  • 读取本地文件
    lib/index.js:1004
    if (existsSync(file)) try {
  • 读取本地文件
    lib/index.js:1005
    const parsed = JSON.parse(readFileSync(file, "utf8"));
  • 写入本地文件
    lib/index.js:1020
    mkdirSync(path.dirname(file), {
  • 写入本地文件
    lib/index.js:1025
    writeFileSync(tmp, JSON.stringify(this.data, null, 2), {
  • 读取本地文件
    lib/index.js:1205
    config.privateKey = readFileSync(expandHome(auth.keyPath));
  • 发起网络请求
    lib/index.js:1252
    const request = http.request({
  • 读取本地文件
    lib/index.js:2623
    realRoot = await realpath(input.root);
  • 删除本地文件
    lib/index.js:3726
    async rm(abs) {
  • 执行外部命令来自配置/变量
    lib/index.js:5509
    const child = spawn(file, args, {
  • 删除本地文件
    lib/index.js:7285
    rmSync(file, { force: true });
  • 执行外部命令来自配置/变量
    lib/index.js:9915
    execFile(powershellPath(), [
  • 删除本地文件
    lib/index.js:9994
    rmSync(this.file(), { force: true });
  • 删除本地文件
    lib/index.js:10299
    await rm(part, { force: true });
  • 删除本地文件
    lib/index.js:10433
    for (const old of this.uploads.values()) await rm(old.file, { force: true });
  • 发起网络请求
    lib/index.js:11067
    this.request({
  • 发起网络请求
    lib/index.js:11090
    this.request(next);
  • 发起网络请求
    lib/index.js:11277
    const started = this.summarizer.request({
  • 发起网络请求
    lib/index.js:13728
    if (mem.store.getSettings().enabled && summary !== void 0 && summary !== "") mem.summarizer.request({
  • 读取本地文件
    src/agent/preimages.ts:44
    if (!existsSync(index)) return []
  • 读取本地文件
    src/agent/preimages.ts:46
    const parsed = JSON.parse(readFileSync(index, 'utf8')) as { entries?: PreimageEntry[] }
  • 写入本地文件
    src/agent/preimages.ts:66
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    src/agent/preimages.ts:72
    if (!existsSync(blob)) writeFileSync(blob, content)
  • 写入本地文件
    src/agent/preimages.ts:72
    if (!existsSync(blob)) writeFileSync(blob, content)
  • 写入本地文件
    src/agent/preimages.ts:87
    writeFileSync(tmp, JSON.stringify({ version: 1, entries }, null, 2))
  • 读取本地文件
    src/agent/preimages.ts:97
    return existsSync(blob) ? readFileSync(blob) : undefined
  • 读取本地文件
    src/agent/preimages.ts:97
    return existsSync(blob) ? readFileSync(blob) : undefined
  • 读取本地文件
    src/agent/present-tool.ts:99
    const localExists = options.localExists ?? ((p: string) => existsSync(p))
  • 读取本地文件
    src/assets-route.ts:78
    if (type === undefined || !existsSync(file) || !statSync(file).isFile()) {
  • 读取本地文件
    src/assets-route.ts:78
    if (type === undefined || !existsSync(file) || !statSync(file).isFile()) {
  • 读取本地文件
    src/assets-route.ts:85
    const useGz = acceptsGzip && existsSync(gzFile)
  • 读取本地文件
    src/assets-route.ts:87
    const stat = statSync(served)
  • 读取本地文件
    src/assets-route.ts:109
    const stream = createReadStream(served)
  • 删除本地文件
    src/fs-atomic.ts:40
    rmSync(tmp, { force: true })
  • 读取本地文件
    src/gateway.ts:577
    const stat = statSync(file)
  • 读取本地文件
    src/gateway.ts:581
    const fresh = { text: readFileSync(file, 'utf8'), version }
  • 读取本地文件
    src/local/browse.ts:72
    const dirents = await readdir(dir, { withFileTypes: true })
  • 写入本地文件
    src/local/browse.ts:107
    await mkdir(target)
  • 读取本地文件
    src/local/browse.ts:117
    return (await stat(p)).isDirectory()
  • 读取本地文件
    src/local/browse.ts:133
    const ok = access(root).then(
  • 执行外部命令固定命令
    src/local/local-git.ts:27
    child = spawn('git', [...GIT_GLOBAL_ARGS, ...argv], { cwd, env, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
  • 读取本地文件
    src/local/local-git.ts:98
    const real = await realpath(file)
  • 读取本地文件
    src/local/local-git.ts:99
    if (!isInside(await realpath(root), real)) return { stdout: '', code: 1, truncated: false }
  • 读取本地文件
    src/local/local-git.ts:114
    return await readFile(abs, 'utf8')
  • 读取本地文件
    src/local/local-git.ts:121
    await access(abs)
  • 写入本地文件
    src/local/local-git.ts:128
    await writeFile(abs, text, 'utf8')
  • 写入本地文件
    src/local/local-git.ts:131
    await mkdir(abs, { recursive: true })
  • 删除本地文件
    src/local/local-git.ts:133
    async rm(abs) {
  • 删除本地文件
    src/local/local-git.ts:134
    for (const p of abs) await rm(p, { force: true })
  • 发起网络请求
    src/memory/mount.ts:180
    mem.summarizer.request({ sessionId: sid, mode: 'compaction', compactionSummary: summary })
  • 发起网络请求
    src/memory/runtime.ts:86
    const started = this.summarizer.request({ sessionId: sid, mode: 'manual' })
  • 读取本地文件
    src/memory/scope.ts:96
    resolved = await realpath(cwd)
  • 读取本地文件
    src/memory/store.ts:150
    if (known !== undefined && existsSync(known)) return known
  • 读取本地文件
    src/memory/store.ts:154
    if (existsSync(preferred)) {
  • 读取本地文件
    src/memory/store.ts:160
    if (existsSync(candidate)) {
  • 读取本地文件
    src/memory/store.ts:176
    if (!existsSync(dir)) return []
  • 读取本地文件
    src/memory/store.ts:178
    return readdirSync(dir, { withFileTypes: true })
  • 读取本地文件
    src/memory/store.ts:216
    return existsSync(this.sessionFile(sessionId))
  • 删除本地文件
    src/memory/store.ts:544
    rmSync(file, { force: true })
  • 写入本地文件
    src/memory/store.ts:578
    mkdirSync(path.dirname(file), { recursive: true })
  • 写入本地文件
    src/memory/store.ts:579
    appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8')
  • 写入本地文件
    src/memory/store.ts:603
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/memory/store.ts:605
    writeFileSync(tmp, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 发起网络请求
    src/memory/summarizer.ts:269
    this.request({ sessionId, mode: 'turns', ...(turn !== undefined ? { turn } : {}) })
  • 发起网络请求
    src/memory/summarizer.ts:289
    this.request(next)
  • 执行外部命令来自配置/变量
    src/open-external.ts:29
    const child = spawn(file, args, { detached: true, stdio: 'ignore', windowsHide: true, shell: false })
  • 读取本地文件
    src/prefs.ts:51
    if (!existsSync(file)) return (this.data = { ...DEFAULTS })
  • 读取本地文件
    src/prefs.ts:53
    const parsed = JSON.parse(readFileSync(file, 'utf8')) as Partial<Prefs>
  • 写入本地文件
    src/prefs.ts:87
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/prefs.ts:89
    writeFileSync(tmp, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 读取本地文件
    src/runtime.ts:222
    const root = await realpath(cwd)
  • 读取本地文件
    src/sftp/archive.ts:41
    lstat(p: string): Promise<ArchiveStat>
  • 读取本地文件
    src/sftp/archive.ts:42
    readdir(p: string): Promise<string[]>
  • 读取本地文件
    src/sftp/archive.ts:161
    async lstat(p) {
  • 读取本地文件
    src/sftp/archive.ts:166
    async readdir(p) {
  • 读取本地文件
    src/sftp/archive.ts:188
    async lstat(p) {
  • 读取本地文件
    src/sftp/archive.ts:189
    const s = await lstat(p)
  • 读取本地文件
    src/ssh/connection.ts:215
    config.privateKey = readFileSync(expandHome(auth.keyPath))
  • 发起网络请求
    src/ssh/connection.ts:266
    const request = http.request({
  • 读取本地文件
    src/ssh/hostkey.ts:55
    if (existsSync(file)) {
  • 读取本地文件
    src/ssh/hostkey.ts:57
    const parsed = JSON.parse(readFileSync(file, 'utf8')) as KnownHostsShape
  • 写入本地文件
    src/ssh/hostkey.ts:70
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/ssh/hostkey.ts:72
    writeFileSync(tmp, JSON.stringify(this.data, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 写入本地文件
    src/terminal/registry.ts:330
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/terminal/registry.ts:332
    writeFileSync(tmp, JSON.stringify({ version: 1, terminals: entries }, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 读取本地文件
    src/terminal/registry.ts:342
    if (file === undefined || !existsSync(file)) return []
  • 读取本地文件
    src/terminal/registry.ts:344
    const parsed = JSON.parse(readFileSync(file, 'utf8')) as { terminals?: unknown }
  • 写入本地文件
    src/update/http.ts:74
    await mkdir(deps.dir, { recursive: true })
  • 写入本地文件
    src/update/http.ts:85
    await pipeline(req, limit, createWriteStream(temp))
  • 删除本地文件
    src/update/http.ts:87
    await rm(temp, { force: true })
  • 写入本地文件
    src/update/source.ts:195
    const out = createWriteStream(part)
  • 写入本地文件
    src/update/source.ts:219
    await rename(part, dest)
  • 删除本地文件
    src/update/source.ts:222
    await rm(part, { force: true })
  • 读取本地文件
    src/update/tarball.ts:85
    const data = await readFile(file)
  • 读取本地文件
    src/update/updater.ts:95
    const pkg = JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
  • 读取本地文件
    src/update/updater.ts:120
    const onDisk = existsSync(this.deps.packageFile) ? readVersion(this.deps.packageFile) : this.current
  • 写入本地文件
    src/update/updater.ts:174
    await mkdir(this.deps.dir, { recursive: true })
  • 写入本地文件
    src/update/updater.ts:194
    await mkdir(this.deps.dir, { recursive: true })
  • 写入本地文件
    src/update/updater.ts:196
    await rename(tempFile, file)
  • 删除本地文件
    src/update/updater.ts:199
    for (const old of this.uploads.values()) await rm(old.file, { force: true })
  • 删除本地文件
    src/update/updater.ts:208
    await rm(tempFile, { force: true })
  • 读取本地文件
    src/update/updater.ts:256
    const names = (await readdir(this.deps.dir)).filter((n) => n.endsWith('.tgz'))
  • 读取本地文件
    src/update/updater.ts:259
    return { full, mtime: (await stat(full)).mtimeMs }
  • 删除本地文件
    src/update/updater.ts:265
    await rm(f.full, { force: true })
  • 执行外部命令来自配置/变量
    src/vault/auto-unlock.ts:39
    const child = execFile(
  • 读取本地文件
    src/vault/auto-unlock.ts:110
    return existsSync(this.file())
  • 写入本地文件
    src/vault/auto-unlock.ts:117
    mkdirSync(path.dirname(file), { recursive: true })
  • 写入本地文件
    src/vault/auto-unlock.ts:119
    writeFileSync(tmp, JSON.stringify(shape), { encoding: 'utf8', mode: 0o600 })
  • 写入本地文件
    src/vault/auto-unlock.ts:122
    chmodSync(file, 0o600)
  • 读取本地文件
    src/vault/auto-unlock.ts:131
    if (!existsSync(file)) return undefined
  • 读取本地文件
    src/vault/auto-unlock.ts:132
    const shape = JSON.parse(readFileSync(file, 'utf8')) as Partial<SavedShape>
  • 删除本地文件
    src/vault/auto-unlock.ts:139
    rmSync(this.file(), { force: true })
  • 读取本地文件
    src/vault/store.ts:136
    if (!existsSync(file)) {
  • 读取本地文件
    src/vault/store.ts:142
    const parsed = JSON.parse(readFileSync(file, 'utf8')) as Partial<VaultFileShape>
  • 写入本地文件
    src/vault/store.ts:162
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/vault/store.ts:166
    writeFileSync(tmp, JSON.stringify(this.data, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 写入本地文件
    src/vault/store.ts:751
    mkdirSync(pluginRoot(), { recursive: true, mode: 0o700 })
  • 读取本地文件
    src/workspace/bindings.ts:128
    if (existsSync(file)) {
  • 读取本地文件
    src/workspace/bindings.ts:130
    const parsed = JSON.parse(readFileSync(file, 'utf8')) as { bindings?: RemoteBinding[] }
  • 写入本地文件
    src/workspace/bindings.ts:146
    mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/workspace/bindings.ts:148
    writeFileSync(tmp, JSON.stringify({ version: 1, bindings: [...this.load().values()] }, null, 2), { encoding: 'utf8', mode: 0o600 })
  • 读取本地文件
    src/workspace/bindings.ts:179
    if (!existsSync(metaPath)) return undefined
  • 读取本地文件
    src/workspace/bindings.ts:181
    const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as Partial<MetaFile>
  • 读取本地文件
    src/workspace/bindings.ts:217
    existsSync(b.localPath) &&
  • 读取本地文件
    src/workspace/bindings.ts:245
    if (!existsSync(candidate)) return undefined
  • 写入本地文件
    src/workspace/bindings.ts:324
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    src/workspace/bindings.ts:341
    writeFileSync(path.join(dir, WORKSPACE_META_FILE), JSON.stringify(meta, null, 2), 'utf8')
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

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

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-attachment@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-fs@deepseek-ai/dsh-fs-observation-policy@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-http-proxy@deepseek-ai/dsh-output-retention@deepseek-ai/dsh-sandbox
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 管理SSH主机
  • 打开远程终端
  • 浏览编辑远程文件
v0.12.42026-10-26
· 兼容 DSH 0.2.1 · 修复:DSH 0.2.1 起宿主不再提供 webRuntime 服务(信任列表移到 connection 服务),插件此前把它当作 HTTP 路由的硬依赖,新内核下上传离线包、文件上传 / 下载、终端 WebSocket、编辑器资源与 HTML 预览路由全部没有挂载,宿主对这些请求一律回 405(「上传失败(HTTP405)」)。现在只等 webServer,信任列表每次请求从 webRuntime 或 connection 现取,都取不到时只放行本机回环。 · 作用域记忆(新增,默认关闭) · 三层记忆:会话(固定 7 个分节:目标 / 约束与用户纠正 / 已定决策 / 进度 / 关键事实 / 已排除方案 / 待确认问题)、项目(按宿主登记的工作区,子目录会话归入最近的已登记祖先)、全局。本机与远程会话都生效,数据存本机 ~/.dsh/workspaces/memory/。 · 注入走系统提示词片段(不进对话历史,文本稳定时命中提示词缓存):全局 + 项目记忆即时生效;会话记忆只在新开 / 恢复 / 压缩后刷新,保证压缩后仍在。项目记忆可限定 git 分支(读不到分支时不注入)。子代理不注入。 · 后台总结:每 N 轮(默认 8;旧设置里的默认值 3 自动迁移为 8)用独立的模型调用整理会话记忆,并为项目 / 全局提出待选记忆。门槛从严:只自动记录工作流(编译 / 运行 / 发布等固定步骤)、依赖约束、用户要求与纠正、关键决策、任务目标与进度、反复用到的关键事实;每次最多新增 2 条会话记忆、1 条待选,会话记忆生效满 15 条后只合并 / 清理不再新增;对话被压缩后自动对照压缩摘要补漏;会话记忆 tab 可手动「重新整理」。模型默认沿用会话模型,可在设置里指定。 · 助手工具 scoped_memory(add / update / disable / list / search,无删除):会话层直接写;项目 / 全局只生成待选记忆;搜索只限当前项目与全局。可在设置里关闭。 · 远程工作区页新增「记忆」页签:总开关与设置、全局 / 项目 / 会话树、单条新增 / 编辑 / 启用禁用 / 置顶 / 重要程度 / 删除 / 修改历史与回退、待审批(通过 / 拒绝 / 修改后通过 / 改层级 / 全部通过 / 全部拒绝)、最近删除(30 天可恢复)、关键词搜索、清理已不存在会话的记忆。 · 会话顶部新增「记忆」标签(启用时出现,带待审批数):本会话的会话记忆、待选记忆、本项目记忆、全局记忆;会话条目可一键提升到项目 / 全局。分叉的会话继承父会话当时的会话记忆。 · 标签上的待审批数与「待审核」列只算当前会话所在工作区相关的条目(指向本项目的、本项目会话提出的全局候选;不属于任何项目的会话只算自己提出的),「全部通过 / 拒绝」也只作用于这一批。数字随切换会话、页面数据刷新即时更新(宿主不会自己重读标签文字,插件变化时主动触发一次插槽刷新),后台每 5 秒校准一次。 · 远程工作区 · 新增:主机配置「远程会话保留本机 shell」(默认关闭,在「添加 / 编辑主机」的「高级」里)。接管远程工作区时 pwsh / powershell / cmd 默认从工具清单里去掉;对某台主机打开后,该主机的远程会话保留它们,用于偶尔需要本机环境的场合(对比两边环境、用本机工具访问服务器上的服务等)。这些命令在本机执行、看不到远程文件,仅 Windows 本机有效;表单有橙色警示,开关变化与会话挂载都按 warn 级别写日志,系统提示词会告诉模型这些 shell 跑在本机。保存后立即生效:已打开的会话不用重建,下一轮对话起按新设置保留或隐藏(关闭即收回)。主机字段 allowLocalShell。 · 修复(随上一条):「AI 改动」页签在远程会话里不再把本机 shell 的删除命令误记成远程文件被删除。 · 新增:主机配置「远程会话允许读写本机文件」(allowLocalFiles,默认关闭,在「添加 / 编辑主机」的「高级」里)。远程工作区接管内置 read / write / edit / glob / grep 后,工具的路径参数若是本机 Windows 绝对路径(盘符 C:\… / C:/… 或 UNC \\server\share),交还宿主内置实现在本机执行,用于偶尔需要在远程会话里读写本机配置、日志等文件;相对路径与 / 开头的路径仍作用于远程主机(glob / grep 的可选 path 同口径)。与「远程会话保留本机 shell」同族:明文存 vault(保险箱上锁也能读)、不参与分组继承,系统提示词会说明两种路径口径。本机分支走宿主内置实现,受会话沙箱策略约束(工作区写入模式下
v0.12.32026-10-12内容较长已截断
· SSH 连接 · 修复:远程工作区连接建立后,服务器侧断开连接(ECONNRESET)或保活超时会让整个 DSH 崩溃(宿主进程 exit 1,issue #6)。根因是 rawDial 握手成功后摘掉了 ssh2 Client 的 error 监听,已建立连接上的 error 无人接收成为未捕获异常。现在保留一个常驻监听只记 warn 日志(握手期错误仍由原逻辑记录并 reject,不重复记),断线回收与自动重连沿用连接池的 close / end 处理;跳板链每一跳同样受益。补回归测试 src/ssh/connection.test.ts。 · HTML 预览 · 修复:HTML 预览出问题时只留一片白、看不出原因。.html / .htm 预览页注入纯 ASCII 的预览探针(src/wire/preview-probe.ts,桌面版在 srcdoc 内联时注入,网页版由预览路由对带 ?probe=1 的主文档按 latin1 原字节插入、相对资源不受影响;页面自带 meta CSP、svg / xhtml 不注入),把页面脚本报错、资源加载失败、加载完成但正文为空、10 秒内未完成加载回报给查看器,显示在沙箱提示条下方;沙箱里访问 localStorage / cookie 被拒(SecurityError)时提示可临时解除沙箱,未加载时提示打开开发者工具(标题栏扳手菜单或 Ctrl+Shift+I)看 Console。 · 改进:预览 iframe 的渲染决策抽成纯函数 framePlan(src/client/viewers/html-frame.ts),键同时包含 nonce 与沙箱状态(解除 / 恢复沙箱、保存、刷新都重建浏览上下文),补回归测试(探针注入位置、沙箱切换换键、预览路由注入后长度与 CSP)。 · 修复:Windows 桌面版(兼容模式页面是 new Worker('worker.js')、fetch、动态 import 等运行时相对地址解析到 DSH 根目录(如 comlink 示例报 Failed to construct 'Worker')。根因是桌面版一律走 srcdoc,srcdoc 文档基址是父页面地址。现在加载方式由 frameLoadMode 决定:http(s) 页面上网页版两种模式、桌面版解除沙箱都让 iframe 直接加载预览路由(文档基址 = 文件所在目录,同源,Worker 可用);桌面版沙箱仍走 srcdoc 内联(不透明源的请求拿不到桌面渲染进程令牌,关闭「允许浏览器访问」时会被拒),非 http(s) 页面(dsh-app:// / file:)走 srcdoc;srcdoc 能拿到预览路由时注入 指向文件目录(尽力而为,页面自带 时不覆盖;签发失败不报错,照常内联)。 · 改进:探针提示分类(probeNotes):沙箱下报错含 origin 'null' / SecurityError / Worker / 内联模块导入失败 / localStorage / sessionStorage / cookie 时追加「该页面需要同源能力,请点击『临时解除沙箱』」;页面有自己的脚本但没有可见元素时改为中性说明「页面没有可见元素(内容可能由脚本弹窗或控制台输出)」,不再按故障提示(探针统计可见元素时排除 script / style 等,并回报脚本数)。沙箱 iframe 与预览路由 CSP sandbox 均含 allow-modals(alert / confirm / prompt 不再被静默忽略,仍无 allow-same-origin)。 · 右侧栏标签右键菜单与固定标签 · 新增:右侧栏标签右键菜单(宿主「关闭」之后,分隔线开头)增加「关闭左侧 / 关闭右侧 / 关闭其他 / 关闭全部」——只作用于该标签所在窗格、跳过固定标签,没有可关闭目标时禁用;以及「固定 / 取消固定」。文件类标签(插件文件查看器、宿主文档预览等 dsh-resource://file/... 地址)另有「复制路径」「在文件管理中显示」(文件不在当前会话工作区内或文件管理未开启时禁用;远程工作区会话不提供「在文件管理中显示」,只保留「复制路径」)。对所有停靠窗格标签生效(含宿主文件 / 浏览器 / 终端)。 · 新增:固定标签。固定后排在所在窗格最左(多个按固定先后),不显示关闭按钮(只能右键关闭);以任何方式关闭固定标签即取消固定。打开侧边栏(或切到已展开的会话)时自动补开缺失的固定标签到记住的窗格(不存在则最左窗格;窗格只有引导页时顶替它),并恢复原来的激活标签;分屏下同一内容已在任意窗格 / 浮窗时不重复打开;拖到另一窗格后记住新窗格。终端、浏览器、SSH 终端等可
v0.12.22026-10-09
· 新增:Git 对比视图默认折叠未改动的部分(与 VS Code 一致,只显示改动及上下 3 行,折叠处可点开);标题栏「展开全部」开关可显示完整文件。 · 新增:代码编辑器右侧默认显示缩略图(minimap),底部状态栏的「缩略图」开关可关闭。两项开关都记在本机,并在所有打开的编辑器间同步。 · 安装 先安装并启用侧边栏插件 dsh-better-sidebar,再安装本插件(命令不带版本号,自动装最新版): dsh plugin --profile add @yh4922/dsh-workspace 也可以直接装本 Release 的安装包: 本版本的固定包:yh4922-dsh-workspace-0.12.2.tgz(需要锁定版本时用它)。 升级:先 dsh plugin --profile remove @yh4922/dsh-workspace,再用上面的命令安装——对同一个包再装一次时,插件管理器判断不出这次装的是哪个包(依赖值没有变化),会回滚并提示「无法从依赖变更中确定安装了哪一个包」。 从旧版本升级(包名变更):0.7.7 及更早的包名是 dsh-workspace,先 dsh plugin --profile remove dsh-workspace,再安装新包名。 DSH Desktop:在「插件管理」→「添加插件」里填入上面的包名或地址;升级同样是先移除、再添加(插件管理里没有「更新」按钮)。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-workspace Listed on dsh-plugin.org DeepSeek Harness(DSH)的远程工作区插件:在 DSH 里管理 SSH 主机,打开远程终端、浏览和编辑远程文件、查看远程 Git,并把远程目录添加为工作区,让智能体的读写和命令都作用在远程机器上。 功能 - 主机管理:多层分组、组默认值继承、跳板机链、SOCKS5 / HTTP 代理;首次连接记录主机指纹(TOFU),之后指纹变化会拦截 - 凭据保险箱:密码、私钥等敏感字段用主密码加密(AES-256-GCM + scrypt)后保存在本机;可选「在本机记住」自动解锁(Windows 用 DPAPI 保护) - SSH 终端:交互式终端,页面与侧边栏共用同一会话,断线自动重连 - 文件管理(原「远程文件」,本地与远程工作区通用):目录树、Monaco 编辑器(语法高亮、Ctrl+S 保存、冲突检测)、Markdown / HTML / 图片预览、右键菜单(新建 / 上传 / 下载 / 重命名 / 复制 / 粘贴 / 删除 / 复制路径);可接管 DSH 自带的「文件」侧栏 - Git 仓库(原「远程 Git」,本地与远程工作区通用):改动(暂存 / 放弃 / 提交)、diff、带分叉图的提交历史;可查看任意分支,本地分支不检出也能直接修改和提交
展开完整文档(README 剩余 1 节)
dsh-workspace

Listed on dsh-plugin.org

DeepSeek Harness(DSH)的远程工作区插件:在 DSH 里管理 SSH 主机,打开远程终端、浏览和编辑远程文件、查看远程 Git,并把远程目录添加为工作区,让智能体的读写和命令都作用在远程机器上。

功能

- 主机管理:多层分组、组默认值继承、跳板机链、SOCKS5 / HTTP 代理;首次连接记录主机指纹(TOFU),之后指纹变化会拦截
- 凭据保险箱:密码、私钥等敏感字段用主密码加密(AES-256-GCM + scrypt)后保存在本机;可选「在本机记住」自动解锁(Windows 用 DPAPI 保护)
- SSH 终端:交互式终端,页面与侧边栏共用同一会话,断线自动重连
- 文件管理(原「远程文件」,本地与远程工作区通用):目录树、Monaco 编辑器(语法高亮、Ctrl+S 保存、冲突检测)、Markdown / HTML / 图片预览、右键菜单(新建 / 上传 / 下载 / 重命名 / 复制 / 粘贴 / 删除 / 复制路径);可接管 DSH 自带的「文件」侧栏
- Git 仓库(原「远程 Git」,本地与远程工作区通用):改动(暂存 / 放弃 / 提交)、diff、带分叉图的提交历史;可查看任意分支,本地分支不检出也能直接修改和提交
- 检查更新:全局配置里一键检查 GitHub 最新版本并下载安装(GitHub 不通时回退 npm 官方源),也可上传离线 .tgz 包升级
- 添加工作区:接管 DSH 的「添加工作区」弹窗,可选本机目录(快捷位置 + 盘符)或远程主机目录(按分组展示)
- 远程智能体工具:远程工作区会话里,智能体的 read / write / edit / glob / grep / bash 都在远程主机上执行,本地会话不受影响
- 侧边栏:直接挂在 DSH 原生右侧栏,本地与远程工作区会话通用,详见下方「侧边栏」

侧边栏

右侧栏的全部能力由本插件直接注册到 DSH 原生右栏,不需要安装其他侧边栏插件。

- 文件管理:文件名搜索、目录自动刷新、Git 状态着色(M / A / D / U 等徽标)、右键「在对话中引用」(@ 引用)、「打开方式」(系统默认应用、VS Code / Cursor / Zed、自定义 URL 模板)、拖放上传(文件夹保留目录结构)、Ctrl / Shift 多选与批量操作、多选「压缩并下载」ZIP
- 文件查看器:聊天里的文件链接、交付卡片、@ 引用统一由插件查看器打开(本机与远程文件都可以)
- 代码 / 文本:Monaco 编辑器(Ctrl+S 保存、冲突检测)
- Markdown:预览 / 代码单栏切换(代码模式可编辑保存),支持代码高亮、Mermaid、KaTeX 公式、浮动目录;选中文字可「加入对话」
- HTML:预览 / 源码双模式,默认沙箱,可临时解除沙箱
- 其他格式由 file-viewer 渲染:PDF、Word(docx / doc / odt)、表格(xlsx / xls / csv / ods)、PPTX / ODP、音视频、压缩包(zip)等;.ppt、CAD 不支持预览,只提供下载;.rtf 按文本打开
- Git 仓库:改动、diff、提交历史之外,新增「AI 改动」页签,按本轮 / 全部会话列出模型改过的文件并可对比;diff 中的疑似密钥默认打码,可临时显示原文
- 任务:当前任务树的后台任务(实时输出、终止)、子代理(树 / 拓扑图)、工作流运行与团队看板;可设置出现新子代理 / 后台任务时自动打开
- 侧边对话:分叉当前会话,在右侧栏开一个不打断主对话的侧边对话;在线程列表里删除时会一并从左侧会话列表移除(DSH 归档)
- 终端:远程工作区会话里,引导页的「新建终端」直接打开插件 SSH 终端(连到工作区主机、进入工作区目录);本机会话保持 DSH 自带终端不变。此时引导页不再单独显示「SSH 终端」卡片,关闭该接管后恢复显示
- 标签右键菜单与固定标签:右侧栏任意停靠标签的右键菜单增加「关闭左侧 / 右侧 / 其他 / 全部」(只作用于所在窗格、跳过固定标签)与「固定」,文件标签另有「复制路径」「在文件管理中显示」(远程工作区会话只有「复制路径」);固定的标签排在窗格最左、不显示关闭按钮,打开侧边栏时自动补开,关闭即取消固定,按会话分别保存(终端、浏览器等可开多个的标签不能固定)
- 对外入口
- 智能体工具 open_in_sidebar:在当前会话右侧栏打开文件、文件夹或网页(远程会话按远程路径解析)
- 浏览器端客户端服务 dshWorkspace,供其他插件调用:openFile(path, { line?, mode? })、openTab(kind)、reveal(path)
- 管理页:「远程工作区 → 侧边栏」页签集中管理以上能力:各标签开关、文件关联、忽略规则、大文件阈值、HTML 沙箱默认值、open_in_sidebar 与任务页自动打开、远程工作区使用插件 SSH 终端、自定义 CSS、输入法守卫

与 dsh-better-sidebar 的关系

本插件已全部覆盖 dsh-better-sidebar 的侧边栏功能,不再依赖它。两者同时启用会出现功能重叠,建议在「远程工作区 → 侧边栏」页签里一键禁用它(只禁用不卸载,可随时重新启用)。

依赖

| 依赖 | 说明 |
| DSH | >= 0.1.5-rc.2(同时兼容 0.1.x 与 0.2.x;开发基于 DSH Desktop 2.0.16 / 宿主 0.2.0-rc.1) |
| Node.js | >= 20 |

安装

从 npm 安装最新版(命令不带版本号,自动装最新一版):

dsh plugin --profile <profile> add @yh4922/dsh-workspace
也可以装 Releases 里的最新安装包,或下载后用本地路径安装(文件名带版本号):

dsh plugin --profile <profile> add https://github.com/yh4922/dsh-workspace/releases/latest/download/dsh-workspace.tgz
dsh plugin --profile <profile> add /path/to/yh4922-dsh-workspace-<版本>.tgz

- <profile> 是要安装到的配置档,例如 web
- 升级:先 dsh plugin --profile <profile> remove @yh4922/dsh-workspace,再用上面的命令安装。对同一个包/地址再装一次时,插件管理器判断不出这次装的是哪个包(依赖值没有变化),会回滚并提示「无法从依赖变更中确定安装了哪一个包」
- 0.7.7 及更早版本的用户:当时的包名是 dsh-workspace,请先 dsh plugin --profile <profile> remove dsh-workspace 再装新包名;否则会同时存在两个包,运行的仍是旧代码
- DSH Desktop:desktop 配置档不能用命令行管理,请在「插件管理」→「添加插件」里填入 @yh4922/dsh-workspace 或上面的地址;升级同样是先移除、再添加(插件管理里没有「更新」按钮)
- 本插件依赖 ssh2,其中有原生构建脚本。pnpm 拦下构建时,在插件管理的失败界面点「允许这些脚本并重试」;命令行安装则按提示放行 ssh2、cpu-features
- 不能直接从 Git 仓库地址安装:仓库里没有编译产物(lib/)

卸载

dsh plugin --profile <profile> remove dsh-workspace

在 DSH Desktop 运行时卸载或升级本插件,曾导致 DSH 崩溃。建议先退出 DSH 再操作。

使用

1. 左侧栏打开「远程工作区」,第一次使用时设置主密码
2. 新建主机(可以放进分组),点「测试连接」确认能连上
3. 在「主机 / 文件 / 终端 / 连接日志」几个页签里管理主机、浏览文件、打开终端
4. 点 DSH 的「添加工作区」(「全局配置」页签里的「接管『添加工作区』」需开启,默认开启),选择远程主机和目录,即可创建远程工作区
5. 在会话右侧栏可以打开「文件管理」「Git 仓库」「任务」「侧边对话」(本地与远程工作区都可用);远程工作区会话还有「SSH 终端」。各标签可在「侧边栏」页签里开关
6. 「全局配置」页签里还有自动解锁开关,以及版本信息、更新日志等链接

开发

pnpm install
pnpm typecheck
pnpm test
pnpm build

- 真机集成测试默认跳过,设置 DSHWS_IT=1 并通过环境变量提供测试主机后运行 pnpm test:it
- 目前构建脚本(scripts/)没有放进仓库,单独克隆后暂时不能完整执行 pnpm build

打包与发布

| 命令 | 用途 |
| npm run pack:dev | 开发调试包:版本号为「下一个 patch-dev.时间戳」,只放在本机 pack/,不改 package.json、不推送 |
| npm run release -- [patch\|minor\|major] | 正式发布:累加版本号,把 CHANGELOG.md 的「未发布」定稿为新版本,构建打包,提交并推送 tag,创建附带安装包的 GitHub Release,并把同一个包发布到 npm |
| npm run release -- --dry-run --allow-dirty | 发布演练:只检查、构建和打包(不发 npm、不提交不推送),结束后还原 |

发布前要求:在 main 分支、没有未提交改动、与远程同步,「未发布」下已写好本次的更新日志,且本机已 npm login(发布到 registry.npmjs.org,未登录会在前置检查阶段直接失败)。

安全说明

- 凭据只保存在本机,并用主密码加密;主密码本身不落盘
- 远程工作区会话的系统提示词会要求智能体:没有用户明确要求时,不执行 commit、push、reset --hard、rebase
- 远程命令不受本机沙箱约束,只读沙箱模式下会拒绝写入和 bash

许可证

MIT
预览分享卡片长什么样 ↓yh4922/dsh-workspace
分享 · DeepSeek Harness Hub
yh4922/dsh-workspace

DeepSeek HarnessDSH的远程工作区插件:在 DSH 里管理 SSH…

回到顶部