远程办公助手,面向 DeepSeek Harness(DSH):连接 SSH(密钥或密码),选远程工作区,用 rw_* 工具操作,再通过 SFTP 镜像成本地真实的 DSH 工作区。
65.6/100
综合分
232
GitHub 星标
20,765
npm 周下载
—/5
用户评分
数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-remotenpm 源 · dsh-remote 的发布者账号已校验与本仓库一致
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · security
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 0 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-remote @ 0.8.54
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/29 17:39:18
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 159 处,展示前 104 条)
- 读取本地文件lib/index.js:340
const meta = JSON.parse(readFileSync(path.join(plain, '.dsh-remote-meta.json'), 'utf8')) - 读取本地文件lib/index.js:345
if (!existsSync(plain)) return plain - 写入本地文件lib/index.js:355
mkdirSync(dir, { recursive: true }) - 写入本地文件lib/index.js:358
writeFileSync(path.join(dir, '.dsh-remote-meta.json'), JSON.stringify(meta, null, 2)) - 写入本地文件lib/index.js:364
mkdirSync(to, { recursive: true }) - 读取本地文件lib/index.js:365
for (const e of readdirSync(from, { withFileTypes: true })) { - 写入本地文件lib/index.js:369
else try { copyFileSync(s, d) } catch { } - 读取本地文件lib/index.js:382
if (legacy === target || !existsSync(legacy) || existsSync(target)) return - 写入本地文件lib/index.js:384
mkdirSync(path.dirname(target), { recursive: true }) - 写入本地文件lib/index.js:386
renameSync(legacy, target) - 读取本地文件lib/index.js:451
if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === LOADED_VERSION) rmSync(marker, { force: true }) - 读取本地文件lib/index.js:451
if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === LOADED_VERSION) rmSync(marker, { force: true }) - 删除本地文件lib/index.js:451
if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === LOADED_VERSION) rmSync(marker, { force: true }) - 执行外部命令来自配置/变量lib/index.js:3188
execFile(bin, args, { timeout: 15000, maxBuffer: 1 << 20 }, (err) => { - 执行外部命令来自配置/变量lib/index.js:3205
execFile(bin, args, { timeout: PICK_TIMEOUT_MS, maxBuffer: 1 << 20 }, (err, stdout, stderr) => { - 发起网络请求lib/client.js:770
const res = await fetch(apiPath(path), opts) - 发起网络请求lib/client.js:780
const res = await fetch(apiPath(path), opts) - 写入剪贴板lib/client.js:949
return navigator.clipboard.writeText(text).then(() => true).catch(() => false) - 读取本地文件lib/hostkey.js:42
const j = JSON.parse(readFileSync(file, 'utf8')) - 写入本地文件lib/hostkey.js:61
try { mkdirSync(path.dirname(knownHostsFile), { recursive: true }) } catch {} - 写入本地文件lib/hostkey.js:62
writeFileSync(knownHostsFile, JSON.stringify(kh, null, 2)) - 读取本地文件lib/ignore.js:196
const text = readFileSync(ignoreFile, 'utf8') - 读取本地文件lib/sshconfig.js:78
const readFile = io.readFile || ((p) => readFileSync(p, 'utf8')) - 读取本地文件lib/sshconfig.js:79
const listDir = io.listDir || ((p) => readdirSync(p)) - 读取本地文件lib/sshconfig.js:545
const text = readFileSync(filePath, 'utf8') - 写入本地文件lib/sshconfig.js:696
const mkdir = io.mkdir || ((p) => mkdirSync(p, { recursive: true })) - 写入本地文件lib/sshconfig.js:697
const write = io.write || ((p, data) => writeFileSync(p, data, 'utf8')) - 写入本地文件lib/sshconfig.js:698
const chmod = io.chmod || ((p, mode) => { try { chmodSync(p, mode) } catch { } }) - 执行外部命令来自配置/变量lib/credential.js:47
child = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] }) - 写入本地文件lib/credential.js:140
mkdirSync(secretsDir || '.', { recursive: true }) - 写入本地文件lib/credential.js:141
writeFileSync(joinPath(secretsDir || '.', acc + '.bin'), b64, 'utf8') - 读取本地文件lib/credential.js:235
const b64 = readFileSync(joinPath(secretsDir || '.', acc + '.bin'), 'utf8').trim() - 删除本地文件lib/credential.js:256
try { unlinkSync(joinPath(secretsDir || '.', acc + '.bin')) } catch { } - 写入本地文件lib/sync.js:73
if (!existsSync(lp)) mkdirSync(lp, { recursive: true }) - 读取本地文件lib/sync.js:73
if (!existsSync(lp)) mkdirSync(lp, { recursive: true }) - 读取本地文件lib/sync.js:97
if (existsSync(lp)) { - 读取本地文件lib/sync.js:98
try { lk = localKey(statSync(lp)) } catch { lk = null } - 写入本地文件lib/sync.js:118
writeFileSync(lp, buf) - 写入本地文件lib/sync.js:119
try { utimesSync(lp, new Date(r.mtime * 1000), new Date(r.mtime * 1000)) } catch {} - 读取本地文件lib/sync.js:150
const entries = readdirSync(lDir, { withFileTypes: true }).filter( - 读取本地文件lib/sync.js:174
try { l = statSync(lp) } catch { return } - 读取本地文件lib/sync.js:199
const buf = readFileSync(lp) - 写入本地文件lib/sync.js:206
try { utimesSync(lp, new Date(rmt * 1000), new Date(rmt * 1000)) } catch {} - 写入本地文件lib/sync.js:234
mkdirSync(localDir, { recursive: true }) - 写入本地文件lib/sync.js:236
writeFileSync(tmp, JSON.stringify(state, null, 1)) - 读取本地文件lib/instructions.js:169
try { lk = existsSync(lp) ? localKeyOf(statSync(lp)) : null } catch { lk = null } - 读取本地文件lib/instructions.js:169
try { lk = existsSync(lp) ? localKeyOf(statSync(lp)) : null } catch { lk = null } - 写入本地文件lib/instructions.js:180
mkdirSync(path.dirname(lp), { recursive: true }) - 写入本地文件lib/instructions.js:181
writeFileSync(lp, f.content) - 写入本地文件lib/instructions.js:184
try { utimesSync(lp, new Date(f.mtime * 1000), new Date(f.mtime * 1000)) } catch { } - 读取本地文件lib/instructions-dynamic.js:225
const lst = statSync(lp) - 写入本地文件lib/instructions-dynamic.js:229
mkdirSync(path.dirname(lp), { recursive: true }) - 写入本地文件lib/instructions-dynamic.js:230
writeFileSync(lp, buf) - 写入本地文件lib/instructions-dynamic.js:231
try { utimesSync(lp, new Date((st.mtime || 0) * 1000), new Date((st.mtime || 0) * 1000)) } catch { } - 读取本地文件lib/remote-shell.js:210
try { return existsSync(candidate) } catch { return false } - 读取本地文件lib/binding.js:51
if (!local || !root || !existsSync(root)) return { mirrorDir, remotePath, machine } - 读取本地文件lib/binding.js:55
hostDirs = readdirSync(root) - 读取本地文件lib/binding.js:61
if (!statSync(hostPath, { throwIfNoEntry: false })?.isDirectory?.()) continue - 读取本地文件lib/binding.js:64
entries = readdirSync(hostPath) - 读取本地文件lib/binding.js:70
if (!existsSync(metaPath)) continue - 读取本地文件lib/binding.js:76
const meta = JSON.parse(readFileSync(metaPath, 'utf8')) - 读取本地文件lib/forwards.js:40
const j = JSON.parse(readFileSync(this.file, 'utf8')) - 写入本地文件lib/forwards.js:49
mkdirSync(path.dirname(this.file), { recursive: true }) - 写入本地文件lib/forwards.js:50
writeFileSync(this.file, JSON.stringify({ defs: this.defs }, null, 2)) - 读取本地文件lib/update.js:69
const pkg = JSON.parse(readFileSync(path.join(selfDir(), 'package.json'), 'utf8')) - 发起网络请求lib/update.js:120
res = await fetch(REGISTRY_URL, { signal: controller.signal, headers: { accept: 'application/json' } }) - 写入本地文件lib/update.js:197
mkdirSync(tmpPkg, { recursive: true }) - 写入本地文件lib/update.js:204
mkdirSync(path.dirname(dest), { recursive: true }) - 写入本地文件lib/update.js:205
writeFileSync(dest, f.data) - 读取本地文件lib/update.js:210
const newPkg = JSON.parse(readFileSync(path.join(tmpPkg, 'package.json'), 'utf8')) - 读取本地文件lib/update.js:215
if (!existsSync(newIndex) || statSize(newIndex) < 100) throw new Error('tarball missing lib/index.js') - 读取本地文件lib/update.js:227
const bytes = readFileSync(src) - 写入本地文件lib/update.js:229
try { mkdirSync(path.dirname(dest), { recursive: true }) } catch {} - 读取本地文件lib/update.js:236
if (existsSync(src)) install(src, path.join(installLib, name)) - 读取本地文件lib/update.js:240
if (existsSync(src)) install(src, path.join(dir, rel)) - 写入本地文件lib/update.js:242
writeFileSync(path.join(dir, '.dsh-remote-updated'), String(targetVersion)) - 删除本地文件递归lib/update.js:247
try { rmSync(tmpRoot, { recursive: true, force: true }) } catch {} - 写入本地文件lib/update.js:280
writeFileSync(tmp, data) - 删除本地文件lib/update.js:288
try { rmSync(tmp, { force: true }) } catch {} - 读取本地文件lib/telemetry.js:71
const existing = readFileSync(file, 'utf8').trim() - 写入本地文件lib/telemetry.js:79
mkdirSync(home, { recursive: true }) - 写入本地文件lib/telemetry.js:80
writeFileSync(file, `${created}\n`, 'utf8') - 发起网络请求lib/telemetry.js:147
await fetch(heartbeatUrl(), { - 读取本地文件lib/telemetry.js:194
return UUID_RE.test(readFileSync(path.join(home, ID_FILE), 'utf8').trim()) - 读取本地文件lib/registry.js:19
const j = JSON.parse(readFileSync(file, 'utf8')) - 写入本地文件lib/registry.js:44
try { mkdirSync(path.dirname(file), { recursive: true }) } catch {} - 读取本地文件lib/registry.js:49
const prev = JSON.parse(readFileSync(file, 'utf8')) - 写入本地文件lib/registry.js:58
writeFileSync(file, JSON.stringify(payload, null, 2)) - 发起网络请求lib/http-transport.js:21
async fetch(request) { - 读取本地文件lib/pool.js:408
key = readFileSync(keyPath) - 写入本地文件lib/openssh.js:134
try { mkdirSync(dir, { recursive: true }) } catch { } - 写入本地文件lib/openssh.js:139
writeFileSync(js, 'process.stdout.write((process.env.DSH_REMOTE_SSH_SECRET || "") + "\\n")\n') - 写入本地文件lib/openssh.js:140
writeFileSync(cmd, '@echo off\r\n"' + process.execPath + '" "' + js + '"\r\n') - 写入本地文件lib/openssh.js:148
writeFileSync(sh, '#!/bin/sh\nprintf \'%s\\n\' "$DSH_REMOTE_SSH_SECRET"\n') - 写入本地文件lib/openssh.js:149
try { chmodSync(sh, 0o700) } catch { } - 读取本地文件lib/openssh.js:170
try { accessSync(binary, fsConstants.X_OK); return true } catch { return false } - 读取本地文件lib/openssh.js:178
try { accessSync(path.join(dir, binary + ext), fsConstants.X_OK); return true } catch { } - 执行外部命令来自配置/变量lib/openssh.js:250
const child = spawn(this.sshBinary, args, { - 执行外部命令来自配置/变量lib/openssh.js:517
child = spawn(sshBinary, args, { stdio: ['pipe', 'pipe', 'pipe'], env: secret ? askpassEnv(secret) : undefined }) - 执行外部命令来自配置/变量lib/openssh.js:666
const child = spawn(sshBinary, args, { stdio: ['pipe', 'pipe', 'pipe'], env: secret ? askpassEnv(secret) : undefined }) - 写入本地文件lib/routes-fs.js:233
mkdirSync(path.dirname(local), { recursive: true }) - 读取本地文件lib/remote-shell-child.cjs:33
return JSON.parse(readFileSync(file, 'utf8')) - 读取本地文件lib/remote-shell-child.cjs:46
const exists = deps.exists || ((p) => existsSync(p)) - 执行外部命令来自配置/变量lib/remote-shell-child.cjs:252
const child = spawn(file, args, { stdio: 'inherit' })
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-llm@deepseek-ai/dsh-scope@deepseek-ai/dsh-session@deepseek-ai/dsh-timeout@deepseek-ai/dsh-tools@deepseek-ai/dsh-commands@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-client-locale给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
界面预览
4 张 · 点击查看大图功能亮点
- 连接SSH远程主机
- 选择远程工作区
- SFTP镜像为本地工作区
最近更新
在 GitHub 查看全部 →v0.8.672026-10-31
· 修两个数据级 bug:远端指令注入的消息没有 id(整份会话打不开);远程会话的 .md 链接读本地镜像(ENOENT)
#75 — 远端工作区指令注入的回退分支产出的 user 消息没有 id
拿不到核心 createUserMessage 工厂时,回退对象是 { role:'user', content, source } —— 没有 id。
核心只在恢复会话时校验(assertMessageEventShape 要求 message.id 为非空字符串),写入时不报错,
于是一条注入消息静默落盘,下次打开会话时整份历史拒绝加载(历史加载失败,130+ 轮会话全废)。
回退分支现在生成 id: randomUUID()(与工厂自身的身份源一致)。不改为直接 import dsh-llm:
该包不在本插件依赖里(pnpm 隔离布局 resolve 不到),且插件按设计要在没有 llm 服务的最小组合里也能跑。
回归测试:a message built WITHOUT a factory still carries an id。
#74 — 远程会话里 .md 链接打开本地镜像而不是远程文件
canOpen 曾明确把 .md/.markdown 让给内置 Markdown 预览(#64:源码墙不如渲染),但内置预览读的是
本地镜像 —— 没同步过的文件直接 ENOENT,什么都打不开。现在:
· 远程会话的 md 链接由 remote-link 认领,走 SFTP 打开(本地会话 cwd 不是镜像,仍让位给内置预览);
· 查看器检测到 md 时用宿主自带的 MarkdownText 渲染(#64 的「渲染」诉求保住),老构建没有该组件
才回退源码。
测试:canOpen 决策翻转用例 + 渲染级用例(证明走 MarkdownText 分支而非源码墙)。
另:测试 harness 补了一个已知短板 —— 跨组件挂载时 hook state 现在可 resetState() 重置
(sweep 测试里 link body 曾读到设置页的 state 槽残留)。
v0.8.662026-10-30
· 去掉「远端 dsh 体检与部署」;审计日志可以在本机打开
① 移除远程 dsh 部署 / 体检能力(按要求整体删掉,不留半可用状态)
· 设置页那张「远端 dsh 体检与部署」卡片(体检 / 部署并验证 / 让 AI 排查 / 任务进度)整块删除。
· host 侧对应动作全部删除:/web-attach 的 probe / install / install-status /
install-cancel / investigate;GET 响应也不再带 deploys/deploy
(现在只保留隧道本身:open / close)。
· 删掉 rw_deploy_probe 工具、lib/web-deploy.js、lib/deploy-tasks.js、
lib/web-deploy-skill.js,以及 webInstallPrefix / webInstallVersion /
webInstallRegistry 三个配置项(工具总数 22 → 21)。
· 远端必须有可用的 dsh:用 webAttachCommand(全局或按机器)指向它,README 已改。
② 审计日志能打开日志文件
日志在「运行 DSH 的那台机器」上,浏览器打不开它 —— 所以由宿主执行并回报结果:
· POST /dsh-remote/audit {action:'open'} —— 用本机默认程序打开日志(文件不存在则先创建);
· POST /dsh-remote/audit {action:'reveal'} —— 在文件管理器里定位它;
· 面板新增「打开日志文件」「在文件夹中显示」两个按钮,并且始终显示日志的完整路径,
这样即便宿主没有桌面环境(服务器 / CI),用户也能复制路径自己打开;
失败时给的是「无法自动打开(原因)—— 可以直接复制上面的路径」,不是一句空错。
实测(隔离实例 3213,真 GUI 点击):点「打开日志文件」→ Notepad 真的被拉起、日志文件被创建、
面板显示 已打开:D:\tmp\dsh-it3\remote-workspaces\audit.log;点「在文件夹中显示」→
资源管理器定位到该文件、面板显示 已在文件管理器中定位:…。
③ 修「配置里明明有主机,列表却是空的」
用户报告:面板显示 没在 C:\Users\…\.ssh\config 里找到主机,但文件里 3 个 Host 都在。
根因是 UI 把请求失败谎报成「你的配置没有主机」:loadSshHosts() 的 .catch 直接把列表设成空,
于是 401/500(或宿主半热重载后路由/字段不一致)都会渲染成「去改 ssh 配置吧」—— 让人去修一个没坏的文件。
现在两种状态分开:「读取失败:(这不是你的 ssh 配置问题,文件根本没读到;一直失败说明插件版本与服务端不一致,重启 DSH)」
vs「没在 里找到主机」。
(另注:用户那台实例的进程是 10-25 启动的,而修复版代码是 10-30 写入磁盘 —— 他看到的空列表来自那个时间差,重启即对齐。)
测试:新增 test/audit-open-and-no-deploy.test.js(6 条:工具不再注册、五个部署动作都被
action must be open | close 拒绝、GET 不再返回部署状态、打开日志会创建文件并回报路径、
未知动作 400、无桌面环境时给 501 + 路径);删除 5 个部署相关测试文件;全套 842 条通过。
v0.8.652026-10-30
· 「选目录」提交后多报一条 not a directory (or unreachable)(真机 GUI 测试抓出)
用一个隔离实例(独立 DSH_HOME、端口 3212、插件 0.8.64)在真实 GUI 里走完整流程时暴露:
选远程目录 → 「设为远程工作区」后工作区确实建好了,但面板同时报
not a directory (or unreachable): D:\tmp\...\remote-workspaces\...。
根因是双重提交:DirPicker.commitPath() 在 /mirror 成功后调用 onPicked(res.localMirror),
把本地镜像路径当成"用户选的路径"交回去;设置页拿到它又 POST 了一次 /mirror,于是远端去
stat 一个 Windows 路径。修法:commitPath 同时交出远端路径 onPicked(localMirror, remotePath)
(侧边栏只取第一个参数,不受影响),设置页用它做提示并停止二次提交。
测试 +1(917 条):提交目录不许再 POST 一次 /mirror,且仍要刷新注册表、仍要说"已打开"。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。
README
由 DeepSeek 最新模型翻译生成中文文档已移到仓库首页 →
Moved
The Chinese README is now this repository's main README, so this file is kept
only so that older links to README.zh.md (npm pages, blog posts, mirrors,
browser bookmarks) keep working.
- 中文文档 → \README.md\(仓库首页)
- English docs → \README.en.md\
预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
flymysql/dsh-remote
远程办公助手,面向 DeepSeek Harness(DSH):连接 SSH(密钥或密码),选远程工作区,用 rw_* 工具操作,再通过 SFTP 镜像成本地真实的 DSH 工作区。
相关插件 · 安全 / 权限
智能体逆向工程morluto/rea
用智能体逆向工程任何东西,从应用行为到原生二进制。
★ 422,036已汉化DSH
73.3
智能体集群框架ruvnet/ruflo
面向 Claude Code 与 Codex 的智能体元基座meta-harness。
★ 80,619已汉化MCP
71.8
WorkBuddy 模型接入corrinehu/dsh-workbuddy-connect
2026 年 10 月 20 日起,WorkBuddy…
★ 1,604已汉化DSH
71.7
破甲一键通z91772524-ai/pojia-next
十五个 AI…
★ 1,004已汉化DSH
71.6
Agent 操作层agentic-os-org/ANOLISA
ANOLISA(Agentic Nexus Operating Layer & Interface System Architecture)|智能体操作系统,带运行时、安全、可观测性,以及 Tokenless 响应压缩,降低 token 用量与成本。
★ 653已汉化DSH
70.7
AI 代理虚拟终端strukto-ai/mirage
Mirage 是给 AI 智能体用的虚拟终端:一层 shell,统管数据、命令行和运行时。Unix…
★ 3,762已汉化DSH
70.7