dsh-vps 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +52 -7
- package/README.md +52 -7
- package/bin/dsh-vps +138 -11
- package/gate/server.js +283 -8
- package/install.sh +5 -2
- package/package.json +1 -1
- package/uninstall.sh +3 -0
- package/versions.json +16 -2
package/README.en.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/AIcivilization/deepseek-harness-vps" alt="MIT license"></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/dsh-vps"><img src="https://img.shields.io/npm/v/dsh-vps" alt="npm version"></a>
|
|
10
10
|
<img src="https://img.shields.io/badge/platform-Ubuntu%2022.04%2B%20%2F%20Debian%2012%2B-blue" alt="Platform: Ubuntu 22.04+ / Debian 12+">
|
|
11
|
-
<img src="https://img.shields.io/badge/DeepSeek%20Harness-0.1.
|
|
11
|
+
<img src="https://img.shields.io/badge/DeepSeek%20Harness-0.1.7--rc.1-4176E6" alt="DeepSeek Harness 0.1.7-rc.1">
|
|
12
12
|
<img src="https://img.shields.io/badge/runtime%20dependencies-0-brightgreen" alt="Runtime dependencies: 0">
|
|
13
13
|
<img src="https://img.shields.io/badge/Docker-not%20required-orange" alt="No Docker required">
|
|
14
14
|
<img src="https://img.shields.io/github/stars/AIcivilization/deepseek-harness-vps?style=social" alt="star">
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
> **This is a VPS deployment tool for DSH, not a plugin you install inside DSH. Run the one-command install below on your VPS and it sets up DSH itself, complete with a login page.**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/main/install.sh | sudo bash -s
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With a domain, or behind the GFW, append arguments at the end, e.g. `bash -s -- --domain dsh.example.com --mirror cn`. See [Install](#install) below.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
24
34
|
## Overview
|
|
25
35
|
|
|
26
36
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) trusts the operator's own browser only: its privileged interfaces — settings, API keys, plugin management — sit behind a browser-trust fence, and a plain reverse proxy to the public internet leaves those pages dead.
|
|
@@ -80,7 +90,7 @@ dsh-vps puts a zero-dependency login gateway (dsh-gate) in front of DSH: public
|
|
|
80
90
|
|
|
81
91
|
- Ubuntu 22.04+ / Debian 12+ (root)
|
|
82
92
|
- 2 vCPU / 2 GB RAM or better; ports 80/443 open
|
|
83
|
-
- A domain gives you automatic HTTPS; a public IP with a self-signed certificate works too
|
|
93
|
+
- A domain gives you automatic HTTPS (point its A record at the server first); a public IP with a self-signed certificate works too
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
|
@@ -93,12 +103,47 @@ curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps
|
|
|
93
103
|
|
|
94
104
|
Add `--mirror cn` if you're behind the GFW (Node and DSH come from npmmirror).
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
No domain yet? Drop `--domain` and use the public IP:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/main/install.sh | sudo bash -s
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The certificate then comes from Caddy's internal CA, so the browser warns "Not secure / certificate not trusted". That is expected — proceed anyway. Add a domain in the wizard later and Caddy switches to a real certificate.
|
|
113
|
+
|
|
114
|
+
Or install through npm, running exactly the scripts shipped in this package (pinned, no network fetch; needs Node 22+ on the machine):
|
|
97
115
|
|
|
98
116
|
```bash
|
|
99
117
|
npx dsh-vps-install install --domain dsh.example.com
|
|
100
118
|
```
|
|
101
119
|
|
|
120
|
+
To pin a release instead of `main`, replace `main` with the version tag and point later gateway fetches at the same tag:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/v1.4.3/install.sh \
|
|
124
|
+
| sudo DSHVPS_RAW_BASE=https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/v1.4.3 bash -s -- --domain dsh.example.com
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If the final self-check reports that DSH is not ready yet, the setup link is still printed; the page shows startup progress and the actual error. Troubleshoot with `journalctl -u dsh-gate -n 80 --no-pager`.
|
|
128
|
+
|
|
129
|
+
## Update
|
|
130
|
+
|
|
131
|
+
**DSH itself follows official releases.** The gateway checks npm for the newest official version (the higher of the `latest` and `next` channels) every 6 hours. When a newer one exists, a prompt appears in the bottom-right corner of the DSH page; click "Upgrade now" and confirm. It backs up, installs the new version and runs a self-check, rolling back to the previous version automatically if the check fails — about 1–3 minutes in all. "Later" silences that version. From the server you can also run:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
sudo dsh-vps upgrade # the version verified by this project
|
|
135
|
+
sudo dsh-vps upgrade --latest # the latest official release (same as "Upgrade now")
|
|
136
|
+
sudo dsh-vps rollback # back to the previous version
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Updating the gateway itself** (login page, proxy):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
sudo dsh-vps update-gate
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Machines installed before v1.5.0: run the one-command install above once more to get the in-page upgrade prompt (it enters repair/update mode; accounts and data are kept).
|
|
146
|
+
|
|
102
147
|
## Uninstall
|
|
103
148
|
|
|
104
149
|
```bash
|
|
@@ -106,13 +151,13 @@ curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps
|
|
|
106
151
|
| sudo bash -s -- --yes
|
|
107
152
|
```
|
|
108
153
|
|
|
109
|
-
Removes the service, install directory, Caddy site block and the DSH data directory, packing a backup to `/root/dsh-vps-uninstall-<timestamp>.tar.gz` first. `--keep-data` keeps the DSH data, `--purge-caddy` removes Caddy as well. Uninstall then install again gives you a clean environment.
|
|
154
|
+
Removes the service, install directory, Caddy site block and the DSH data directory, packing a backup (Caddyfile included) to `/root/dsh-vps-uninstall-<timestamp>.tar.gz` first. The Caddyfile you had before installing is restored, and only a wg0 created by `dsh-vps vpn` is removed — your own WireGuard setup is left alone. `--keep-data` keeps the DSH data, `--purge-caddy` removes Caddy as well. Uninstall then install again gives you a clean environment.
|
|
110
155
|
|
|
111
156
|
---
|
|
112
157
|
|
|
113
158
|
## First run
|
|
114
159
|
|
|
115
|
-
Installation prints a **setup link carrying a one-time token** — open it to reach the wizard: admin username/password → (optional) domain, DeepSeek API key & bundled plugins → log in. The wizard only answers to holders of the token, and the token is voided once setup completes. Lost the link? Print it again:
|
|
160
|
+
Installation prints a **setup link carrying a one-time token** — open it to reach the wizard (opening the bare IP or domain only shows a "setup token required" page, by design): admin username/password → (optional) domain, DeepSeek API key & bundled plugins → log in. The wizard only answers to holders of the token, and the token is voided once setup completes. Lost the link? Print it again:
|
|
116
161
|
|
|
117
162
|
```bash
|
|
118
163
|
sudo dsh-vps setup-url
|
|
@@ -120,7 +165,7 @@ sudo dsh-vps setup-url
|
|
|
120
165
|
|
|
121
166
|
Skipping the API key is fine; you can add it later via the "Add API key" prompt or Settings → Models → DeepSeek.
|
|
122
167
|
|
|
123
|
-
The checked plugins install in the background (tens of seconds) and DSH restarts to activate them. Two of them:
|
|
168
|
+
The checked plugins install in the background (tens of seconds), always at their latest npm version, and DSH restarts to activate them. Two of them:
|
|
124
169
|
|
|
125
170
|
- **dsh-market**: browse, search and install community plugins and themes from Settings. Everything else is left to you — add whatever you want from the marketplace afterwards
|
|
126
171
|
- **dsh-vps-manager**: manage this very VPS from inside DSH — `/vps-` queries that skip the model and cost no tokens, a terminal in the conversation, AI operations confirmed by risk level, and a recipe library. Add this machine under Settings → VPS Manager (SSH key login)
|
|
@@ -167,7 +212,7 @@ sudo dsh-vps update-gate # pull and restart the gateway code itself (no git re
|
|
|
167
212
|
sudo dsh-vps ownshost on # apply the settings-page patch (restores settings on a public domain)
|
|
168
213
|
sudo dsh-vps selfcheck # run the regression self-check (login / RPC / WebSocket / session)
|
|
169
214
|
sudo dsh-vps rollback # switch back to the previous DSH version
|
|
170
|
-
sudo dsh-vps reset-admin # emergency reset if you lose the admin password (re-runs the wizard, new token)
|
|
215
|
+
sudo dsh-vps reset-admin # emergency reset if you lose the admin password (re-runs the wizard, new token, signs out every session)
|
|
171
216
|
sudo dsh-vps setup-url # re-print the setup link with its one-time token
|
|
172
217
|
sudo dsh-vps backup # back up data (keeps the latest 3)
|
|
173
218
|
sudo dsh-vps vpn setup macbook # build a WireGuard tunnel and switch to "tunnel only"
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/AIcivilization/deepseek-harness-vps" alt="MIT 许可证"></a>
|
|
10
10
|
<a href="https://www.npmjs.com/package/dsh-vps"><img src="https://img.shields.io/npm/v/dsh-vps" alt="npm 版本"></a>
|
|
11
11
|
<img src="https://img.shields.io/badge/platform-Ubuntu%2022.04%2B%20%2F%20Debian%2012%2B-blue" alt="平台:Ubuntu 22.04+ / Debian 12+">
|
|
12
|
-
<img src="https://img.shields.io/badge/DeepSeek%20Harness-0.1.
|
|
12
|
+
<img src="https://img.shields.io/badge/DeepSeek%20Harness-0.1.7--rc.1-4176E6" alt="DeepSeek Harness 0.1.7-rc.1">
|
|
13
13
|
<img src="https://img.shields.io/badge/runtime%20dependencies-0-brightgreen" alt="运行时依赖:0">
|
|
14
14
|
<img src="https://img.shields.io/badge/Docker-not%20required-orange" alt="无需 Docker">
|
|
15
15
|
<img src="https://img.shields.io/github/stars/AIcivilization/deepseek-harness-vps?style=social" alt="star">
|
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> **这是 DSH 的 VPS 部署工具,不是在 DSH 里安装的插件,在 VPS 上执行下面的一键安装命令,它会装好带登录界面的 DSH 本身。**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/main/install.sh | sudo bash -s
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
有域名、在国内网络时,在末尾追加参数,例如 `bash -s -- --domain dsh.example.com --mirror cn`,详见下方[安装](#安装)。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
25
35
|
## 简介
|
|
26
36
|
|
|
27
37
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)默认只认本机浏览器:它的特权接口(设置、API Key、插件管理)受浏览器信任围栏保护,直接反代到公网后这些页面全部失效。
|
|
@@ -85,7 +95,7 @@ dsh-vps puts a zero-dependency login gateway (dsh-gate) in front of DSH: public
|
|
|
85
95
|
|
|
86
96
|
- Ubuntu 22.04+ / Debian 12+(root)
|
|
87
97
|
- 2C2G 以上,端口 80/443 开放
|
|
88
|
-
- 有域名即可自动 HTTPS
|
|
98
|
+
- 有域名即可自动 HTTPS(先把 A 记录解析到本机);暂无域名也能先用公网 IP + 自签证书
|
|
89
99
|
|
|
90
100
|
---
|
|
91
101
|
|
|
@@ -103,12 +113,47 @@ curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps
|
|
|
103
113
|
| sudo bash -s -- --domain dsh.example.com --mirror cn
|
|
104
114
|
```
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
还没有域名?去掉 `--domain` 即可,按公网 IP 访问:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/main/install.sh | sudo bash -s
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
此时证书由 Caddy 内置 CA 自签,浏览器会提示「不安全 / 证书不受信任」,属预期现象,选择继续访问即可。之后在向导里填上域名,Caddy 自动换成正式证书。
|
|
123
|
+
|
|
124
|
+
走 npm 也行,装的是这个包自带的同一份脚本(版本固定,不联网拉取;需本机已有 Node 22+):
|
|
107
125
|
|
|
108
126
|
```bash
|
|
109
127
|
npx dsh-vps-install install --domain dsh.example.com
|
|
110
128
|
```
|
|
111
129
|
|
|
130
|
+
想固定在某个发布版本(而非 `main`),把地址里的 `main` 换成版本 tag,并让后续拉取的网关文件也用同一版本:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/v1.4.3/install.sh \
|
|
134
|
+
| sudo DSHVPS_RAW_BASE=https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps/v1.4.3 bash -s -- --domain dsh.example.com
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
安装末尾的自检若报「DSH 尚未就绪」,设置链接照样会打印;页面会显示启动进度与具体错误,排障见 `journalctl -u dsh-gate -n 80 --no-pager`。
|
|
138
|
+
|
|
139
|
+
## 更新
|
|
140
|
+
|
|
141
|
+
**DSH 本身跟随官方版本升级。** 网关每 6 小时检查一次 npm 上的官方最新版(正式渠道 latest 与预览渠道 next 中较新的一个),有新版本时 DSH 页面右下角弹出提示,点「立即升级」并确认即可:自动备份 → 安装新版 → 自检,自检不通过自动回滚到原版本,全程约 1–3 分钟。点「稍后」则这个版本不再提示。也可以在服务器上手动执行:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
sudo dsh-vps upgrade # 升到本项目验证过的版本
|
|
145
|
+
sudo dsh-vps upgrade --latest # 升到官方最新版(与页面上的「立即升级」相同)
|
|
146
|
+
sudo dsh-vps rollback # 切回上一个版本
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**网关(登录页、代理)自身的更新**:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sudo dsh-vps update-gate
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
v1.5.0 之前安装的机器,重跑一次上方的一键安装命令即可获得页面升级提示(进入修复/更新模式,账号与数据保留)。
|
|
156
|
+
|
|
112
157
|
## 卸载
|
|
113
158
|
|
|
114
159
|
```bash
|
|
@@ -116,13 +161,13 @@ curl -fsSL https://raw.githubusercontent.com/AIcivilization/deepseek-harness-vps
|
|
|
116
161
|
| sudo bash -s -- --yes
|
|
117
162
|
```
|
|
118
163
|
|
|
119
|
-
删除服务、安装目录、Caddy 站点块与 DSH
|
|
164
|
+
删除服务、安装目录、Caddy 站点块与 DSH 数据目录,删除前打包备份(含 Caddyfile)到 `/root/dsh-vps-uninstall-<时间戳>.tar.gz`。安装前原有的 Caddyfile 会被还原;WireGuard 只移除 `dsh-vps vpn` 建的 wg0,你自己的隧道配置不动。`--keep-data` 保留 DSH 数据,`--purge-caddy` 连 Caddy 一起移除。卸载完再跑安装命令即为全新环境。
|
|
120
165
|
|
|
121
166
|
---
|
|
122
167
|
|
|
123
168
|
## 首次使用
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
安装结束时终端会打印一条**带一次性令牌的初始设置链接**,浏览器打开它进入向导(直接打开 IP 或域名只会看到「需要启动令牌」页,这是有意的):填管理员用户名/密码 → (可选)域名、DeepSeek API Key 与预置插件 → 登录即用。向导只对持有令牌的人开放,令牌在设置完成后自动作废。链接丢了随时重取:
|
|
126
171
|
|
|
127
172
|
```bash
|
|
128
173
|
sudo dsh-vps setup-url
|
|
@@ -130,7 +175,7 @@ sudo dsh-vps setup-url
|
|
|
130
175
|
|
|
131
176
|
API Key 跳过也无妨,登录后仍可在「添加 API Key」引导或 设置 → 模型 → DeepSeek 中补填。
|
|
132
177
|
|
|
133
|
-
|
|
178
|
+
勾选的插件在后台安装(约几十秒),始终取 npm 上的最新版本,装完 DSH 自动重启生效,共两款:
|
|
134
179
|
|
|
135
180
|
- **dsh-market**:设置页内浏览、搜索、一键安装社区插件与主题。其余插件留给你自己挑,装好后在市场里按需添加
|
|
136
181
|
- **dsh-vps-manager**:在 DSH 里直接管理这台 VPS——`/vps-` 系列查询命令不经过模型、不花 token,对话内嵌终端,AI 操作按风险分级确认,另有运维菜谱库。需在 设置 → VPS Manager 中添加本机(SSH 密钥登录)
|
|
@@ -177,7 +222,7 @@ sudo dsh-vps update-gate # 拉取并重启网关自身代码(安装目录非
|
|
|
177
222
|
sudo dsh-vps ownshost on # 应用设置页可用性补丁(公网域名下恢复设置页)
|
|
178
223
|
sudo dsh-vps selfcheck # 跑一遍回归自检(登录 / RPC / WebSocket / 会话)
|
|
179
224
|
sudo dsh-vps rollback # 切回上一 DSH 版本
|
|
180
|
-
sudo dsh-vps reset-admin #
|
|
225
|
+
sudo dsh-vps reset-admin # 忘记管理员密码时的应急重置(重新走向导并换发令牌,已登录的会话全部失效)
|
|
181
226
|
sudo dsh-vps setup-url # 重新打印带一次性令牌的初始设置链接
|
|
182
227
|
sudo dsh-vps backup # 备份数据(保留最近 3 份)
|
|
183
228
|
sudo dsh-vps vpn setup macbook # 建 WireGuard 隧道并切到「仅隧道可访问」
|
package/bin/dsh-vps
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# tunnel 仅我可访问:SSH 本地端口转发(零安装,打印用法)
|
|
15
15
|
# backup 打包 DSH_HOME + state 到 backups/(保留最近 3 份)
|
|
16
16
|
# selfcheck 手动触发升级回归自检探针
|
|
17
|
+
# install-units 安装浏览器一键升级所需的 systemd 单元(install.sh / update-gate 调用)
|
|
18
|
+
# upgrade-run 执行浏览器发起的升级请求(由 dsh-vps-upgrade.service 以 root 调用)
|
|
17
19
|
|
|
18
20
|
set -euo pipefail
|
|
19
21
|
|
|
@@ -30,9 +32,13 @@ VERSIONS_URL="${DSHVPS_VERSIONS_URL:-$RAW_BASE/versions.json}"
|
|
|
30
32
|
# ——2026-09-22 上游漏发 rc.3 的 dsh-client-ui-sidebar-documentpreview 就导致过 ETARGET。
|
|
31
33
|
# 同日 03:46 的 cordis 系列新版本同样与 rc.2 不兼容(HMR 服务缺失致启动崩溃),故取 03:40。
|
|
32
34
|
# 升级 latest_verified 时必须同步推后这个值。
|
|
33
|
-
DEFAULT_RESOLVE_BEFORE="${DSHVPS_RESOLVE_BEFORE:-2026-09-
|
|
35
|
+
DEFAULT_RESOLVE_BEFORE="${DSHVPS_RESOLVE_BEFORE:-2026-09-24T08:20:00Z}"
|
|
34
36
|
VERSIONS_CACHE="$ROOT/state/versions.cache.json"
|
|
35
37
|
NPM_REGISTRY="${DSHVPS_NPM_REGISTRY:-}"
|
|
38
|
+
# 安装时选了 --mirror cn:升级也走 npmmirror,与首次安装同源
|
|
39
|
+
if [[ -z "$NPM_REGISTRY" && -f "$ROOT/state/config.json" ]] && grep -q '"mirror": *"cn"' "$ROOT/state/config.json"; then
|
|
40
|
+
NPM_REGISTRY="https://registry.npmmirror.com"
|
|
41
|
+
fi
|
|
36
42
|
|
|
37
43
|
log() { printf '\033[1;32m[dsh-vps]\033[0m %s\n' "$*"; }
|
|
38
44
|
warn() { printf '\033[1;33m[dsh-vps]\033[0m %s\n' "$*" >&2; }
|
|
@@ -92,11 +98,21 @@ cmd_update_gate() {
|
|
|
92
98
|
rm -f "$sb"
|
|
93
99
|
warn "gate/site-block.js 未更新(保留现有版本)"
|
|
94
100
|
fi
|
|
101
|
+
# dsh-vps 自身:先写临时文件再 rename,正在执行的旧脚本仍持有旧 inode,不会读到半截新内容
|
|
102
|
+
local nb="$ROOT/bin/dsh-vps.update"
|
|
103
|
+
if curl -fsSL --max-time 30 "$RAW_BASE/bin/dsh-vps" -o "$nb" && bash -n "$nb"; then
|
|
104
|
+
chmod 755 "$nb"
|
|
105
|
+
mv "$nb" "$ROOT/bin/dsh-vps"
|
|
106
|
+
else
|
|
107
|
+
rm -f "$nb"
|
|
108
|
+
warn "bin/dsh-vps 未更新(保留现有版本)"
|
|
109
|
+
fi
|
|
95
110
|
log "重启 $SERVICE ..."
|
|
96
111
|
systemctl restart "$SERVICE"
|
|
97
112
|
sleep 3
|
|
98
|
-
#
|
|
99
|
-
|
|
113
|
+
# 以下都用新版 dsh-vps 执行:静态补丁(含升级提示条)与一键升级单元(幂等)
|
|
114
|
+
"$ROOT/bin/dsh-vps" ownshost on >/dev/null 2>&1 || true
|
|
115
|
+
"$ROOT/bin/dsh-vps" install-units || warn "一键升级单元安装失败"
|
|
100
116
|
if curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then
|
|
101
117
|
log "更新完成,gate 已就绪"
|
|
102
118
|
else
|
|
@@ -194,7 +210,7 @@ switch_version() { # $1=版本目录名
|
|
|
194
210
|
|
|
195
211
|
cmd_upgrade() {
|
|
196
212
|
require_root
|
|
197
|
-
local target="" latest_mode=0 assume_yes=0
|
|
213
|
+
local target="" latest_mode=0 assume_yes=0 verified_latest=0
|
|
198
214
|
while [[ $# -gt 0 ]]; do
|
|
199
215
|
case "$1" in
|
|
200
216
|
--latest) latest_mode=1; shift ;;
|
|
@@ -211,11 +227,44 @@ cmd_upgrade() {
|
|
|
211
227
|
local resolve_before="$DEFAULT_RESOLVE_BEFORE"
|
|
212
228
|
|
|
213
229
|
if [[ $latest_mode -eq 1 ]]; then
|
|
214
|
-
warn "--latest
|
|
230
|
+
warn "--latest:将安装官方最新版(npm latest 与 next 渠道中较新的一个),可能未经 dsh-vps 验证(有备份与自动回滚兜底)"
|
|
215
231
|
[[ $assume_yes -eq 1 ]] || { read -rp "确认继续?[y/N] " ans; [[ "$ans" == "y" || "$ans" == "Y" ]] || die "已取消"; }
|
|
216
|
-
local reg_arg=(
|
|
232
|
+
local reg_arg=(dist-tags --json)
|
|
217
233
|
[[ -n "$NPM_REGISTRY" ]] && reg_arg+=(--registry="$NPM_REGISTRY")
|
|
218
|
-
|
|
234
|
+
# 官方同时维护 latest(正式)与 next(预览)两个渠道,取其中版本号更高的;alpha 等内部渠道不取
|
|
235
|
+
target=$(npm view @deepseek-ai/dsh "${reg_arg[@]}" 2>/dev/null | node -e '
|
|
236
|
+
const t = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
237
|
+
const parse = (v) => { const [c, p] = v.split("-", 2); return { c: c.split(".").map(Number), p: p === undefined ? null : p.split(".") }; };
|
|
238
|
+
const cmp = (a, b) => {
|
|
239
|
+
const x = parse(a), y = parse(b);
|
|
240
|
+
for (let i = 0; i < 3; i++) if (x.c[i] !== y.c[i]) return x.c[i] - y.c[i];
|
|
241
|
+
if (x.p === null || y.p === null) return (x.p === null) - (y.p === null);
|
|
242
|
+
for (let i = 0; i < Math.max(x.p.length, y.p.length); i++) {
|
|
243
|
+
const p = x.p[i], q = y.p[i];
|
|
244
|
+
if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
|
|
245
|
+
const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
|
|
246
|
+
if (pn && qn && +p !== +q) return +p - +q;
|
|
247
|
+
if (pn !== qn) return pn ? -1 : 1;
|
|
248
|
+
if (p !== q) return p < q ? -1 : 1;
|
|
249
|
+
}
|
|
250
|
+
return 0;
|
|
251
|
+
};
|
|
252
|
+
const c = [t.latest, t.next].filter((v) => typeof v === "string" && /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v));
|
|
253
|
+
if (!c.length) process.exit(1);
|
|
254
|
+
console.log(c.sort(cmp).pop());
|
|
255
|
+
') || die "无法查询 npm 最新版本"
|
|
256
|
+
# 官方最新版恰好是我们验证过的版本:用验证时冻结的依赖树,而不是当下重新解析
|
|
257
|
+
local vt
|
|
258
|
+
if vt=$(curl -fsSL --max-time 10 "$VERSIONS_URL" 2>/dev/null) \
|
|
259
|
+
&& [[ "$(printf '%s' "$vt" | sed -n 's/.*"latest_verified": *"\([^"]*\)".*/\1/p' | head -1)" == "$target" ]]; then
|
|
260
|
+
local vrb
|
|
261
|
+
vrb=$(printf '%s' "$vt" | sed -n 's/.*"resolve_before": *"\([^"]*\)".*/\1/p' | head -1)
|
|
262
|
+
if [[ -n "$vrb" ]]; then
|
|
263
|
+
resolve_before="$vrb"
|
|
264
|
+
verified_latest=1
|
|
265
|
+
log "$target 是已验证版本,按验证时的依赖树安装(冻结于 $vrb)"
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
219
268
|
elif [[ -z "$target" ]]; then
|
|
220
269
|
# 默认:已验证版本表的 latest_verified
|
|
221
270
|
local versions
|
|
@@ -248,7 +297,7 @@ cmd_upgrade() {
|
|
|
248
297
|
local args=(--no-audit --no-fund --loglevel=error)
|
|
249
298
|
[[ -n "$NPM_REGISTRY" ]] && args+=(--registry="$NPM_REGISTRY")
|
|
250
299
|
local frozen=1
|
|
251
|
-
[[ $latest_mode -eq 1 || "$resolve_before" == "none" ]] && frozen=0
|
|
300
|
+
[[ ($latest_mode -eq 1 && $verified_latest -eq 0) || "$resolve_before" == "none" ]] && frozen=0
|
|
252
301
|
[[ $frozen -eq 1 ]] && args+=(--before="$resolve_before")
|
|
253
302
|
if ! npm install --prefix "$prefix" "${args[@]}" "@deepseek-ai/dsh@$target"; then
|
|
254
303
|
if [[ $frozen -eq 1 ]]; then
|
|
@@ -421,15 +470,21 @@ const MARK = "dsh-vps:ownshost";
|
|
|
421
470
|
const SNIPPET =
|
|
422
471
|
'<script data-dsh-vps="ownshost">/* ' + MARK +
|
|
423
472
|
" */window.__DSH_TRANSPORT__=Object.assign(window.__DSH_TRANSPORT__||{},{ownsHost:true});</script>";
|
|
473
|
+
// 升级提示条:同源脚本,由 gate 提供(/gate/ui.js)
|
|
474
|
+
const UI_MARK = "dsh-vps:ui";
|
|
475
|
+
const UI_SNIPPET = '<script data-dsh-vps="' + UI_MARK + '" src="/gate/ui.js" defer></script>';
|
|
424
476
|
let s = fs.readFileSync(file, "utf8");
|
|
425
477
|
const orig = file + ".orig";
|
|
426
478
|
if (mode === "off") {
|
|
427
479
|
if (fs.existsSync(orig)) fs.copyFileSync(orig, file);
|
|
428
|
-
else fs.writeFileSync(file, s.split("\n").filter((l) => !l.includes(MARK)).join("\n"));
|
|
480
|
+
else fs.writeFileSync(file, s.split("\n").filter((l) => !l.includes(MARK) && !l.includes(UI_MARK)).join("\n"));
|
|
429
481
|
console.log("off");
|
|
430
482
|
process.exit(0);
|
|
431
483
|
}
|
|
432
|
-
|
|
484
|
+
const missing = [];
|
|
485
|
+
if (!s.includes(MARK)) missing.push(SNIPPET);
|
|
486
|
+
if (!s.includes(UI_MARK)) missing.push(UI_SNIPPET);
|
|
487
|
+
if (missing.length === 0) {
|
|
433
488
|
console.log("present");
|
|
434
489
|
process.exit(0);
|
|
435
490
|
}
|
|
@@ -440,7 +495,7 @@ if (m === null) {
|
|
|
440
495
|
process.exit(1);
|
|
441
496
|
}
|
|
442
497
|
const at = m.index + m[0].length;
|
|
443
|
-
fs.writeFileSync(file, s.slice(0, at) + "\n " +
|
|
498
|
+
fs.writeFileSync(file, s.slice(0, at) + missing.map((x) => "\n " + x).join("") + s.slice(at));
|
|
444
499
|
console.log("patched");
|
|
445
500
|
NODE
|
|
446
501
|
node "$tmp" "$idx" "$mode" || rc=$?
|
|
@@ -883,6 +938,76 @@ EOF
|
|
|
883
938
|
|
|
884
939
|
## endregion
|
|
885
940
|
|
|
941
|
+
## region: 浏览器一键升级
|
|
942
|
+
|
|
943
|
+
# 页面上点「立即升级」→ gate(dsh 用户,无特权)只写 state/upgrade.request →
|
|
944
|
+
# systemd path 单元看到文件 → 以 root 跑 dsh-vps upgrade-run。网页拿不到 root,
|
|
945
|
+
# 能触发的只有这一条固定命令:升级到 npm 官方 latest(备份 → 自检 → 失败自动回滚)。
|
|
946
|
+
UPGRADE_REQUEST="$ROOT/state/upgrade.request"
|
|
947
|
+
UPGRADE_STATUS="$ROOT/state/upgrade.status.json"
|
|
948
|
+
UPGRADE_LOG="$ROOT/state/upgrade.log"
|
|
949
|
+
|
|
950
|
+
cmd_install_units() {
|
|
951
|
+
require_root
|
|
952
|
+
cat >/etc/systemd/system/dsh-vps-upgrade.path <<UNIT
|
|
953
|
+
# dsh-vps:监听浏览器发起的 DSH 升级请求(由 dsh-vps install-units 生成)
|
|
954
|
+
[Unit]
|
|
955
|
+
Description=dsh-vps: watch for browser-initiated DSH upgrade requests
|
|
956
|
+
|
|
957
|
+
[Path]
|
|
958
|
+
PathExists=$UPGRADE_REQUEST
|
|
959
|
+
Unit=dsh-vps-upgrade.service
|
|
960
|
+
|
|
961
|
+
[Install]
|
|
962
|
+
WantedBy=multi-user.target
|
|
963
|
+
UNIT
|
|
964
|
+
cat >/etc/systemd/system/dsh-vps-upgrade.service <<UNIT
|
|
965
|
+
# dsh-vps:执行浏览器发起的 DSH 升级(由 dsh-vps install-units 生成)
|
|
966
|
+
[Unit]
|
|
967
|
+
Description=dsh-vps: run a DSH upgrade requested from the browser
|
|
968
|
+
|
|
969
|
+
[Service]
|
|
970
|
+
Type=oneshot
|
|
971
|
+
ExecStart=$ROOT/bin/dsh-vps upgrade-run
|
|
972
|
+
TimeoutStartSec=30min
|
|
973
|
+
UNIT
|
|
974
|
+
systemctl daemon-reload
|
|
975
|
+
systemctl enable --now dsh-vps-upgrade.path >/dev/null 2>&1 || die "启用 dsh-vps-upgrade.path 失败"
|
|
976
|
+
log "浏览器一键升级已启用(dsh-vps-upgrade.path)"
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
write_upgrade_status() { # $1=state $2=from $3=to
|
|
980
|
+
local owner
|
|
981
|
+
owner=$(stat -c %U "$ROOT/state" 2>/dev/null || echo dsh)
|
|
982
|
+
printf '{"state":"%s","from":"%s","to":"%s","at":%s}\n' "$1" "$2" "$3" "$(date +%s)000" >"$UPGRADE_STATUS"
|
|
983
|
+
chown "$owner" "$UPGRADE_STATUS" 2>/dev/null || true
|
|
984
|
+
chmod 600 "$UPGRADE_STATUS"
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
cmd_upgrade_run() {
|
|
988
|
+
require_root
|
|
989
|
+
# 先删请求文件:path 单元在文件仍存在时会再次触发
|
|
990
|
+
[[ -f "$UPGRADE_REQUEST" ]] || exit 0
|
|
991
|
+
rm -f "$UPGRADE_REQUEST"
|
|
992
|
+
local from to rc=0 owner
|
|
993
|
+
from=$(current_version)
|
|
994
|
+
write_upgrade_status running "$from" ""
|
|
995
|
+
owner=$(stat -c %U "$ROOT/state" 2>/dev/null || echo dsh)
|
|
996
|
+
: >"$UPGRADE_LOG"
|
|
997
|
+
chown "$owner" "$UPGRADE_LOG" 2>/dev/null || true
|
|
998
|
+
chmod 600 "$UPGRADE_LOG"
|
|
999
|
+
# 独立进程执行:写在 `|| rc=$?` 里的函数/子 shell 会让 set -e 失效,出错也不停
|
|
1000
|
+
"$ROOT/bin/dsh-vps" upgrade --latest --yes >>"$UPGRADE_LOG" 2>&1 || rc=$?
|
|
1001
|
+
to=$(current_version)
|
|
1002
|
+
if [[ $rc -eq 0 ]]; then
|
|
1003
|
+
write_upgrade_status success "$from" "$to"
|
|
1004
|
+
else
|
|
1005
|
+
write_upgrade_status failed "$from" "$to"
|
|
1006
|
+
fi
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
## endregion
|
|
1010
|
+
|
|
886
1011
|
usage() {
|
|
887
1012
|
cat <<'EOF'
|
|
888
1013
|
dsh-vps 管理命令
|
|
@@ -913,6 +1038,8 @@ reset-admin) shift; cmd_reset_admin "$@" ;;
|
|
|
913
1038
|
setup-url) cmd_setup_url ;;
|
|
914
1039
|
backup) cmd_backup ;;
|
|
915
1040
|
selfcheck) cmd_selfcheck ;;
|
|
1041
|
+
install-units) cmd_install_units ;;
|
|
1042
|
+
upgrade-run) cmd_upgrade_run ;;
|
|
916
1043
|
vpn) shift; cmd_vpn "$@" ;;
|
|
917
1044
|
tunnel) cmd_tunnel ;;
|
|
918
1045
|
"" | -h | --help) usage ;;
|
package/gate/server.js
CHANGED
|
@@ -94,6 +94,15 @@ const OWNS_HOST_SNIPPET =
|
|
|
94
94
|
`<script data-dsh-vps="ownshost">/* ${OWNS_HOST_MARK} */` +
|
|
95
95
|
"window.__DSH_TRANSPORT__=Object.assign(window.__DSH_TRANSPORT__||{},{ownsHost:true});</script>";
|
|
96
96
|
let ownshostLogged = false;
|
|
97
|
+
// 升级提示条:同源脚本 /gate/ui.js。bin/dsh-vps 的静态补丁同样写入它,两边共用同一标记。
|
|
98
|
+
const UI_MARK = "dsh-vps:ui";
|
|
99
|
+
const UI_SNIPPET = `<script data-dsh-vps="${UI_MARK}" src="/gate/ui.js" defer></script>`;
|
|
100
|
+
|
|
101
|
+
// DSH 版本检测:跟随官方最新版(npm latest 与 next 渠道中较新的一个)。页面上确认后,gate 只写 state/upgrade.request,
|
|
102
|
+
// 由 root 的 dsh-vps-upgrade.path/.service 执行升级(gate 自身无权改 /opt/dsh-vps/dsh)。
|
|
103
|
+
const DSH_PACKAGE = "@deepseek-ai/dsh";
|
|
104
|
+
const UPDATE_CHECK_INTERVAL_MS = 6 * 3_600_000;
|
|
105
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
97
106
|
|
|
98
107
|
// DSH 的若干特权端点(dsh-market 的 restart / backup 导出 / self-uninstall)要求
|
|
99
108
|
// "直连回环":只要出现 x-forwarded-for / x-real-ip / forwarded 任一头,
|
|
@@ -106,6 +115,16 @@ const FORWARDING_HEADERS = new Set(["forwarded", "x-forwarded-for", "x-real-ip"]
|
|
|
106
115
|
// 在 gate 之外拉起一个新的 DSH 进程,抢占 3080 端口,gate 的子进程随后
|
|
107
116
|
// EADDRINUSE 且再也抓不到 launchToken → 永久"会话尚未就绪"。
|
|
108
117
|
const MARKET_RESTART_PATHS = new Set(["/dsh-market/restart", "/dsh-market/restart/"]);
|
|
118
|
+
// dshmarket 1.64+ 的 v1 接口:内部直接调用上面那个旧端点(不经 HTTP),必须一起接管
|
|
119
|
+
const MARKET_RESTART_V1_PATHS = new Set(["/dsh-market/api/v1/restart", "/dsh-market/api/v1/restart/"]);
|
|
120
|
+
const MARKET_V1_SCHEMA = "dsh-market/update-api/v1";
|
|
121
|
+
|
|
122
|
+
// dshmarket 的写操作(安装/更新/卸载/备份导出……)为防 DNS 重绑定,要求 Host 必须是回环地址,
|
|
123
|
+
// Origin 必须与 Host 一致;经 gate 转发时 Host 是公网域名,于是一律 403 "untrusted origin"。
|
|
124
|
+
// gate 已完成登录校验,这里替它做同样的同源检查(Origin 与公网 Host 一致、非跨站),
|
|
125
|
+
// 通过后把 Host/Origin 改写成 DSH 的回环地址再转发。设 GATE_MARKET_LOOPBACK=0 可关闭。
|
|
126
|
+
const MARKET_PREFIX = "/dsh-market/";
|
|
127
|
+
const MARKET_LOOPBACK = process.env.GATE_MARKET_LOOPBACK !== "0";
|
|
109
128
|
// 设 GATE_TAKEOVER_RESTART=0 则放行给 DSH 自己处理(会退回到上面那个坑,仅供对照排障)
|
|
110
129
|
const TAKEOVER_RESTART = process.env.GATE_TAKEOVER_RESTART !== "0";
|
|
111
130
|
|
|
@@ -849,15 +868,18 @@ function collectAndInject(upRes, res, status, respHeaders) {
|
|
|
849
868
|
return;
|
|
850
869
|
}
|
|
851
870
|
// 静态文件补丁(bin/dsh-vps ownshost on)已经在页面里时不再重复注入
|
|
852
|
-
|
|
871
|
+
const missing = [];
|
|
872
|
+
if (!body.includes(OWNS_HOST_MARK)) missing.push(OWNS_HOST_SNIPPET);
|
|
873
|
+
if (!body.includes(UI_MARK)) missing.push(UI_SNIPPET);
|
|
874
|
+
if (missing.length) {
|
|
853
875
|
// 只有完整的 HTML 文档才改写;找不到 <head> 说明不是文档(片段/JSON 误标),原样转发
|
|
854
876
|
const at = body.search(/<head\b[^>]*>/i);
|
|
855
877
|
if (at !== -1) {
|
|
856
878
|
const end = body.indexOf(">", at) + 1;
|
|
857
|
-
body = body.slice(0, end) +
|
|
879
|
+
body = body.slice(0, end) + missing.join("") + body.slice(end);
|
|
858
880
|
if (!ownshostLogged) {
|
|
859
881
|
ownshostLogged = true;
|
|
860
|
-
log("injected
|
|
882
|
+
log("injected page snippets by proxy (静态文件补丁未生效,建议 sudo dsh-vps ownshost on)");
|
|
861
883
|
}
|
|
862
884
|
}
|
|
863
885
|
}
|
|
@@ -889,6 +911,11 @@ function proxyHttp(req, res) {
|
|
|
889
911
|
}
|
|
890
912
|
const cookie = upstreamCookieHeader(req.headers.cookie, authority);
|
|
891
913
|
if (cookie !== void 0) headers.cookie = cookie;
|
|
914
|
+
if (MARKET_LOOPBACK && (req.url || "").startsWith(MARKET_PREFIX)) {
|
|
915
|
+
const loopback = `127.0.0.1:${DSH_PORT}`;
|
|
916
|
+
headers.host = loopback;
|
|
917
|
+
if (headers.origin !== void 0) headers.origin = `http://${loopback}`;
|
|
918
|
+
}
|
|
892
919
|
|
|
893
920
|
let responded = false;
|
|
894
921
|
const upstreamReq = http.request(
|
|
@@ -952,6 +979,18 @@ function proxyHttp(req, res) {
|
|
|
952
979
|
req.pipe(upstreamReq);
|
|
953
980
|
}
|
|
954
981
|
|
|
982
|
+
/** 市场请求的同源检查:Origin(若有)须与浏览器访问的公网 Host 一致,且不是跨站请求。 */
|
|
983
|
+
function marketRequestSameOrigin(req) {
|
|
984
|
+
if (String(req.headers["sec-fetch-site"] || "") === "cross-site") return false;
|
|
985
|
+
const origin = req.headers.origin;
|
|
986
|
+
if (origin === void 0) return true; // 同源 GET 导航(如备份下载)不带 Origin
|
|
987
|
+
try {
|
|
988
|
+
return new URL(origin).host === requestAuthority(req.headers);
|
|
989
|
+
} catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
955
994
|
function denyUnauthenticated(req, res, pathname) {
|
|
956
995
|
if (pathname.startsWith("/api")) {
|
|
957
996
|
sendText(res, 401, "gate authentication required");
|
|
@@ -969,15 +1008,16 @@ function denyUnauthenticated(req, res, pathname) {
|
|
|
969
1008
|
* 这里按官方客户端的协议回 202 + ok,然后由 gate 自己重启 DSH 子进程:
|
|
970
1009
|
* 子进程是全新的,/dsh-market/status 的 boot id 随之变化,前端会自动 reload。
|
|
971
1010
|
*/
|
|
972
|
-
function handleMarketRestart(req, res) {
|
|
1011
|
+
function handleMarketRestart(req, res, v1) {
|
|
973
1012
|
if (req.method !== "POST") {
|
|
974
1013
|
res.writeHead(405, { allow: "POST", "content-length": "0" });
|
|
975
1014
|
res.end();
|
|
976
1015
|
return;
|
|
977
1016
|
}
|
|
978
|
-
log(
|
|
1017
|
+
log(`market restart requested${v1 ? " (v1)" : ""}; gate takes over`);
|
|
1018
|
+
const result = { ok: true, managedBy: "dsh-gate", note: "由 gate 重启 DSH 子进程" };
|
|
979
1019
|
res.writeHead(202, { "content-type": "application/json", "cache-control": "no-store" });
|
|
980
|
-
res.end(JSON.stringify({
|
|
1020
|
+
res.end(JSON.stringify(v1 ? { schema: MARKET_V1_SCHEMA, result } : result));
|
|
981
1021
|
// 让 202 先落地,再动手(客户端随后轮询 /dsh-market/status 等 boot id 变化)
|
|
982
1022
|
setTimeout(() => restartDsh("market restart"), 300);
|
|
983
1023
|
}
|
|
@@ -1573,6 +1613,229 @@ async function handleSetup(req, res) {
|
|
|
1573
1613
|
res.end();
|
|
1574
1614
|
}
|
|
1575
1615
|
|
|
1616
|
+
//#region DSH 版本检测与浏览器一键升级
|
|
1617
|
+
|
|
1618
|
+
const update = { latest: null, checkedAt: 0, error: null };
|
|
1619
|
+
|
|
1620
|
+
function upgradeRequestPath() {
|
|
1621
|
+
return path.join(STATE_DIR, "upgrade.request");
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function currentDshVersion() {
|
|
1625
|
+
try {
|
|
1626
|
+
const v = fs.readlinkSync(path.join(GATE_HOME, "dsh", "current"));
|
|
1627
|
+
return SEMVER_PATTERN.test(v) ? v : null;
|
|
1628
|
+
} catch {
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/** semver 比较(含预发布段):a>b 返回正数。 */
|
|
1634
|
+
function compareVersions(a, b) {
|
|
1635
|
+
const split = (v) => {
|
|
1636
|
+
const [core, pre] = v.split("-", 2);
|
|
1637
|
+
return { core: core.split(".").map(Number), pre: pre === void 0 ? null : pre.split(".") };
|
|
1638
|
+
};
|
|
1639
|
+
const x = split(a);
|
|
1640
|
+
const y = split(b);
|
|
1641
|
+
for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
|
|
1642
|
+
if (x.pre === null || y.pre === null) return (x.pre === null) - (y.pre === null);
|
|
1643
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
1644
|
+
const p = x.pre[i];
|
|
1645
|
+
const q = y.pre[i];
|
|
1646
|
+
if (p === void 0 || q === void 0) return p === void 0 ? -1 : 1;
|
|
1647
|
+
const pn = /^\d+$/.test(p);
|
|
1648
|
+
const qn = /^\d+$/.test(q);
|
|
1649
|
+
if (pn && qn && Number(p) !== Number(q)) return Number(p) - Number(q);
|
|
1650
|
+
if (pn !== qn) return pn ? -1 : 1;
|
|
1651
|
+
if (p !== q) return p < q ? -1 : 1;
|
|
1652
|
+
}
|
|
1653
|
+
return 0;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/** 官方 latest(正式)与 next(预览)两个渠道中版本号更高的一个;alpha 等内部渠道不取。 */
|
|
1657
|
+
function newestDshVersion() {
|
|
1658
|
+
return new Promise((resolve) => {
|
|
1659
|
+
const url = `${npmRegistry()}/-/package/${encodeURIComponent(DSH_PACKAGE).replace(/^%40/, "@")}/dist-tags`;
|
|
1660
|
+
const req = https.get(url, { headers: { accept: "application/json" }, timeout: 10_000 }, (res) => {
|
|
1661
|
+
const chunks = [];
|
|
1662
|
+
res.on("data", (c) => chunks.push(c));
|
|
1663
|
+
res.on("end", () => {
|
|
1664
|
+
try {
|
|
1665
|
+
const tags = res.statusCode === 200 ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
|
|
1666
|
+
const candidates = [tags.latest, tags.next].filter((v) => typeof v === "string" && SEMVER_PATTERN.test(v));
|
|
1667
|
+
resolve(candidates.length ? candidates.sort(compareVersions).pop() : null);
|
|
1668
|
+
} catch {
|
|
1669
|
+
resolve(null);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
});
|
|
1673
|
+
req.on("timeout", () => req.destroy());
|
|
1674
|
+
req.on("error", () => resolve(null));
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async function checkDshLatest() {
|
|
1679
|
+
const v = await newestDshVersion();
|
|
1680
|
+
update.checkedAt = Date.now();
|
|
1681
|
+
if (v) {
|
|
1682
|
+
if (v !== update.latest) log(`dsh latest on registry: ${v} (running ${currentDshVersion() || "unknown"})`);
|
|
1683
|
+
update.latest = v;
|
|
1684
|
+
update.error = null;
|
|
1685
|
+
} else {
|
|
1686
|
+
update.error = "无法查询 npm 最新版本";
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function readUpgradeStatus() {
|
|
1691
|
+
try {
|
|
1692
|
+
const st = JSON.parse(fs.readFileSync(path.join(STATE_DIR, "upgrade.status.json"), "utf8"));
|
|
1693
|
+
return st && typeof st.state === "string" ? st : null;
|
|
1694
|
+
} catch {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function updateInfo() {
|
|
1700
|
+
const current = currentDshVersion();
|
|
1701
|
+
const latest = update.latest;
|
|
1702
|
+
return {
|
|
1703
|
+
current,
|
|
1704
|
+
latest,
|
|
1705
|
+
available: Boolean(current && latest && compareVersions(latest, current) > 0),
|
|
1706
|
+
checkedAt: update.checkedAt || null,
|
|
1707
|
+
error: update.error,
|
|
1708
|
+
requested: fs.existsSync(upgradeRequestPath()),
|
|
1709
|
+
status: readUpgradeStatus(),
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/** GET:更新状态;POST:请求升级到官方最新版(写请求文件,交给 root 的 path 单元)。 */
|
|
1714
|
+
async function handleUpdate(req, res, user) {
|
|
1715
|
+
const json = (status, body) => {
|
|
1716
|
+
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
1717
|
+
res.end(JSON.stringify(body));
|
|
1718
|
+
};
|
|
1719
|
+
if (req.method === "GET") return json(200, updateInfo());
|
|
1720
|
+
if (req.method !== "POST") return json(405, { error: "method not allowed" });
|
|
1721
|
+
// 触发的是 root 操作:必须是本站页面发起(fetch POST 必带 Origin)
|
|
1722
|
+
const origin = req.headers.origin;
|
|
1723
|
+
let sameOrigin = false;
|
|
1724
|
+
try {
|
|
1725
|
+
sameOrigin = origin !== void 0 && new URL(origin).host === requestAuthority(req.headers);
|
|
1726
|
+
} catch {
|
|
1727
|
+
/* 非法 Origin */
|
|
1728
|
+
}
|
|
1729
|
+
if (!sameOrigin || String(req.headers["sec-fetch-site"] || "") === "cross-site") return json(403, { error: "cross-origin request refused" });
|
|
1730
|
+
await checkDshLatest(); // 以点击时的最新结果为准
|
|
1731
|
+
const info = updateInfo();
|
|
1732
|
+
if (info.requested || (info.status && info.status.state === "running")) return json(409, { error: "升级已在进行中" });
|
|
1733
|
+
if (!info.available) return json(409, { error: "已是最新版本" });
|
|
1734
|
+
fs.writeFileSync(upgradeRequestPath(), JSON.stringify({ target: info.latest, from: info.current, by: user, at: Date.now() }) + "\n", { mode: 0o600 });
|
|
1735
|
+
log(`upgrade requested from browser by ${user}: ${info.current} -> ${info.latest}`);
|
|
1736
|
+
return json(202, { ok: true, from: info.current, to: info.latest });
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// 注入 DSH 页面的升级提示条(同源脚本,无外部依赖)。
|
|
1740
|
+
// 只做提示与确认;真正的升级由 root 服务执行,失败会自动回滚。
|
|
1741
|
+
const UI_JS = `(function () {
|
|
1742
|
+
if (window.top !== window || document.getElementById("dshvps-update")) return;
|
|
1743
|
+
var KEY = "dshvps-update-dismissed";
|
|
1744
|
+
function store(k, v) { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) { return null; } }
|
|
1745
|
+
var box, timer, busy = false, asked = false;
|
|
1746
|
+
function el(tag, css, text) { var n = document.createElement(tag); if (css) n.style.cssText = css; if (text) n.textContent = text; return n; }
|
|
1747
|
+
var BTN = "margin-left:8px;padding:5px 12px;border-radius:7px;border:1px solid #2a3547;cursor:pointer;font:inherit;";
|
|
1748
|
+
function render(msg, actions) {
|
|
1749
|
+
if (!box) {
|
|
1750
|
+
box = el("div", "position:fixed;right:16px;bottom:16px;z-index:2147483647;max-width:min(420px,calc(100vw - 32px));" +
|
|
1751
|
+
"padding:12px 14px;border-radius:10px;background:#11161f;color:#dbe2ea;border:1px solid #2a3547;" +
|
|
1752
|
+
"box-shadow:0 8px 30px rgba(0,0,0,.35);font:13px/1.6 system-ui,-apple-system,'Segoe UI',sans-serif");
|
|
1753
|
+
box.id = "dshvps-update";
|
|
1754
|
+
document.body.appendChild(box);
|
|
1755
|
+
}
|
|
1756
|
+
box.textContent = "";
|
|
1757
|
+
box.appendChild(el("div", "", msg));
|
|
1758
|
+
if (actions && actions.length) {
|
|
1759
|
+
var row = el("div", "margin-top:8px;text-align:right");
|
|
1760
|
+
actions.forEach(function (a) {
|
|
1761
|
+
var b = el("button", BTN + (a.primary ? "background:#2563eb;color:#fff;border-color:#2563eb" : "background:#0d1219;color:#dbe2ea"), a.label);
|
|
1762
|
+
b.onclick = a.onClick;
|
|
1763
|
+
row.appendChild(b);
|
|
1764
|
+
});
|
|
1765
|
+
box.appendChild(row);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function hide() { if (box) { box.remove(); box = null; } }
|
|
1769
|
+
function get() { return fetch("/gate/update", { cache: "no-store", credentials: "same-origin" }).then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); }); }
|
|
1770
|
+
function poll() {
|
|
1771
|
+
get().then(function (u) {
|
|
1772
|
+
var st = u.status;
|
|
1773
|
+
if (u.requested || (st && st.state === "running")) {
|
|
1774
|
+
render("正在升级 DeepSeek Harness" + (u.latest ? " 到 " + u.latest : "") + "…(约 1–3 分钟,期间页面会短暂不可用,请勿关闭)");
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (st && asked && st.state === "success") {
|
|
1778
|
+
render("升级完成(" + st.from + " → " + st.to + "),正在刷新…");
|
|
1779
|
+
clearInterval(timer);
|
|
1780
|
+
setTimeout(function () { location.reload(); }, 1500);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (st && asked && st.state === "failed") {
|
|
1784
|
+
clearInterval(timer);
|
|
1785
|
+
render("升级未成功,已自动回滚到 " + (st.to || st.from) + ",当前可继续使用。详情:sudo cat /opt/dsh-vps/state/upgrade.log", [{ label: "知道了", onClick: hide }]);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}).catch(function () {
|
|
1789
|
+
if (asked) render("正在升级,DeepSeek Harness 重启中…");
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
function start() {
|
|
1793
|
+
if (busy) return;
|
|
1794
|
+
if (!window.confirm("升级期间 DeepSeek Harness 会重启,约 1–3 分钟不可用。\\n升级前自动备份;新版本自检不通过会自动回滚到当前版本。\\n\\n确认升级?")) return;
|
|
1795
|
+
busy = true;
|
|
1796
|
+
fetch("/gate/update", { method: "POST", credentials: "same-origin" }).then(function (r) {
|
|
1797
|
+
return r.json().then(function (b) { if (!r.ok) throw new Error(b.error || String(r.status)); return b; });
|
|
1798
|
+
}).then(function () {
|
|
1799
|
+
asked = true;
|
|
1800
|
+
store("dshvps-update-asked", String(Date.now()));
|
|
1801
|
+
render("已提交升级请求,等待开始…");
|
|
1802
|
+
timer = setInterval(poll, 3000);
|
|
1803
|
+
}).catch(function (e) {
|
|
1804
|
+
busy = false;
|
|
1805
|
+
render("无法开始升级:" + e.message, [{ label: "关闭", onClick: hide }]);
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
function init() {
|
|
1809
|
+
// 升级过程中刷新了页面:继续跟进,完成后给出结果
|
|
1810
|
+
var t = Number(store("dshvps-update-asked") || 0);
|
|
1811
|
+
asked = t > 0 && Date.now() - t < 30 * 60000;
|
|
1812
|
+
get().then(function (u) {
|
|
1813
|
+
var st = u.status;
|
|
1814
|
+
if (u.requested || (st && st.state === "running")) { asked = true; timer = setInterval(poll, 3000); poll(); return; }
|
|
1815
|
+
if (asked && st && st.at > t) { store("dshvps-update-asked", "0"); if (st.state === "failed") poll(); return; }
|
|
1816
|
+
if (!u.available || store(KEY) === u.latest) return;
|
|
1817
|
+
render("DeepSeek Harness 有新版本 " + u.latest + "(当前 " + u.current + ")", [
|
|
1818
|
+
{ label: "稍后", onClick: function () { store(KEY, u.latest); hide(); } },
|
|
1819
|
+
{ label: "立即升级", primary: true, onClick: start },
|
|
1820
|
+
]);
|
|
1821
|
+
}).catch(function () {});
|
|
1822
|
+
}
|
|
1823
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
|
|
1824
|
+
else init();
|
|
1825
|
+
})();
|
|
1826
|
+
`;
|
|
1827
|
+
|
|
1828
|
+
function handleUiJs(req, res) {
|
|
1829
|
+
res.writeHead(200, {
|
|
1830
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1831
|
+
"cache-control": "no-cache",
|
|
1832
|
+
"x-content-type-options": "nosniff",
|
|
1833
|
+
});
|
|
1834
|
+
res.end(req.method === "HEAD" ? void 0 : UI_JS);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
//#endregion
|
|
1838
|
+
|
|
1576
1839
|
/** 升级回归自检(dsh-vps upgrade 调用):健康 + token 兑换 + 登录态 credentials/describe 探针。 */
|
|
1577
1840
|
async function handleSelfcheck(req, res) {
|
|
1578
1841
|
const report = {
|
|
@@ -1645,6 +1908,7 @@ function main() {
|
|
|
1645
1908
|
if (pathname === "/logout") return handleLogout(req, res);
|
|
1646
1909
|
if (pathname === "/gate/health") return handleHealthGuarded(req, res);
|
|
1647
1910
|
if (pathname === "/gate/selfcheck") return handleSelfcheckGuarded(req, res);
|
|
1911
|
+
if (pathname === "/gate/ui.js") return handleUiJs(req, res);
|
|
1648
1912
|
if (pathname === "/setup") return handleSetup(req, res);
|
|
1649
1913
|
if (!loadAdmin()) {
|
|
1650
1914
|
// 尚未完成初始设置:浏览器导航导向导,/api 保持 401
|
|
@@ -1662,11 +1926,18 @@ function main() {
|
|
|
1662
1926
|
sendText(res, 503, "gate not configured: admin account missing (run `dsh-vps reset-admin`)");
|
|
1663
1927
|
return;
|
|
1664
1928
|
}
|
|
1665
|
-
|
|
1929
|
+
const user = sessionUser(req);
|
|
1930
|
+
if (!user) {
|
|
1666
1931
|
denyUnauthenticated(req, res, pathname);
|
|
1667
1932
|
return;
|
|
1668
1933
|
}
|
|
1669
|
-
if (
|
|
1934
|
+
if (pathname === "/gate/update") return handleUpdate(req, res, user);
|
|
1935
|
+
if (pathname.startsWith(MARKET_PREFIX) && !marketRequestSameOrigin(req)) {
|
|
1936
|
+
sendText(res, 403, "cross-origin market request refused by gate");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (TAKEOVER_RESTART && MARKET_RESTART_PATHS.has(pathname)) return handleMarketRestart(req, res, false);
|
|
1940
|
+
if (TAKEOVER_RESTART && MARKET_RESTART_V1_PATHS.has(pathname)) return handleMarketRestart(req, res, true);
|
|
1670
1941
|
proxyHttp(req, res);
|
|
1671
1942
|
})
|
|
1672
1943
|
.catch((err) => {
|
|
@@ -1682,6 +1953,10 @@ function main() {
|
|
|
1682
1953
|
|
|
1683
1954
|
spawnDshWithPreflight();
|
|
1684
1955
|
|
|
1956
|
+
// DSH 新版本检测:启动 1 分钟后查一次,之后每 6 小时一次
|
|
1957
|
+
setTimeout(() => checkDshLatest().catch(() => {}), 60_000).unref();
|
|
1958
|
+
setInterval(() => checkDshLatest().catch(() => {}), UPDATE_CHECK_INTERVAL_MS).unref();
|
|
1959
|
+
|
|
1685
1960
|
// DSH Cookie 续期:剩余有效期 < 24h 时用同一 launchToken 重新兑换
|
|
1686
1961
|
setInterval(() => {
|
|
1687
1962
|
if (dsh.cookie && dsh.cookie.expiresAt <= Date.now()) {
|
package/install.sh
CHANGED
|
@@ -16,7 +16,7 @@ set -euo pipefail
|
|
|
16
16
|
|
|
17
17
|
## region: 常量与参数
|
|
18
18
|
|
|
19
|
-
DSH_VERSION="0.1.
|
|
19
|
+
DSH_VERSION="0.1.7-rc.1" # 钉住版本(设计文档 §10 已验证版本表,勿随意改)
|
|
20
20
|
# 只钉顶层包版本是不够的:DSH 各子包的依赖是 ^0.1.5-rc.2 这类浮动范围,上游一发新的
|
|
21
21
|
# 预发布波次,解析结果就整体漂上去。2026-09-22 上游发了 0.1.5-rc.3 波次,但漏发了
|
|
22
22
|
# dsh-client-ui-sidebar-documentpreview,于是 ETARGET 装不上。用 --before 把解析冻结在
|
|
@@ -24,8 +24,9 @@ DSH_VERSION="0.1.5-rc.2" # 钉住版本(设计文档 §10 已验证版本表
|
|
|
24
24
|
# 注意:同一天 03:46 上游还先发了一批 cordis 系列(cordis 4.0.3 / cordis-plugin-hmr 1.0.18 等),
|
|
25
25
|
# 与 rc.2 不兼容——web profile 启动即报 "user patch-layer watching requires the Cordis HMR service"。
|
|
26
26
|
# 冻结点必须早于这一批,所以是 03:40 而不是 05:00。
|
|
27
|
+
# 之后升到 0.1.7-rc.1(npm next 渠道),冻结点 2026-09-24T08:20Z——该时刻解析出的依赖树已逐包比对、实测通过。
|
|
27
28
|
# 设为 none 可关闭冻结。
|
|
28
|
-
DSH_RESOLVE_BEFORE="${DSH_RESOLVE_BEFORE:-2026-09-
|
|
29
|
+
DSH_RESOLVE_BEFORE="${DSH_RESOLVE_BEFORE:-2026-09-24T08:20:00Z}"
|
|
29
30
|
INSTALL_ROOT="/opt/dsh-vps"
|
|
30
31
|
DSH_USER="dsh"
|
|
31
32
|
DSH_HOME_DIR="/home/dsh/.dsh"
|
|
@@ -391,6 +392,8 @@ EOF
|
|
|
391
392
|
systemctl enable dsh-gate >/dev/null 2>&1
|
|
392
393
|
systemctl restart dsh-gate
|
|
393
394
|
log "dsh-gate.service 已启动(DSH_TRUSTED_HOST=${trusted})"
|
|
395
|
+
# 浏览器一键升级:DSH 有新版本时页面提示,确认后由 root 服务执行升级
|
|
396
|
+
"$INSTALL_ROOT/bin/dsh-vps" install-units || warn "一键升级单元安装失败(不影响使用,可稍后 sudo dsh-vps install-units)"
|
|
394
397
|
}
|
|
395
398
|
|
|
396
399
|
## endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vps",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Ship DeepSeek Harness to your own VPS: one command installs a zero-dependency login gateway, automatic HTTPS through Caddy, systemd supervision, and optional WireGuard-only access.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
package/uninstall.sh
CHANGED
|
@@ -108,6 +108,9 @@ log "步骤 2/6:停止并移除 $SERVICE"
|
|
|
108
108
|
systemctl stop "$SERVICE" 2>/dev/null || true
|
|
109
109
|
systemctl disable "$SERVICE" 2>/dev/null || true
|
|
110
110
|
rm -f "$UNIT_FILE"
|
|
111
|
+
# 浏览器一键升级单元
|
|
112
|
+
systemctl disable --now dsh-vps-upgrade.path 2>/dev/null || true
|
|
113
|
+
rm -f /etc/systemd/system/dsh-vps-upgrade.path /etc/systemd/system/dsh-vps-upgrade.service
|
|
111
114
|
systemctl daemon-reload 2>/dev/null || true
|
|
112
115
|
systemctl reset-failed "$SERVICE" 2>/dev/null || true
|
|
113
116
|
|
package/versions.json
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest_verified": "0.1.
|
|
3
|
-
"resolve_before": "2026-09-
|
|
2
|
+
"latest_verified": "0.1.7-rc.1",
|
|
3
|
+
"resolve_before": "2026-09-24T08:20:00Z",
|
|
4
4
|
"updated_at": "2026-09-24",
|
|
5
5
|
"versions": [
|
|
6
|
+
{
|
|
7
|
+
"dsh": "0.1.7-rc.1",
|
|
8
|
+
"min_gate": "1.5.0",
|
|
9
|
+
"verified_at": "2026-09-24",
|
|
10
|
+
"resolve_before": "2026-09-24T08:20:00Z",
|
|
11
|
+
"notes": "npm next 渠道。会话兑换 / 向导写 API Key / 升级自检 / ownsHost(网页端逻辑未变)/ 插件市场经网关安装 全部实测通过"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"dsh": "0.1.5-rc.3",
|
|
15
|
+
"min_gate": "1.4.4",
|
|
16
|
+
"verified_at": "2026-09-24",
|
|
17
|
+
"resolve_before": "2026-09-24T08:00:00Z",
|
|
18
|
+
"notes": "会话兑换 / 向导写 API Key / 升级自检 / ownsHost 补丁 / 插件市场经网关安装 全部实测通过"
|
|
19
|
+
},
|
|
6
20
|
{
|
|
7
21
|
"dsh": "0.1.5-rc.2",
|
|
8
22
|
"min_gate": "0.1.0",
|