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

站点发布管理 TodayWei/dsh-wei-sitecontrol

DeepSeek Harness
TodayWei53 天前有提交已提供中文文档扫描:含敏感能力

登记网站服务,统一管理启停、Git 提交与 SSH 发布

29.6/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

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

自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add TodayWei/dsh-wei-sitecontrol
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么[目录]—
dsh 原生插件 · market
暂无时间戳·判据 →
装得上吗[实装验证]—
静态安装检查有提示项,装前建议看一眼 README
数据截至 2026/09/25·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 53 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

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

验证判据与等级说明 →

可信度证据

安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

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

✗npm 包dsh-wei-sitecontrol(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/9/22 02:03:07

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

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

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 70 处,展示前 62 条)
  • 发起网络请求
    client.js:232
    res = await fetch(path, options)
  • 写入本地文件
    lib/registry.js:265
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    lib/registry.js:271
    if (!existsSync(this.path)) return empty
  • 读取本地文件
    lib/registry.js:273
    const parsed = JSON.parse(readFileSync(this.path, 'utf8'))
  • 写入本地文件
    lib/registry.js:288
    writeFileSync(tmp, JSON.stringify({ version: 1, ...data }, null, 2), 'utf8')
  • 写入本地文件
    lib/registry.js:289
    renameSync(tmp, this.path)
  • 读取本地文件
    lib/lifecycle.js:26
    return readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/ /g, ' ').trim() || null
  • 执行外部命令固定命令
    lib/lifecycle.js:28
    const out = execFileSync(
  • 执行外部命令固定命令
    lib/lifecycle.js:86
    const out = execFileSync('netstat', ['-ano'], { encoding: 'utf8', windowsHide: true, maxBuffer: 8 * 1024 * 1024 })
  • 写入本地文件
    lib/lifecycle.js:109
    mkdirSync(this.logDir, { recursive: true })
  • 写入本地文件
    lib/lifecycle.js:169
    appendFileSync(this.logPath(siteId), `${line}\n`, 'utf8')
  • 写入本地文件
    lib/lifecycle.js:177
    writeFileSync(this.logPath(siteId), '', 'utf8')
  • 读取本地文件
    lib/lifecycle.js:187
    if (lines.length === 0 && existsSync(this.logPath(siteId))) {
  • 读取本地文件
    lib/lifecycle.js:189
    const size = statSync(this.logPath(siteId)).size
  • 读取本地文件
    lib/lifecycle.js:193
    const raw = size > maxBytes ? readFileSync(this.logPath(siteId)).subarray(size - maxBytes).toString('utf8') : readFileSync(this.logPath(siteId), 'utf8
  • 执行外部命令来自配置/变量
    lib/lifecycle.js:240
    const child = spawn(site.command, {
  • 执行外部命令固定命令
    lib/lifecycle.js:306
    spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true })
  • 执行外部命令固定命令
    lib/lifecycle.js:362
    if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(conflict.pid), '/T', '/F'], { windowsHide: true })
  • 执行外部命令固定命令
    lib/lifecycle.js:403
    if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true })
  • 发起网络请求
    lib/lifecycle.js:436
    const res = await fetch(url, { signal: controller.signal, redirect: 'manual' })
  • 读取本地文件
    lib/gitops.js:68
    return existsSync(join(dir, '.git')) ? dir : null
  • 读取本地文件
    lib/gitops.js:79
    if (!existsSync(dir)) throw new Error(`site workspace does not exist: ${dir}`)
  • 读取本地文件
    lib/gitops.js:82
    if (existsSync(join(dir, '.git'))) {
  • 读取本地文件
    lib/deploy.js:41
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
  • 读取本地文件
    lib/deploy.js:149
    if (!existsSync(root)) {
  • 读取本地文件
    lib/deploy.js:165
    const totalBytes = files.reduce((sum, f) => sum + (statSync(f.abs).size || 0), 0)
  • 写入本地文件
    lib/vault.js:74
    mkdirSync(this.dir, { recursive: true })
  • 写入本地文件
    lib/vault.js:81
    chmodSync(this.dir, 0o700)
  • 执行外部命令固定命令
    lib/vault.js:88
    if (user) execFileSync('icacls', [this.dir, '/inheritance:r', '/grant:r', `${user}:(OI)(CI)F`], { windowsHide: true, stdio: 'ignore' })
  • 读取本地文件
    lib/vault.js:102
    if (existsSync(dest) && !overwrite) throw new Error(`密钥 "${name}" 已存在(如需替换请显式覆盖)`)
  • 读取本地文件
    lib/vault.js:106
    if (!existsSync(sourcePath)) throw new Error(`文件不存在:${sourcePath}`)
  • 读取本地文件
    lib/vault.js:107
    bytes = readFileSync(sourcePath)
  • 写入本地文件
    lib/vault.js:119
    writeFileSync(tmp, bytes, { mode: 0o600 })
  • 写入本地文件
    lib/vault.js:120
    renameSync(tmp, dest)
  • 写入本地文件
    lib/vault.js:129
    chmodSync(file, 0o600)
  • 执行外部命令固定命令
    lib/vault.js:137
    execFileSync('icacls', [file, '/inheritance:r', '/grant:r', `${user}:F`], { windowsHide: true, stdio: 'ignore' })
  • 读取本地文件
    lib/vault.js:146
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/vault.js:147
    const stat = statSync(file)
  • 读取本地文件
    lib/vault.js:148
    const info = inspect(readFileSync(file, 'utf8'))
  • 删除本地文件
    lib/vault.js:186
    rmSync(file, { force: true })
  • 写入本地文件
    lib/scripts.js:24
    mkdirSync(this.dir, { recursive: true })
  • 读取本地文件
    lib/scripts.js:36
    if (!existsSync(this.dir)) return []
  • 读取本地文件
    lib/scripts.js:37
    return readdirSync(this.dir)
  • 读取本地文件
    lib/scripts.js:40
    const stat = statSync(join(this.dir, file))
  • 写入本地文件
    lib/scripts.js:53
    writeFileSync(tmp, text.endsWith('\n') ? text : `${text}\n`, 'utf8')
  • 写入本地文件
    lib/scripts.js:54
    renameSync(tmp, dest)
  • 读取本地文件
    lib/scripts.js:55
    const stat = statSync(dest)
  • 读取本地文件
    lib/scripts.js:61
    if (!existsSync(file)) throw new Error(`脚本 "${name}" 不存在`)
  • 读取本地文件
    lib/scripts.js:62
    return readFileSync(file, 'utf8')
  • 删除本地文件
    lib/scripts.js:68
    rmSync(file, { force: true })
  • 读取本地文件
    lib/profile.js:19
    const bytes = readFileSync(path)
  • 读取本地文件
    lib/profile.js:90
    if (!existsSync(dir) || !statSync(dir).isDirectory()) return null
  • 读取本地文件
    lib/profile.js:90
    if (!existsSync(dir) || !statSync(dir).isDirectory()) return null
  • 读取本地文件
    lib/profile.js:91
    const candidates = readdirSync(dir).filter((file) => /\.(pem|key|ppk)$/i.test(file))
  • 读取本地文件
    lib/profile.js:99
    if (!existsSync(dir)) return null
  • 读取本地文件
    lib/profile.js:100
    const names = readdirSync(dir).filter((file) => /\.(txt|md)$/i.test(file))
  • 执行外部命令固定命令
    lib/sysmon.js:117
    return execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
  • 读取本地文件
    lib/ssh.js:38
    if (!existsSync(target.privateKeyPath)) throw new Error(`私钥文件不存在:${target.privateKeyPath}`)
  • 读取本地文件
    lib/ssh.js:39
    options.privateKey = readFileSync(target.privateKeyPath)
  • 发起网络请求
    lib/githttp.js:213
    const req = mod.request(
  • 发起网络请求
    lib/githttp.js:242
    const connectReq = http.request({
  • 发起网络请求
    lib/githttp.js:257
    const req = https.request(
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 登记网站与HTTP服务
  • 管理服务启停
  • Git提交与SSH部署

DeepSeek Harness 的站点控制器:注册网站/HTTP 服务并管理其生命周期、git 和基于发布的 SSH 部署——支持服务器探测、保存部署脚本、回滚和定时发布。

README

由 DeepSeek 最新模型翻译生成
dsh-wei-sitecontrol DSH 插件:把在这台机器上用 DSH 做出来的网站 / HTTP 服务登记成一份带依赖信息的注册表,然后在 DSH 里统一管理它们的生命周期、git 提交与 SSH 发布。 - 🧭 侧栏入口 + 管理面板(列表、状态、启停、日志、git、发布) - 🤖 给 Agent 的 site_ 工具 + 一份登记技能(skill/SKILL.md) - 🗄️ 注册表落盘 $DSH_HOME/storages/dsh-wei-sitecontrol/registry.json - 🔧 内置纯 JS git(isomorphic-git,无需系统 git)与 SSH 发布(ssh2) - 📈 监视:面板里的「监视」页显示本机 CPU / 内存 / 磁盘、每个站点进程的实时占用,以及 DSH 下正在跑的任务名字(只列名字、不带进度,agent 忙碌时也看得清) 双半结构 | 半边 | 文件 | 职责 | | Host | index.js + lib/ | 站点注册表、进程监督(启停/日志/健康检查)、git 层、SSH 发布层、/dsh-wei-sitecontrol HTTP API、site_ 工具 |
展开完整文档(README 剩余 1 节)
dsh-wei-sitecontrol

DSH 插件:把在这台机器上用 DSH 做出来的网站 / HTTP 服务登记成一份带依赖信息的注册表,然后在 DSH 里统一管理它们的生命周期、git 提交与 SSH 发布。

- 🧭 侧栏入口 + 管理面板(列表、状态、启停、日志、git、发布)
- 🤖 给 Agent 的 site_ 工具 + 一份登记技能(skill/SKILL.md)
- 🗄️ 注册表落盘 $DSH_HOME/storages/dsh-wei-sitecontrol/registry.json
- 🔧 内置纯 JS git(isomorphic-git,无需系统 git)与 SSH 发布(ssh2)
- 📈 监视:面板里的「监视」页显示本机 CPU / 内存 / 磁盘、每个站点进程的实时占用,以及 DSH 下正在跑的任务名字(只列名字、不带进度,agent 忙碌时也看得清)

双半结构

| 半边 | 文件 | 职责 |
| Host | index.js + lib/ | 站点注册表、进程监督(启停/日志/健康检查)、git 层、SSH 发布层、/dsh-wei-sitecontrol HTTP API、site_ 工具 |
| Client | client.js | 侧栏底部入口(sidebar.footer.action)+ 管理面板(shell.overlay),通过同源 /dsh-wei-sitecontrol API 与 Host 通信 |

lib/ 分工:registry.js(数据模型 + 持久化 + 脱敏)、lifecycle.js(进程监督)、gitops.js(git)、deploy.js(SSH 发布)。

安装

方式 A:从发布包安装(推荐给其他人)

1) 在 profile 下安装插件包(会一并解析 ssh2 / isomorphic-git 依赖)
dsh plugin --profile web add \dsh-wei-sitecontrol-0.1.0.tgz
等价于在 %USERPROFILE%\.dsh\profiles\web 目录里 npm install 这个 tgz

2) 把插件行插入 profile 组合(见 cordis.patch.yml) 编辑 %USERPROFILE%\.dsh\profiles\web\cordis.patch.yml,追加:
- insert:
- id: wei-sitecontrol
name: dsh-wei-sitecontrol

3) 登记技能(可被 Agent 自动发现) 复制 skill/SKILL.md → %USERPROFILE%\.dsh\skills\dsh-wei-sitecontrol\SKILL.md

4) 重启 dsh web

方式 B:从源码目录安装(开发者)

cd ; npm install
让 profile 能解析裸包名(Windows 用 junction,等价于 ln -s) cmd /c mklink /J "%USERPROFILE%\.dsh\profiles\web\node_modules\dsh-wei-sitecontrol" "" 然后同上第 2~4 步

数据与卸载

- 注册表、密钥保险库、部署脚本、日志都在 $DSH_HOME/storages/dsh-wei-sitecontrol/
- 密钥文件权限:POSIX 600 / Windows ACL 仅当前用户;插件从不通过 API、工具或日志返回密钥内容(只给指纹)
- 卸载:删 profile patch 里的插件行 → 删 node_modules 里的链接 → 可选删除上述数据目录
- 插件卸载(DSH 关闭 / profile 重载)时会停止它启动的所有站点子进程,并释放路由与定时器(有回归测试锁定)

HTTP API

前缀 /dsh-wei-sitecontrol(与 DSH Web 同端口):

| 方法与路径 | 说明 |
| GET /sites | 站点列表(含运行状态) |
| POST /sites | 登记或更新站点 |
| DELETE /sites/ | 删除站点(运行中会先停止) |
| POST /sites//start\|stop\|restart | 生命周期 |
| GET /sites//logs?tail=&search= | 日志(内存环 + 落盘日志) |
| GET /sites//health | HTTP 健康探测 |
| POST /sites//reclaim | 端口被占用(孤儿进程)时释放 { confirm: true } |
| POST /sites//git/init | 目录还不是仓库时初始化(默认分支 main,幂等) |
| GET /sites//git/status | 分支/变更/领先落后/最近提交 |
| POST /sites//git/commit | 暂存全部并提交 { message } |
| POST /sites//git/push | 推送 { remote?, branch?, token? } |
| POST /sites//deploy | 发布 { target, dryRun?, allowInstall?, overrides? } |
| POST /sites//deploy-plan | 预览发布计划 { target, detect? }(detect 默认 true,只读探测服务器) |
| GET/POST /targets,DELETE /targets/,POST /targets//ping,POST /targets//detect | 发布目标管理、连通性测试与环境探测 |
| GET /keys,POST /keys,DELETE /keys/ | 密钥保险库(只返回元数据与指纹) |
| POST /targets/import | 从"一台服务器一个目录"的档案(pem + 说明)一键建档 |
| POST /targets//provision | 描述驱动建档:{ text, user?, discover? } → 解析描述 + 连服务器读配置 + 回填发布字段 |
| GET /scripts、GET /scripts/、POST /scripts、DELETE /scripts/ | 部署脚本仓库 |
| GET /schedules、POST /schedules、DELETE /schedules/ | 定时发布排期(入库、重启后自动恢复) |
| GET /sites//releases?target=、POST /sites//rollback | release 模式的发布历史与回滚 |
| GET /status | 站点数与运行中数量(面板徽标用) |
| GET /monitor | 监视快照:{ summary, machine, sites, tasks, probeError, sampledAt } —— 机器 CPU/内存/磁盘(按 tick 差值算,首次为 null 而不是编一个数)、各站点进程的 CPU/内存/pid、以及任务名列表(已过滤采样进程本身) |

SSH 部署

描述驱动建档(推荐入口)

不用手填一堆字段:把环境说明整段文本交给插件即可。文本存在 envText(数据库里),目录只需要 pem。

面板:目标区最上方「环境描述」文本框 → 「按描述建档并连服务器确认」 POST /targets//provision { "text": "ip:203.0.113.10\nssh端口:TCP:22\n描述:…使用tomcat服务,tomcat应用目录在:/opt/tomcat9。程序发布目录在/srv/www。", "user": "root" }

执行顺序:解析文本(ip / ssh 端口 / 描述 / 应用目录 / 发布目录 / 服务类型 / 单元名;Tomcat 描述会自动推出 configFile 与 releasesDir,并选 deployMode: release)→ 立即入库(SSH 失败也不丢)→ 连服务器读它自己的配置( / nginx root / DocumentRoot)→ 用服务器真相覆盖推断值 → 写回全部发布字段 + 带时间戳的 verified 快照。

- envText 是权威来源;environment 是它解析出的描述行,面板同时展示两者
- POST /targets 是 PATCH 语义:只发 {name, envText} 不会动其它字段(已有测试锁定)
- Agent 侧同名工具:site_target_provision

定时发布

POST /schedules { site, target, at, note } → 排期入库(记录在 registry.json) GET /schedules → pending / running / done / failed / cancelled DELETE /schedules/ → 取消(pending 才可取消)

到点执行的与手动发布完全同一条代码路径,并把每步结果(含失败步骤名)写回排期记录;DSH 重启后启动时重新装订所有 pending 排期,长时间等待自动续订。面板上可用 datetime-local 直接设定。Agent 工具:site_schedule_publish / site_schedule_list / site_schedule_cancel。

密钥保险库

从本机已有文件导入(推荐:内容不进对话、不进日志) POST /keys { "name": "prod", "path": "E:\\keys\\prod.pem" } 面板上传路径(content 直传,同样只回元数据) POST /keys { "name": "prod", "content": "-----BEGIN RSA PRIVATE KEY-----\n..." }

- 落盘位置:/keys/.pem,POSIX 600 / Windows ACL 仅当前用户(目录同样收紧)
- 密钥内容永不出现在任何 API、工具或日志输出中,只有元数据:bytes、uploadedAt、keyType、encrypted、fingerprint(ssh-keygen 风格的 SHA256:…)、warning
- 导入即校验:非 PEM 拒绝、无法解析拒绝、密钥名禁止路径穿越、重复导入需显式 overwrite
- PKCS#8 提示:内置 SSH 客户端(ssh2)不支持 -----BEGIN PRIVATE KEY-----(PKCS#8)。这类密钥会被接受并标记 warning,请先转换:ssh-keygen -p -m PEM -f key.pem(AWS 的 .pem 通常是 PKCS#1,可直接用)
- 加密私钥(带 passphrase)可以导入,此时不计算指纹

目标服务器字段

| 字段 | 说明 |
| host / port / user | SSH 地址与账号 |
| keyName | 保险库密钥名(首选凭据);也可用 privateKeyPath(本机文件)或 password |
| uploadDir | 上传目录(如 /srv/www/site、/opt/tomcat9/webapps/site);也可由探测建议 |
| serviceKind | static / httpd / nginx / tomcat / docker / custom,决定停止与启动方式 |
| serviceName | 非标准单元名或容器名(如 tomcat9) |
| appHome | 运行时目录(如 Tomcat 的 CATALINA_HOME),生成的启停命令会优先用它 |
| environment | 环境/用途描述(通常由档案导入填入,面板与 Agent 都会看到) |
| sourceDir | 该目标的凭据/说明来自哪个档案目录(便于回头核对) |
| restartOnDeploy | 发布时是否停止/重启服务(默认 true)。静态内容直接从磁盘提供服务时可设 false,避免停机 |
| deployMode | inplace(默认,直接覆盖 uploadDir)或 release(每次发布到新时间戳目录再切换配置,可回滚) |
| releasesDir | release 模式:发布目录的父目录(如 /opt/releases) |
| configFile | release 模式:要改写 docBase 的配置文件(如 Tomcat 的 conf/server.xml) |
| contextPath | release 模式:要切换的 Context 路径,默认 / |
| verified | 探测落库的快照(只读):时间戳、系统、各服务是否存在、Tomcat home/unit/appBase、服务器配置里的发布目标 docBase、监听端口 |
| installCommand / stopCommand / startCommand / restartCommand | 覆盖该服务类型的默认命令(留空则用内置剧本) |
| backup / backupDir / keepReleases | 是否备份(默认开)、备份目录(默认 .bak)、保留份数(默认 3) |
| scriptName | 关联的部署脚本名(默认 -deploy) |

服务器档案一键导入(一个目录一台服务器)

约定目录结构(与 \keys\ 一致):

keys//
.pem 私钥(导入保险库,内容不外显) hostreadme.txt ip / ssh端口 / 描述 / 应用目录 / 发布目录(UTF-8 或 GBK 都能读)

POST /targets/import { "dir": "E:\\\\keys\\示例站点" } 或让 Agent 执行:site_target_import({ dir: "E:\\\\keys\\示例站点" })

一次完成:私钥入保险库(以目录名为密钥名)、解析说明里的 ip 与 ssh 端口、推断服务类型(tomcat/nginx/httpd/docker/static)、取出应用目录(CATALINA_HOME)与发布目录,并把描述写入 environment。解析不到的项会在返回里以「待补」列出,不会自动上传或执行任何远端操作。

支持的中文/英文标签(大小写不敏感):ip: / host: · ssh端口: / ssh port: · 描述: / 说明: / 环境: · 程序发布目录在… / 发布目录: / publish dir: · tomcat应用目录在… / CATALINA_HOME:

探测会把确认到的环境写回目标

POST /targets//detect(工具 site_server_detect)不只是看一眼——它会:

1. 读服务器自己的配置:Tomcat 的 、nginx 的 root、httpd 的 DocumentRoot,从而得到真正的发布目录,而不是靠猜;
2. 回填为空的发布字段:uploadDir(来自 docBase/root)、configFile、appHome(CATALINA_HOME)、serviceName(systemd 单元名)、serviceKind;
3. 写入 verified 快照(带时间戳),后续任何时候都能在面板/工具里看到“上次确认的服务器环境是什么”;
4. 绝不覆盖手工设过的值 —— 显式配置永远优先;要只读探测可传 persist: false。

返回里的 filled 数组就是本次自动补齐了什么(面板会显示成「本次自动补齐:…」)。

release 模式:新目录 + 配置切换(可回滚)

collect → connect → detect → mkdir / → upload 到该目录 → 读 configFile → 改写 contextPath 的 docBase → 备份配置 .bak- → 新配置写到 .new- 再 mv 原子覆盖 → restart → curl 校验 → 清理旧发布(保留 N 份)

为什么值得:覆盖式发布没有回滚,而回滚在出问题时就是一切。

- 护栏:找不到匹配的 Context 就拒绝改写并中止(绝不猜、绝不盲改);配置先备份;新配置走临时文件 + mv 原子替换;每步都有报告
- 无中断:文件先上传到新目录,配置切换前线上仍在跑旧版本;只有 restart 那一下有短暂停机(若 restartOnDeploy: false 且 Context reloadable="true",连停机都没有)
- 回滚:site_releases 看历史(含当前生效目录),site_rollback 切回上一版(或不指定时自动选上一版),同样先备份配置、改完重启并校验
- 旧发布目录留在磁盘上直到被 keepReleases 清理,所以回滚随时可用

发布流程

collect → connect → detect → resolve uploadDir → (install) → script → upload(SFTP) → run script → verify

- detect:一条复合探测命令,取回 OS/包管理器、httpd|apache2 及配置目录、nginx、Tomcat 的 CATALINA_HOME/conf/server.xml/webapps、Docker 版本与运行中容器、相关 systemd 单元、监听端口、候选站点目录
- install:仅当运行时缺失且显式传 allowInstall: true 时才执行安装命令(例如 yum install -y httpd)
- script:发布脚本存在 /scripts/.sh,首次发布自动生成、之后可编辑;经 bash -s 由标准输入执行,远端不留脚本文件。脚本负责远端侧:备份(保留 N 份并清理旧的)→ 停止服务 → (文件已由 SFTP 上传)→ 启动服务 → 校验(列目录 + 列监听端口)
- 文件传输由 Node 侧 SFTP 完成,排除 node_modules/、.git/、缓存目录等;站点可用 exclude 追加

相关工具

site_key_add · site_key_list · site_key_remove · site_target_add · site_target_list · site_server_detect · site_deploy_plan · site_deploy · site_script · site_monitor

监视(site_monitor 与面板「监视」页)

回答两个问题:这台机器现在有多忙,以及 DSH 底下正在跑什么(只要名字)。

CPU 18%(×20) · 内存 40% · 站点 1/4 运行 · 任务 6 · 2026-09-10T19:12:03.114Z 磁盘: C:\ 91% | D:\ 65% | E:\ 72% 站点占用: 进驰官网 running :8080 CPU 0% 内存 32.1 MB pid 25032,33212
在跑的任务(只有名字,共 6 个):
[dsh] DSH 本体 pid 29840 CPU 1% 内存 210.4 MB · node .../dsh/lib/bin.js --profile web [site] 站点 · 进驰官网 pid 33212 CPU 0% 内存 32.0 MB · python -m http.server 8080 [child] conhost.exe pid 1316 CPU 0% 内存 7.6 MB · \??\C:\WINDOWS\system32\conhost.exe ...

- CPU 是相对单核的百分比(多线程任务可以超过 100%);机器级那条按核数换算归一
- 首次采样不会编数字:没有前一帧就报 —,窗口内第二次采样起才有百分比
- 进程表:Windows 走一次 Get-CimInstance Win32_Process(约 0.3 秒),POSIX 走 ps -eo;采样结果缓存 1.5 秒,所以面板、工具、第二个浏览器标签同时轮询也只花一次采样的代价
- 任务名字来自 DSH 进程树 + 已登记站点的命令行匹配(复用孤儿清理那套保守规则),只报 pid / 名字 / CPU / 内存,不含进度与输出 —— 这是刻意的,agent 正在干活时它必须依然可读
- 采样用的 PowerShell 进程自己会被过滤掉,不会出现在列表里
- 进程表取不到时(例如 PowerShell 被限制)仍然返回 CPU / 内存,并带 probeError 让面板显示一条醒目提示,而不是整页报错

数据契约(面板与 Agent 共同依赖)

Site.status.state 的 6 个取值(面板按此上色):

| 取值 | 含义 |
| stopped | 未运行(含正常退出) |
| starting | 已 spawn,尚未确认可用 |
| running | 子进程存活 |
| unhealthy | 进程在,但健康探测判定异常 |
| stopping | 已发出停止信号,等待退出 |
| failed | 非零退出、spawn 失败或探测判定失败 |

- GET /status 的 running 计数 = running | starting | unhealthy(与监督器的 live 定义一致)
- GET /sites//health 在失败时除 { ok, status, ms, url } 外还会带 error(no port or url configured / timeout / 异常信息)
- POST /sites//git/push 的 token 可省略:此时回落到站点配置的 gitTokenEnv 指向的环境变量;两者都没有时,私有 HTTPS 远端会推送失败(面板不采集也不发送任何密钥)
- POST /sites 登记新站点必须显式给出 workspace 绝对路径,否则返回 400(避免默认落到 DSH 进程工作目录而污染 name+workspace 幂等键);携带 id 的更新请求可省略

配置(插件行 config)

| 字段 | 默认 | 说明 |
| dataDir | $DSH_HOME/storages/dsh-wei-sitecontrol | 注册表与日志目录 |
| ringLines | 5000 | 每个站点内存日志行数上限 |
| stopGraceMs | 3000 | 停止时的宽限期 |
| autoReclaimOrphans | true | 启动时自动清理"命令行与站点自身命令匹配"的遗留进程(见下) |
| gitAuthorName / gitAuthorEmail | DSH Site Manager / site-manager@localhost | 内置 git 的提交作者 |

启动时的孤儿清理(autoReclaimOrphans)

站点是 DSH 的子进程,正常关闭时会被干净停掉。但 DSH 被硬杀(断电、任务管理器结束任务、taskkill /F)时没有任何机会执行清理,站点进程就会活下来继续占着端口,导致下次启动失败。

所以插件在启动约 1.5 秒后会做一次清扫,判据严格且保守:

1. 只看已登记站点的端口;
2. 取出占用者的命令行,必须与站点自身的命令匹配(命令里的脚本名/端口号等特征 token 全部出现);
3. 命中才终止(Windows taskkill /T /F);
4. 不匹配的进程绝不碰,只记录为冲突(status.conflict.foreign = true)并在站点日志里写明占用 pid 与命令行,由面板显示红字提示,由人决定是否点「释放端口」。
以下情况一律不杀:命令行含 @deepseek-ai/dsh 或 lib/bin.js(harness 自身)、命令行读不出来、站点没有端口、执行清扫的是 DSH 自己的 pid。

想让插件完全不碰进程,把该字段设为 false,改用面板的「释放端口」按钮或 site_reclaim 工具手动处理。

已知限制

- 内置 git 只支持 HTTP(S) 远端:git@host:path 这类 SSH 远端无法推送(isomorphic-git 不支持 SSH 传输)。本地 status/commit 不受影响。要推 SSH 远端请改为 HTTPS + token,或改用系统 git。
- 内置 git 的 HTTP 传输是本插件自己实现的(lib/githttp.js),因为 isomorphic-git 自带的 Node 客户端有两个坑:①它内部用 simple-get,而 Node 19+ 的默认连接池是 keepAlive + timeout:5000 —— 服务器沉默 5 秒(推送到 GitHub 时 receive-pack 处理包就会这样)就报出与真实原因无关的 Request timed out;②Node 的 http(s).request 不读 Windows 系统代理,在需要代理的网络里直连会 connect ETIMEDOUT。现在:自管连接与超时,直连失败自动改走 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY 或探测到的本地代理(127.0.0.1:7890 等)的 CONNECT 隧道,而 127. / 192.168. / 10. / 172.16-31.* 与 NO_PROXY 命中的地址始终直连(局域网 git 服务器不会绕远路)。
- 站点是本 DSH 进程的子进程:DSH 退出时会被回收;需要长期守护请交给 systemd/pm2 等,再用本插件做登记与发布。
- Windows 停止方式为 taskkill /T /F(强制),POSIX 为进程组 SIGTERM → SIGKILL。
- 密钥与密码只存本机注册表(registry.json),API 与工具返回中一律脱敏。
预览分享卡片长什么样 ↓TodayWei/dsh-wei-sitecontrol
分享 · DeepSeek Harness Hub
TodayWei/dsh-wei-sitecontrol

登记网站服务,统一管理启停、Git 提交与 SSH 发布

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部