dsh-vpn 0.1.4 → 0.1.6
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.md +11 -1
- package/lib/client.js +96 -5
- package/lib/index.js +193 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,16 @@ dsh plugin --profile web add dsh-vpn
|
|
|
38
38
|
3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
|
|
39
39
|
4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
|
|
40
40
|
|
|
41
|
+
页面第一行显示插件版本,有新版本时旁边会提示,点「更新」即可升级插件,重启 dsh 后生效(需要 dsh 0.2 及以上;更早的版本请在「设置 → 插件」中更新)。为了供应链安全,发布不足 24 小时的新版本会等满 24 小时再提供更新。
|
|
42
|
+
|
|
43
|
+
## 语言
|
|
44
|
+
|
|
45
|
+
整个插件的语言跟随 dsh 的界面语言(「设置 → 通用设置 → 语言」),目前支持 dsh 提供的中文和 English:
|
|
46
|
+
|
|
47
|
+
- 设置页切换语言后立即生效。
|
|
48
|
+
- agent 安装、卸载前的审批提示,以及 agent 收到的提示,也使用同一种语言。浏览器端会把 dsh 当前的界面语言告诉插件,插件把它保存下来,dsh 重启后仍然沿用。
|
|
49
|
+
- 只有从来没有打开过 dsh 网页界面时(例如纯命令行运行),才临时使用电脑的系统语言。
|
|
50
|
+
|
|
41
51
|
## 防火墙
|
|
42
52
|
|
|
43
53
|
安装时插件会检测 VPS 上启用的防火墙,自动放行所选协议的端口(使用 IP 证书时还有 TCP 80):
|
|
@@ -76,7 +86,7 @@ IP 证书由 sing-box 1.14 内置的 ACME 向 Let's Encrypt 申请([2026 年
|
|
|
76
86
|
| 接口鉴权 | 设置页的接口挂在 dsh 自己的 `/api` 下,复用 dsh 的登录 cookie 和防跨站、防 DNS rebinding 检查 |
|
|
77
87
|
| 主机指纹 | 首次连接时记录 SSH 主机指纹,之后指纹变化就拒绝连接,防止中间人截获密码 |
|
|
78
88
|
| sudo | 非 root 用户用 `sudo -S`:密码只经 stdin 传入,不会出现在命令行,也不会传给脚本 |
|
|
79
|
-
| agent 操作 | agent
|
|
89
|
+
| agent 操作 | agent 安装或卸载前会弹出审批,写明目标服务器、协议,以及是否重新生成密钥 |
|
|
80
90
|
| 证书 | 推荐用 IP 证书,所有协议都正常校验;自签模式下 Hysteria2 锁定指纹,TUIC 和 AnyTLS 会标注“需跳过证书校验” |
|
|
81
91
|
|
|
82
92
|
## 组成
|
package/lib/client.js
CHANGED
|
@@ -55,7 +55,13 @@ var vpnApi = {
|
|
|
55
55
|
status: () => post("/status"),
|
|
56
56
|
links: () => post("/links"),
|
|
57
57
|
install: (regenerate) => post("/install", { regenerate }),
|
|
58
|
-
uninstall: () => post("/uninstall")
|
|
58
|
+
uninstall: () => post("/uninstall"),
|
|
59
|
+
/** Installed version; with `check`, also the newest release on npm. */
|
|
60
|
+
version: (check = false) => post("/version", { check }),
|
|
61
|
+
/** Install the newest release through dsh's plugin manager. */
|
|
62
|
+
update: () => post("/update"),
|
|
63
|
+
/** Tell the host which language dsh's UI shows (approval prompts follow it). */
|
|
64
|
+
reportUiLocale: (locale) => post("/ui-locale", { locale })
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
// src/client/locales.ts
|
|
@@ -64,6 +70,17 @@ var zh = {
|
|
|
64
70
|
title: "VPN \u8282\u70B9",
|
|
65
71
|
nav: "VPN \u8BBE\u7F6E",
|
|
66
72
|
description: "\u5728\u4F60\u81EA\u5DF1\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72\u52A0\u5BC6 VPN\uFF0C\u4FDD\u62A4\u4F60\u5728\u516C\u5171 Wi-Fi \u7B49\u7F51\u7EDC\u4E2D\u7684\u8FDE\u63A5\u5B89\u5168\u548C\u9690\u79C1\uFF1B\u626B\u7801\u5373\u53EF\u5BFC\u5165\u624B\u673A\u6216\u7535\u8111\u4E0A\u7684\u5BA2\u6237\u7AEF\u3002",
|
|
73
|
+
version: "\u7248\u672C v{version}",
|
|
74
|
+
update: "\u66F4\u65B0",
|
|
75
|
+
updating: "\u6B63\u5728\u66F4\u65B0\u2026",
|
|
76
|
+
"update.latest": "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
|
|
77
|
+
"update.available": "\u6709\u65B0\u7248\u672C v{version}",
|
|
78
|
+
"update.tooNew": "\u65B0\u7248\u672C v{version} \u521A\u53D1\u5E03\uFF0C\u4E3A\u5B89\u5168\u8D77\u89C1 {time} \u540E\u53EF\u66F4\u65B0",
|
|
79
|
+
"update.done": "\u5DF2\u66F4\u65B0\u5230 v{version}\uFF0C\u91CD\u542F dsh \u540E\u751F\u6548",
|
|
80
|
+
"update.unsupported": "\u5F53\u524D dsh \u7248\u672C\u4E0D\u652F\u6301\u5728\u6B64\u66F4\u65B0\uFF0C\u8BF7\u5728\u300C\u8BBE\u7F6E \u2192 \u63D2\u4EF6\u300D\u4E2D\u66F4\u65B0 dsh-vpn\u3002",
|
|
81
|
+
"update.busy": "\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002",
|
|
82
|
+
"update.check_failed": "\u65E0\u6CD5\u68C0\u67E5\u65B0\u7248\u672C\uFF1A{message}",
|
|
83
|
+
"update.install_failed": "\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
|
|
67
84
|
serverSection: "\u670D\u52A1\u5668",
|
|
68
85
|
host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
|
|
69
86
|
hostHint: "\u4F60\u8D2D\u4E70\u7684 VPS \u7684\u516C\u7F51\u5730\u5740\u3002",
|
|
@@ -155,6 +172,17 @@ var en = {
|
|
|
155
172
|
title: "VPN node",
|
|
156
173
|
nav: "VPN",
|
|
157
174
|
description: "Deploy an encrypted VPN to your own VPS in one click to keep your connections private and secure on networks like public Wi-Fi; import it into client apps by scanning QR codes.",
|
|
175
|
+
version: "Version v{version}",
|
|
176
|
+
update: "Update",
|
|
177
|
+
updating: "Updating\u2026",
|
|
178
|
+
"update.latest": "You have the latest version",
|
|
179
|
+
"update.available": "v{version} is available",
|
|
180
|
+
"update.tooNew": "v{version} was just released; for safety it can be installed after {time}",
|
|
181
|
+
"update.done": "Updated to v{version}. Restart dsh to use it.",
|
|
182
|
+
"update.unsupported": "This dsh version cannot update plugins from here. Update dsh-vpn in Settings \u2192 Plugins.",
|
|
183
|
+
"update.busy": "An update is already running.",
|
|
184
|
+
"update.check_failed": "Could not check for updates: {message}",
|
|
185
|
+
"update.install_failed": "Update failed: {message}",
|
|
158
186
|
serverSection: "Server",
|
|
159
187
|
host: "Server IP or domain",
|
|
160
188
|
hostHint: "Public address of your VPS.",
|
|
@@ -280,7 +308,11 @@ var STYLES = `
|
|
|
280
308
|
.dshvpn-qr svg { width: 180px; height: 180px; }
|
|
281
309
|
.dshvpn-link { display: block; max-width: 100%; max-height: 3.2em; overflow: hidden; color: var(--dsw-alias-label-secondary, #666); }
|
|
282
310
|
.dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
|
|
283
|
-
.dshvpn-section-head h2 { margin: 0
|
|
311
|
+
.dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
312
|
+
.dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
|
|
313
|
+
.dshvpn-version { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #666); }
|
|
314
|
+
.dshvpn-update-note { font-size: 12px; color: var(--dsw-alias-label-secondary, #666); }
|
|
315
|
+
.dshvpn-update-note.dshvpn-new { color: var(--dsw-alias-state-success-primary, #2f855a); font-weight: 600; }
|
|
284
316
|
.dshvpn-section-head p { margin: 0; }
|
|
285
317
|
.dshvpn-qr-section { padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
|
|
286
318
|
.dshvpn-qr-section h3 { font-size: 15px; }
|
|
@@ -294,6 +326,10 @@ var STYLES = `
|
|
|
294
326
|
.dshvpn-qr-empty-icon { font-size: 36px; line-height: 1; color: var(--dsw-alias-label-tertiary, #888); }
|
|
295
327
|
`;
|
|
296
328
|
|
|
329
|
+
// src/client/VpnSettingsSection.tsx
|
|
330
|
+
var import_react2 = require("react");
|
|
331
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
332
|
+
|
|
297
333
|
// src/client/VpnSettingsPage.tsx
|
|
298
334
|
var import_react = require("react");
|
|
299
335
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
@@ -1375,13 +1411,57 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
|
|
|
1375
1411
|
// src/client/VpnSettingsSection.tsx
|
|
1376
1412
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1377
1413
|
function VpnSettingsSection(props) {
|
|
1378
|
-
const { t } = props;
|
|
1414
|
+
const { t, api } = props;
|
|
1415
|
+
const [info, setInfo] = (0, import_react2.useState)(null);
|
|
1416
|
+
const [note, setNote] = (0, import_react2.useState)(null);
|
|
1417
|
+
const [updating, setUpdating] = (0, import_react2.useState)(false);
|
|
1418
|
+
const when = (iso) => new Date(iso).toLocaleString();
|
|
1419
|
+
const describe = (v) => {
|
|
1420
|
+
if (v.target) return { text: t("update.available", { version: v.target }), fresh: true };
|
|
1421
|
+
if (v.availableAt && v.latest) return { text: t("update.tooNew", { version: v.latest, time: when(v.availableAt) }) };
|
|
1422
|
+
return null;
|
|
1423
|
+
};
|
|
1424
|
+
(0, import_react2.useEffect)(() => {
|
|
1425
|
+
let live = true;
|
|
1426
|
+
void api.version().then((r) => {
|
|
1427
|
+
if (live && r.ok) setInfo(r);
|
|
1428
|
+
});
|
|
1429
|
+
void api.version(true).then((r) => {
|
|
1430
|
+
if (!live || !r.ok) return;
|
|
1431
|
+
setInfo(r);
|
|
1432
|
+
setNote(describe(r));
|
|
1433
|
+
});
|
|
1434
|
+
return () => {
|
|
1435
|
+
live = false;
|
|
1436
|
+
};
|
|
1437
|
+
}, [api]);
|
|
1438
|
+
const update = async () => {
|
|
1439
|
+
setUpdating(true);
|
|
1440
|
+
setNote(null);
|
|
1441
|
+
const r = await api.update();
|
|
1442
|
+
setUpdating(false);
|
|
1443
|
+
if (!r.ok) {
|
|
1444
|
+
const known = ["unsupported", "busy", "check_failed"].includes(r.error) ? r.error : "install_failed";
|
|
1445
|
+
setNote({ text: t(`update.${known}`, { message: r.message ?? r.error }) });
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
setInfo((prev) => ({ ...prev, ...r.info, canUpdate: prev?.canUpdate ?? true }));
|
|
1449
|
+
if (r.status === "updated") setNote({ text: t("update.done", { version: r.version }), fresh: true });
|
|
1450
|
+
else setNote(describe(r.info) ?? { text: t("update.latest") });
|
|
1451
|
+
};
|
|
1379
1452
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-section", children: [
|
|
1380
1453
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dshvpn-section-head", children: [
|
|
1381
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
1454
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-title-row", children: [
|
|
1455
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("nav") }),
|
|
1456
|
+
info?.current && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dshvpn-version", children: t("version", { version: info.current }) }),
|
|
1457
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Button, { size: "sm", variant: "outline", disabled: updating, onClick: () => {
|
|
1458
|
+
void update();
|
|
1459
|
+
}, children: updating ? t("updating") : t("update") }),
|
|
1460
|
+
note && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `dshvpn-update-note${note.fresh ? " dshvpn-new" : ""}`, children: note.text })
|
|
1461
|
+
] }),
|
|
1382
1462
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dshvpn-muted", children: t("description") })
|
|
1383
1463
|
] }),
|
|
1384
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api
|
|
1464
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api, view: "page" })
|
|
1385
1465
|
] });
|
|
1386
1466
|
}
|
|
1387
1467
|
|
|
@@ -1400,6 +1480,17 @@ function apply(ctx) {
|
|
|
1400
1480
|
style.remove();
|
|
1401
1481
|
};
|
|
1402
1482
|
}, "dsh-vpn: styles");
|
|
1483
|
+
const reportLocale = (locale, attempt = 0) => {
|
|
1484
|
+
void vpnApi.reportUiLocale(locale).then((r) => {
|
|
1485
|
+
if (!r.ok && attempt < 3) setTimeout(() => {
|
|
1486
|
+
reportLocale(locale, attempt + 1);
|
|
1487
|
+
}, 2e3 * (attempt + 1));
|
|
1488
|
+
});
|
|
1489
|
+
};
|
|
1490
|
+
reportLocale(ctx.locale.getSnapshot().active);
|
|
1491
|
+
ctx.on("locale/change", (snapshot) => {
|
|
1492
|
+
reportLocale(snapshot.active);
|
|
1493
|
+
});
|
|
1403
1494
|
const t = ctx.locale.bind(NS);
|
|
1404
1495
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1405
1496
|
name: "settings.section",
|
package/lib/index.js
CHANGED
|
@@ -20264,8 +20264,24 @@ function json(status, value) {
|
|
|
20264
20264
|
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }
|
|
20265
20265
|
});
|
|
20266
20266
|
}
|
|
20267
|
-
function apiHandlers(service) {
|
|
20267
|
+
function apiHandlers(service, uiLang, onUiLang, updater) {
|
|
20268
20268
|
return {
|
|
20269
|
+
"/version": async (body, signal) => {
|
|
20270
|
+
const current = updater?.current;
|
|
20271
|
+
const canUpdate = updater?.manager !== void 0;
|
|
20272
|
+
if (!updater || body.check !== true) return { ok: true, current, canUpdate };
|
|
20273
|
+
try {
|
|
20274
|
+
return { ok: true, canUpdate, ...await updater.check(signal) };
|
|
20275
|
+
} catch (err) {
|
|
20276
|
+
return { ok: false, error: "check_failed", current, canUpdate, message: err instanceof Error ? err.message : String(err) };
|
|
20277
|
+
}
|
|
20278
|
+
},
|
|
20279
|
+
"/update": async (_body, signal) => updater ? updater.update(signal) : { ok: false, error: "unsupported" },
|
|
20280
|
+
"/ui-locale": async (body) => {
|
|
20281
|
+
if (typeof body.locale !== "string" || body.locale.length > 35) return { ok: false, error: "bad_request", message: "locale must be a string" };
|
|
20282
|
+
if (uiLang?.set(body.locale)) onUiLang?.();
|
|
20283
|
+
return { ok: true, lang: uiLang?.get() };
|
|
20284
|
+
},
|
|
20269
20285
|
"/settings/get": async () => ({ ok: true, settings: publicView(await service.store.get()) }),
|
|
20270
20286
|
"/settings/save": async (body) => {
|
|
20271
20287
|
let saved;
|
|
@@ -20305,8 +20321,8 @@ async function handle(handler, request) {
|
|
|
20305
20321
|
return json(500, { ok: false, error: "internal", message: err instanceof Error ? err.message : String(err) });
|
|
20306
20322
|
}
|
|
20307
20323
|
}
|
|
20308
|
-
function registerApi(ctx, service) {
|
|
20309
|
-
for (const [path, handler] of Object.entries(apiHandlers(service))) {
|
|
20324
|
+
function registerApi(ctx, service, uiLang, onUiLang, updater) {
|
|
20325
|
+
for (const [path, handler] of Object.entries(apiHandlers(service, uiLang, onUiLang, updater))) {
|
|
20310
20326
|
ctx.effect(() => ctx.connection.fetch.register({
|
|
20311
20327
|
path: `${API_BASE}${path}`,
|
|
20312
20328
|
methods: ["POST"],
|
|
@@ -20672,6 +20688,15 @@ var CredentialSettingsStore = class {
|
|
|
20672
20688
|
});
|
|
20673
20689
|
}
|
|
20674
20690
|
};
|
|
20691
|
+
var UI_LANG_KEY = credentialKey("dsh-vpn", "ui-locale");
|
|
20692
|
+
async function readUiLang(ctx) {
|
|
20693
|
+
const record = await ctx.credentials.readRecord(UI_LANG_KEY);
|
|
20694
|
+
const lang = record?.kind === "grant" ? record.payload?.lang : void 0;
|
|
20695
|
+
return lang === "zh" || lang === "en" ? lang : void 0;
|
|
20696
|
+
}
|
|
20697
|
+
async function writeUiLang(ctx, lang) {
|
|
20698
|
+
await ctx.credentials.modifyRecord(UI_LANG_KEY, async () => ({ kind: "grant", payload: { lang } }));
|
|
20699
|
+
}
|
|
20675
20700
|
|
|
20676
20701
|
// src/i18n.ts
|
|
20677
20702
|
import { execFileSync } from "node:child_process";
|
|
@@ -20696,6 +20721,26 @@ var cached;
|
|
|
20696
20721
|
function osLang() {
|
|
20697
20722
|
return cached ??= detectOsLang();
|
|
20698
20723
|
}
|
|
20724
|
+
var UiLang = class {
|
|
20725
|
+
current;
|
|
20726
|
+
constructor(initial) {
|
|
20727
|
+
this.current = initial;
|
|
20728
|
+
}
|
|
20729
|
+
/** @returns true when the value changed (worth persisting). */
|
|
20730
|
+
set(tag) {
|
|
20731
|
+
const next = fromTag(tag) ?? "en";
|
|
20732
|
+
if (next === this.current) return false;
|
|
20733
|
+
this.current = next;
|
|
20734
|
+
return true;
|
|
20735
|
+
}
|
|
20736
|
+
get() {
|
|
20737
|
+
return this.current ?? osLang();
|
|
20738
|
+
}
|
|
20739
|
+
/** Explicitly reported (not the OS fallback). */
|
|
20740
|
+
get reported() {
|
|
20741
|
+
return this.current;
|
|
20742
|
+
}
|
|
20743
|
+
};
|
|
20699
20744
|
var protocolNames = (ids, sep) => ids.map((id) => PROTOCOLS.find((p) => p.id === id)?.name ?? id).join(sep);
|
|
20700
20745
|
var TLS_PROTOCOLS = ["hy2", "tuic", "anytls"];
|
|
20701
20746
|
function installPrompt(lang, p) {
|
|
@@ -20709,6 +20754,126 @@ function notConfiguredMessage(lang) {
|
|
|
20709
20754
|
return lang === "zh" ? "\u5C1A\u672A\u914D\u7F6E\u670D\u52A1\u5668\u3002\u8BF7\u70B9 dsh \u5DE6\u4E0B\u89D2\u300C\u8BBE\u7F6E\u300D\u2192\u300CVPN \u8BBE\u7F6E\u300D\uFF0C\u586B\u5199\u670D\u52A1\u5668 IP\u3001\u7AEF\u53E3\u3001\u7528\u6237\u540D\u548C\u5BC6\u7801\u3002" : "No server configured. Open dsh Settings (bottom left) \u2192 VPN and fill in the server IP, port, username and password first.";
|
|
20710
20755
|
}
|
|
20711
20756
|
|
|
20757
|
+
// src/update.ts
|
|
20758
|
+
import { readFileSync } from "node:fs";
|
|
20759
|
+
var PACKAGE_NAME = "dsh-vpn";
|
|
20760
|
+
var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
20761
|
+
var NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20762
|
+
function currentVersion() {
|
|
20763
|
+
try {
|
|
20764
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
20765
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
20766
|
+
} catch {
|
|
20767
|
+
return "0.0.0";
|
|
20768
|
+
}
|
|
20769
|
+
}
|
|
20770
|
+
var RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
20771
|
+
function compareVersions(a, b) {
|
|
20772
|
+
const pa = RELEASE.exec(a);
|
|
20773
|
+
const pb = RELEASE.exec(b);
|
|
20774
|
+
if (!pa || !pb) return 0;
|
|
20775
|
+
for (let i = 1; i <= 3; i++) {
|
|
20776
|
+
const d = Number(pa[i]) - Number(pb[i]);
|
|
20777
|
+
if (d !== 0) return d;
|
|
20778
|
+
}
|
|
20779
|
+
return 0;
|
|
20780
|
+
}
|
|
20781
|
+
function planUpdate(current, doc, now = Date.now()) {
|
|
20782
|
+
const latest = doc["dist-tags"]?.latest;
|
|
20783
|
+
const info = { current };
|
|
20784
|
+
if (typeof latest !== "string" || !RELEASE.test(latest)) return info;
|
|
20785
|
+
info.latest = latest;
|
|
20786
|
+
if (compareVersions(latest, current) <= 0) return info;
|
|
20787
|
+
const published = (v) => Date.parse(doc.time?.[v] ?? "");
|
|
20788
|
+
const mature = (v) => {
|
|
20789
|
+
const t = published(v);
|
|
20790
|
+
return Number.isFinite(t) && now - t >= MIN_RELEASE_AGE_MS;
|
|
20791
|
+
};
|
|
20792
|
+
const candidates = Object.keys(doc.versions ?? { [latest]: {} }).filter((v) => RELEASE.test(v) && compareVersions(v, current) > 0 && compareVersions(v, latest) <= 0 && mature(v)).sort(compareVersions);
|
|
20793
|
+
const target = candidates.at(-1);
|
|
20794
|
+
if (target) info.target = target;
|
|
20795
|
+
if (target !== latest) {
|
|
20796
|
+
const t = published(latest);
|
|
20797
|
+
if (Number.isFinite(t)) info.availableAt = new Date(t + MIN_RELEASE_AGE_MS).toISOString();
|
|
20798
|
+
}
|
|
20799
|
+
return info;
|
|
20800
|
+
}
|
|
20801
|
+
async function registriesOf(manager) {
|
|
20802
|
+
const list = [];
|
|
20803
|
+
if (manager) {
|
|
20804
|
+
try {
|
|
20805
|
+
const r = await manager.registries();
|
|
20806
|
+
for (const url of [r.resolved, r.registry, ...r.fallbackRegistries]) if (url) list.push(url);
|
|
20807
|
+
} catch {
|
|
20808
|
+
}
|
|
20809
|
+
}
|
|
20810
|
+
list.push(NPM_REGISTRY);
|
|
20811
|
+
return [...new Set(list.map((u) => u.endsWith("/") ? u : `${u}/`))];
|
|
20812
|
+
}
|
|
20813
|
+
async function fetchPackument(registries, signal, fetchImpl = fetch) {
|
|
20814
|
+
let lastError;
|
|
20815
|
+
for (const registry of registries) {
|
|
20816
|
+
try {
|
|
20817
|
+
const res = await fetchImpl(`${registry}${PACKAGE_NAME}`, {
|
|
20818
|
+
headers: { accept: "application/json" },
|
|
20819
|
+
signal: AbortSignal.any([AbortSignal.timeout(15e3), ...signal ? [signal] : []])
|
|
20820
|
+
});
|
|
20821
|
+
if (!res.ok) throw new Error(`${registry}: HTTP ${res.status}`);
|
|
20822
|
+
return await res.json();
|
|
20823
|
+
} catch (err) {
|
|
20824
|
+
lastError = err;
|
|
20825
|
+
}
|
|
20826
|
+
}
|
|
20827
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
20828
|
+
}
|
|
20829
|
+
var Updater = class {
|
|
20830
|
+
constructor(current = currentVersion(), fetchImpl = fetch, now = Date.now) {
|
|
20831
|
+
this.current = current;
|
|
20832
|
+
this.fetchImpl = fetchImpl;
|
|
20833
|
+
this.now = now;
|
|
20834
|
+
}
|
|
20835
|
+
current;
|
|
20836
|
+
fetchImpl;
|
|
20837
|
+
now;
|
|
20838
|
+
manager;
|
|
20839
|
+
running = false;
|
|
20840
|
+
async check(signal) {
|
|
20841
|
+
const doc = await fetchPackument(await registriesOf(this.manager), signal, this.fetchImpl);
|
|
20842
|
+
return planUpdate(this.current, doc, this.now());
|
|
20843
|
+
}
|
|
20844
|
+
async update(signal) {
|
|
20845
|
+
const manager = this.manager;
|
|
20846
|
+
if (!manager) return { ok: false, error: "unsupported" };
|
|
20847
|
+
if (this.running) return { ok: false, error: "busy" };
|
|
20848
|
+
this.running = true;
|
|
20849
|
+
try {
|
|
20850
|
+
let info;
|
|
20851
|
+
try {
|
|
20852
|
+
info = await this.check(signal);
|
|
20853
|
+
} catch (err) {
|
|
20854
|
+
return { ok: false, error: "check_failed", message: messageOf(err) };
|
|
20855
|
+
}
|
|
20856
|
+
if (!info.target) return { ok: true, status: info.availableAt ? "too-new" : "up-to-date", info };
|
|
20857
|
+
const result = await manager.installBundle(`${PACKAGE_NAME}@${info.target}`);
|
|
20858
|
+
if (result.application === "applied" || result.application === "restart-required") {
|
|
20859
|
+
return { ok: true, status: "updated", info, version: info.target, restartRequired: result.application === "restart-required" };
|
|
20860
|
+
}
|
|
20861
|
+
const output = result.packageResult?.output?.trim();
|
|
20862
|
+
return { ok: false, error: "install_failed", info, message: tail(output || messageOf(result.error) || result.application) };
|
|
20863
|
+
} finally {
|
|
20864
|
+
this.running = false;
|
|
20865
|
+
}
|
|
20866
|
+
}
|
|
20867
|
+
};
|
|
20868
|
+
function messageOf(err) {
|
|
20869
|
+
if (err instanceof Error) return err.message;
|
|
20870
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
|
|
20871
|
+
return err === void 0 ? "" : String(err);
|
|
20872
|
+
}
|
|
20873
|
+
function tail(text, max = 600) {
|
|
20874
|
+
return text.length > max ? `\u2026${text.slice(-max)}` : text;
|
|
20875
|
+
}
|
|
20876
|
+
|
|
20712
20877
|
// src/index.ts
|
|
20713
20878
|
var name = "dsh-vpn";
|
|
20714
20879
|
var inject = ["tools", "credentials"];
|
|
@@ -20738,8 +20903,8 @@ var OUTPUT = {
|
|
|
20738
20903
|
].join("\n")
|
|
20739
20904
|
}]
|
|
20740
20905
|
};
|
|
20741
|
-
function toToolValue(r) {
|
|
20742
|
-
const message = r.error === "not_configured" ? notConfiguredMessage(
|
|
20906
|
+
function toToolValue(r, uiLang) {
|
|
20907
|
+
const message = r.error === "not_configured" ? notConfiguredMessage(uiLang.get()) : r.message;
|
|
20743
20908
|
return {
|
|
20744
20909
|
ok: r.ok,
|
|
20745
20910
|
...r.error ? { error: r.error } : {},
|
|
@@ -20749,6 +20914,16 @@ function toToolValue(r) {
|
|
|
20749
20914
|
}
|
|
20750
20915
|
function apply(ctx, config) {
|
|
20751
20916
|
const service = new VpnService(new CredentialSettingsStore(ctx), { timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider });
|
|
20917
|
+
const uiLang = new UiLang();
|
|
20918
|
+
readUiLang(ctx).then((lang) => {
|
|
20919
|
+
if (lang && !uiLang.reported) uiLang.set(lang);
|
|
20920
|
+
}, () => {
|
|
20921
|
+
});
|
|
20922
|
+
const persistUiLang = () => {
|
|
20923
|
+
const lang = uiLang.reported;
|
|
20924
|
+
if (lang) writeUiLang(ctx, lang).catch(() => {
|
|
20925
|
+
});
|
|
20926
|
+
};
|
|
20752
20927
|
ctx.tools.register(defineTool({
|
|
20753
20928
|
name: "vpn_install",
|
|
20754
20929
|
description: `Install or reconfigure sing-box on the VPS configured in dsh Settings \u2192 VPN, then return share links. Opens the ports in the VPS firewall automatically (result.firewall). Idempotent: rerunning keeps existing keys and ports unless \`regenerate\` is set. Protocols: ${PROTOCOL_IDS.join(", ")}; omit to use the ones selected in settings.`,
|
|
@@ -20762,7 +20937,7 @@ function apply(ctx, config) {
|
|
|
20762
20937
|
},
|
|
20763
20938
|
output: OUTPUT,
|
|
20764
20939
|
async execute(args, exec) {
|
|
20765
|
-
return toToolValue(await service.install({ protocols: args.protocols, regenerate: args.regenerate }, exec.signal));
|
|
20940
|
+
return toToolValue(await service.install({ protocols: args.protocols, regenerate: args.regenerate }, exec.signal), uiLang);
|
|
20766
20941
|
}
|
|
20767
20942
|
}));
|
|
20768
20943
|
ctx.tools.register(defineTool({
|
|
@@ -20770,21 +20945,21 @@ function apply(ctx, config) {
|
|
|
20770
20945
|
description: "Report whether sing-box is installed, running and listening on the configured VPS.",
|
|
20771
20946
|
parameters: {},
|
|
20772
20947
|
output: OUTPUT,
|
|
20773
|
-
execute: async (_args, exec) => toToolValue(await service.status(exec.signal))
|
|
20948
|
+
execute: async (_args, exec) => toToolValue(await service.status(exec.signal), uiLang)
|
|
20774
20949
|
}));
|
|
20775
20950
|
ctx.tools.register(defineTool({
|
|
20776
20951
|
name: "vpn_links",
|
|
20777
20952
|
description: "Return the share links (vless://, hysteria2://, \u2026) of the installed server. Tell the user they can scan QR codes in dsh Settings \u2192 VPN (\u5237\u65B0\u4E8C\u7EF4\u7801).",
|
|
20778
20953
|
parameters: {},
|
|
20779
20954
|
output: OUTPUT,
|
|
20780
|
-
execute: async (_args, exec) => toToolValue(await service.links(exec.signal))
|
|
20955
|
+
execute: async (_args, exec) => toToolValue(await service.links(exec.signal), uiLang)
|
|
20781
20956
|
}));
|
|
20782
20957
|
ctx.tools.register(defineTool({
|
|
20783
20958
|
name: "vpn_uninstall",
|
|
20784
20959
|
description: "Stop and remove sing-box, its config and credentials from the configured VPS.",
|
|
20785
20960
|
parameters: {},
|
|
20786
20961
|
output: OUTPUT,
|
|
20787
|
-
execute: async (_args, exec) => toToolValue(await service.uninstall(exec.signal))
|
|
20962
|
+
execute: async (_args, exec) => toToolValue(await service.uninstall(exec.signal), uiLang)
|
|
20788
20963
|
}));
|
|
20789
20964
|
if (config.confirm) {
|
|
20790
20965
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
@@ -20792,7 +20967,7 @@ function apply(ctx, config) {
|
|
|
20792
20967
|
if (decision.kind !== "allow" || exec.name !== "vpn_install" && exec.name !== "vpn_uninstall") return decision;
|
|
20793
20968
|
const s = await service.store.get();
|
|
20794
20969
|
if (!s) return decision;
|
|
20795
|
-
const lang =
|
|
20970
|
+
const lang = uiLang.get();
|
|
20796
20971
|
const target = `${s.host}:${s.port}`;
|
|
20797
20972
|
if (exec.name === "vpn_uninstall") return { kind: "ask", reason: uninstallPrompt(lang, target) };
|
|
20798
20973
|
const args = exec.arguments;
|
|
@@ -20808,8 +20983,15 @@ function apply(ctx, config) {
|
|
|
20808
20983
|
};
|
|
20809
20984
|
});
|
|
20810
20985
|
}
|
|
20986
|
+
const updater = new Updater();
|
|
20987
|
+
ctx.inject(["pluginManager"], (pmCtx) => {
|
|
20988
|
+
updater.manager = pmCtx.pluginManager;
|
|
20989
|
+
pmCtx.effect(() => () => {
|
|
20990
|
+
updater.manager = void 0;
|
|
20991
|
+
}, "dsh-vpn: plugin manager");
|
|
20992
|
+
});
|
|
20811
20993
|
ctx.inject(["connection"], (webCtx) => {
|
|
20812
|
-
registerApi(webCtx, service);
|
|
20994
|
+
registerApi(webCtx, service, uiLang, persistUiLang, updater);
|
|
20813
20995
|
});
|
|
20814
20996
|
}
|
|
20815
20997
|
export {
|
package/package.json
CHANGED