Bundle
dsh-wei-sitecontrol
Site controller for DeepSeek Harness: register websites/HTTP services and manage their lifecycle, git, and release-based SSH deploys — with server probing, stored deploy scripts, rollback and scheduled publishing.
- Source
- TodayWei
- License
- MIT
- Updated
- Updated 9 hours ago
Readme
# 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:从发布包安装(推荐给其他人)
```powershell
# 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:从源码目录安装(开发者)
```powershell
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/<id>` | 删除站点(运行中会先停止) |
| `POST /sites/<id>/start\|stop\|restart` | 生命周期 |
| `GET /sites/<id>/logs?tail=&search=` | 日志(内存环 + 落盘日志) |
| `GET /sites/<id>/health` | HTTP 健康探测 |
| `POST /sites/<id>/reclaim` | 端口被占用(孤儿进程)时释放 `{ confirm: true }` |
| `POST /sites/<id>/git/init` | 目录还不是仓库时初始化(默认分支 `main`,幂等) |
| `GET /sites/<id>/git/status` | 分支/变更/领先落后/最近提交 |
| `POST /sites/<id>/git/commit` | 暂存全部并提交 `{ message }` |
| `POST /sites/<id>/git/push` | 推送 `{ remote?, branch?, token? }` |
| `POST /sites/<id>/deploy` | 发布 `{ target, dryRun?, allowInstall?, overrides? }` |
| `POST /sites/<id>/deploy-plan` | 预览发布计划 `{ target, detect? }`(detect 默认 true,只读探测服务器) |
| `GET/POST /targets`,`DELETE /targets/<name>`,`POST /targets/<name>/ping`,`POST /targets/<name>/detect` | 发布目标管理、连通性测试与环境探测 |
| `GET /keys`,`POST /keys`,`DELETE /keys/<name>` | 密钥保险库(只返回元数据与指纹) |
| `POST /targets/import` | 从"一台服务器一个目录"的档案(pem + 说明)一键建档 |
| `POST /targets/<name>/provision` | **描述驱动建档**:`{ text, user?, discover? }` → 解析描述 + 连服务器读配置 + 回填发布字段 |
| `GET /scripts`,`GET /scripts/<name>`,`POST /scripts`,`DELETE /scripts/<name>` | 部署脚本仓库 |
| `GET /schedules`,`POST /schedules`,`DELETE /schedules/<id>` | 定时发布排期(入库、重启后自动恢复) |
| `GET /sites/<id>/releases?target=`,`POST /sites/<id>/rollback` | release 模式的发布历史与回滚 |
| `GET /status` | 站点数与运行中数量(面板徽标用) |
| `GET /monitor` | **监视快照**:`{ summary, machine, sites, tasks, probeError, sampledAt }` —— 机器 CPU/内存/磁盘(按 tick 差值算,首次为 `null` 而不是编一个数)、各站点进程的 CPU/内存/pid、以及任务名列表(已过滤采样进程本身) |
## SSH 部署
### 描述驱动建档(推荐入口)
不用手填一堆字段:把环境说明**整段文本**交给插件即可。文本存在 `envText`(数据库里),目录只需要 pem。
```powershell
# 面板:目标区最上方「环境描述」文本框 → 「按描述建档并连服务器确认」
POST /targets/<name>/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 失败也不丢)→ **连服务器读它自己的配置**(`<Context docBase>` / 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/<id> → 取消(pending 才可取消)
```
到点执行的**与手动发布完全同一条代码路径**,并把每步结果(含失败步骤名)写回排期记录;DSH 重启后启动时重新装订所有 pending 排期,长时间等待自动续订。面板上可用 `datetime-local` 直接设定。Agent 工具:`site_schedule_publish` / `site_schedule_list` / `site_schedule_cancel`。
### 密钥保险库
```powershell
# 从本机已有文件导入(推荐:内容不进对话、不进日志)
POST /keys { "name": "prod", "path": "E:\\keys\\prod.pem" }
# 面板上传路径(content 直传,同样只回元数据)
POST /keys { "name": "prod", "content": "-----BEGIN RSA PRIVATE KEY-----\n..." }
```
- 落盘位置:`<dataDir>/keys/<name>.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` | 是否备份(默认**开**)、备份目录(默认 `<uploadDir>.bak`)、保留份数(默认 3) |
| `scriptName` | 关联的部署脚本名(默认 `<目标名>-deploy`) |
### 服务器档案一键导入(一个目录一台服务器)
约定目录结构(与 `<workspace>\keys\` 一致):
```
keys/<服务器名>/
<服务器名>.pem 私钥(导入保险库,内容不外显)
hostreadme.txt ip / ssh端口 / 描述 / 应用目录 / 发布目录(UTF-8 或 GBK 都能读)
```
```powershell
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/<name>/detect`(工具 `site_server_detect`)不只是看一眼——它会:
1. **读服务器自己的配置**:Tomcat 的 `<Context path="/" docBase="…">`、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 <releasesDir>/<YYYYMMDD-HHMMSS> → 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**:发布脚本存在 `<dataDir>/scripts/<name>.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/<id>/health` 在失败时除 `{ ok, status, ms, url }` 外还会带 `error`(`no port or url configured` / `timeout` / 异常信息)
- `POST /sites/<id>/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 与工具返回中一律脱敏。
Install
dsh plugin --profile web add github:TodayWei/dsh-wei-sitecontrol
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-wei-sitecontrol from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.