dsh-vpn 0.1.0 → 0.1.2

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 CHANGED
@@ -9,15 +9,22 @@
9
9
 
10
10
  ## 使用
11
11
 
12
+ 用你启动 dsh 的同一种方式安装。profile 名就是启动 dsh 时 `--profile` 后面的名字;用 `dsh web` 启动的话就是 `web`:
13
+
12
14
  ```bash
15
+ # 用 npx 运行 dsh 的
16
+ npx @deepseek-ai/dsh plugin --profile web add dsh-vpn
17
+ # 全局安装了 dsh 的
13
18
  dsh plugin --profile web add dsh-vpn
14
19
  ```
15
20
 
16
- 打开 dsh →「插件」→ dsh-vpn:
21
+ 装好后重启 dsh,点左下角「设置」,左侧菜单里的「**VPN 设置**」就是本插件的设置页(dsh 0.1.5 和 0.1.7 相同)。
22
+
23
+ 在 VPN 设置里:
17
24
 
18
25
  1. 填写服务器 IP、SSH 端口、用户名、密码,点「保存」和「测试连接」。
19
26
  2. 勾选要安装的协议,选择证书类型(推荐「IP 证书」),保存后点「安装 / 更新」。
20
- 3. 在「节点二维码」里点「刷新」,用手机或电脑上的客户端扫码导入。
27
+ 3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
21
28
  4. 在云服务商控制台的安全组里放行页面提示的端口。用 IP 证书时,TCP 80 端口要一直开放,用于续期。
22
29
 
23
30
  ## 证书
@@ -54,7 +61,7 @@ IP 证书由 sing-box 1.14 内置的 ACME 向 Let's Encrypt 申请([2026 年
54
61
  | `src/settings.ts` / `src/credential-store.ts` | 服务器设置的校验和存储 |
55
62
  | `src/api.ts` | 设置页调用的 `/api/dsh-vpn/*` 接口 |
56
63
  | `src/index.ts` | agent 工具 `vpn_install` / `vpn_status` / `vpn_links` / `vpn_uninstall`,以及执行前的审批 |
57
- | `src/client/` | 浏览器端:设置页、二维码 |
64
+ | `src/client/` | 浏览器端:设置弹窗里的「VPN 设置」一栏、二维码 |
58
65
  | `src/skill.ts` + `assets/vpn-ops.md` | `vpn-ops` 技能:操作流程和排障 |
59
66
 
60
67
  ## 远端落盘位置
@@ -79,7 +86,7 @@ npm 上的 `@deepseek-ai/*` 包版本过旧,所以 `build` 不做类型检查
79
86
 
80
87
  ## 发布
81
88
 
82
- 推送与 `package.json` 版本号一致的标签,GitHub Actions 会先跑测试,再发布到 npm,并附带来源证明(provenance)。需要在仓库 Secrets 里配置 `NPM_TOKEN`:
89
+ 推送与 `package.json` 版本号一致的标签,GitHub Actions 会先跑测试,再发布到 npm。需要在仓库 Secrets 里配置 `NPM_TOKEN`。仓库是私有的,npm 不支持为私有仓库生成来源证明(provenance),所以发布时不带来源证明:
83
90
 
84
91
  ```bash
85
92
  git tag v0.1.0 && git push origin v0.1.0
package/assets/vpn-ops.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # vpn-ops
2
2
 
3
- Deploy and operate a personal sing-box server on the user's VPS. The plugin logs in over SSH with the password saved on its settings page (dsh → Plugins → dsh-vpn). No local ssh client is needed.
3
+ Deploy and operate a personal sing-box server on the user's VPS. The plugin logs in over SSH with the password saved in dsh Settings (bottom-left gear) → VPN 设置. No local ssh client is needed.
4
4
 
5
5
  ## Workflow
6
6
 
7
7
  1. **Check first.** Call `vpn_status`.
8
- - `error: not_configured` → tell the user to open dsh → 插件 (Plugins) → dsh-vpn, fill in server IP, SSH port, username and password, then click 保存 and 测试连接. Never ask for the password in chat.
8
+ - `error: not_configured` → tell the user to open dsh 设置 (Settings, bottom left) → VPN 设置, fill in server IP, SSH port, username and password, then click 保存 and 测试连接. Never ask for the password in chat.
9
9
  - Installed and active → offer `vpn_links` instead of reinstalling.
10
10
  2. **Install.** Call `vpn_install`. Omit `protocols` to use the selection saved in settings. Only pass `regenerate: true` when the user explicitly wants new keys, because every old link stops working.
11
- 3. **Deliver.** Say the links are ready and that the settings page shows scannable QR codes (刷新 button). Client apps: v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta / mihomo.
11
+ 3. **Deliver.** Say the links are ready and that 设置 → VPN 设置 shows scannable QR codes (刷新二维码 button). Client apps: v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta / mihomo.
12
12
  4. **Remind** the user to open the reported ports in the cloud provider's security group, using the right transport for each protocol.
13
13
 
14
14
  ## Certificates
package/lib/client.js CHANGED
@@ -62,6 +62,7 @@ var vpnApi = {
62
62
  var NS = "dsh-vpn";
63
63
  var zh = {
64
64
  title: "VPN \u8282\u70B9",
65
+ nav: "VPN \u8BBE\u7F6E",
65
66
  description: "\u5728\u4F60\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72 sing-box\uFF0C\u5E76\u7528\u4E8C\u7EF4\u7801\u5BFC\u5165\u624B\u673A\u6216\u7535\u8111\u4E0A\u7684\u4EE3\u7406\u5BA2\u6237\u7AEF\u3002",
66
67
  serverSection: "\u670D\u52A1\u5668",
67
68
  host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
@@ -114,8 +115,12 @@ var zh = {
114
115
  working: "\u6267\u884C\u4E2D\u2026",
115
116
  qrSection: "\u8282\u70B9\u4E8C\u7EF4\u7801",
116
117
  qrHint: "\u7528 v2rayN\u3001NekoBox\u3001Shadowrocket\u3001sing-box\u3001Clash Meta \u7B49\u5BA2\u6237\u7AEF\u626B\u63CF\u5BFC\u5165\u3002\u4E8C\u7EF4\u7801\u7B49\u540C\u4E8E\u5BC6\u7801\uFF0C\u8BF7\u52FF\u5916\u4F20\u3002",
117
- refresh: "\u5237\u65B0",
118
- qrEmpty: "\u70B9\u300C\u5237\u65B0\u300D\u4ECE\u670D\u52A1\u5668\u8BFB\u53D6\u8282\u70B9\u5E76\u751F\u6210\u4E8C\u7EF4\u7801\u3002",
118
+ refresh: "\u5237\u65B0\u4E8C\u7EF4\u7801",
119
+ refreshing: "\u6B63\u5728\u5237\u65B0\u2026",
120
+ refreshedAt: "\u4E0A\u6B21\u5237\u65B0 {time}",
121
+ generate: "\u751F\u6210\u8282\u70B9\u4E8C\u7EF4\u7801",
122
+ qrEmpty: "\u8FD8\u6CA1\u6709\u4E8C\u7EF4\u7801\u3002\u5B89\u88C5\u5B8C\u6210\u540E\uFF0C\u70B9\u4E0B\u9762\u7684\u6309\u94AE\u4ECE\u670D\u52A1\u5668\u8BFB\u53D6\u8282\u70B9\u5E76\u751F\u6210\u4E8C\u7EF4\u7801\u3002",
123
+ qrNeedsSave: "\u8BF7\u5148\u586B\u5199\u5E76\u4FDD\u5B58\u670D\u52A1\u5668\u4FE1\u606F\u3002",
119
124
  copy: "\u590D\u5236\u94FE\u63A5",
120
125
  copied: "\u5DF2\u590D\u5236",
121
126
  portLabel: "\u7AEF\u53E3 {port}",
@@ -145,6 +150,7 @@ var zh = {
145
150
  };
146
151
  var en = {
147
152
  title: "VPN node",
153
+ nav: "VPN",
148
154
  description: "Deploy sing-box to your VPS in one click and import it into proxy apps by scanning QR codes.",
149
155
  serverSection: "Server",
150
156
  host: "Server IP or domain",
@@ -197,8 +203,12 @@ var en = {
197
203
  working: "Working\u2026",
198
204
  qrSection: "Node QR codes",
199
205
  qrHint: "Scan with v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta and similar apps. A QR code is as secret as a password.",
200
- refresh: "Refresh",
201
- qrEmpty: 'Click "Refresh" to read the nodes from the server and show QR codes.',
206
+ refresh: "Refresh QR codes",
207
+ refreshing: "Refreshing\u2026",
208
+ refreshedAt: "Last refreshed {time}",
209
+ generate: "Generate node QR codes",
210
+ qrEmpty: "No QR codes yet. After installing, click below to read the nodes from the server and generate QR codes.",
211
+ qrNeedsSave: "Fill in and save the server details first.",
202
212
  copy: "Copy link",
203
213
  copied: "Copied",
204
214
  portLabel: "Port {port}",
@@ -243,6 +253,8 @@ var STYLES = `
243
253
  .dshvpn-hostkey small { flex-basis: 100%; }
244
254
  .dshvpn-protocols { display: flex; flex-direction: column; gap: 6px; }
245
255
  .dshvpn-protocol { display: flex; align-items: center; gap: 8px; }
256
+ .dshvpn-checkbox { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
257
+ .dshvpn-checkbox input { margin: 0; }
246
258
  .dshvpn-radio { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; cursor: pointer; }
247
259
  .dshvpn-radio input { margin-top: 3px; }
248
260
  .dshvpn-radio span { display: flex; flex-direction: column; gap: 2px; }
@@ -261,6 +273,19 @@ var STYLES = `
261
273
  .dshvpn-qr { background: #fff; padding: 6px; border-radius: 6px; line-height: 0; align-self: center; }
262
274
  .dshvpn-qr svg { width: 180px; height: 180px; }
263
275
  .dshvpn-link { display: block; max-width: 100%; max-height: 3.2em; overflow: hidden; color: var(--dsw-alias-label-secondary, #666); }
276
+ .dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
277
+ .dshvpn-section-head h2 { margin: 0 0 4px; font-size: 18px; font-weight: 600; }
278
+ .dshvpn-section-head p { margin: 0; }
279
+ .dshvpn-qr-section { padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
280
+ .dshvpn-qr-section h3 { font-size: 15px; }
281
+ .dshvpn-refresh { gap: 6px; font-weight: 600; }
282
+ .dshvpn-refresh-lg { padding: 10px 22px !important; font-size: 14px !important; }
283
+ .dshvpn-spin { display: inline-block; animation: dshvpn-spin 1s linear infinite; }
284
+ @keyframes dshvpn-spin { to { transform: rotate(360deg); } }
285
+ .dshvpn-qr-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 28px 16px; text-align: center;
286
+ border: 2px dashed var(--dsw-alias-border-l3, rgba(127,127,127,.35)); border-radius: 12px; }
287
+ .dshvpn-qr-empty p { margin: 0; font-size: 13px; color: var(--dsw-alias-label-secondary, #666); max-width: 420px; }
288
+ .dshvpn-qr-empty-icon { font-size: 36px; line-height: 1; color: var(--dsw-alias-label-tertiary, #888); }
264
289
  `;
265
290
 
266
291
  // src/client/VpnSettingsPage.tsx
@@ -994,6 +1019,14 @@ function transportsFor(id) {
994
1019
  const t = PROTOCOLS.find((p) => p.id === id)?.transport ?? "TCP";
995
1020
  return t === "TCP+UDP" ? ["tcp", "udp"] : [t.toLowerCase()];
996
1021
  }
1022
+ function Checkbox({ label, checked, disabled, onChange }) {
1023
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-checkbox", children: [
1024
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked, disabled, onChange: (e) => {
1025
+ onChange(e.target.checked);
1026
+ } }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label })
1028
+ ] });
1029
+ }
997
1030
  function QrCard({ item, t }) {
998
1031
  const [copied, setCopied] = (0, import_react.useState)(false);
999
1032
  const info = PROTOCOLS.find((p) => p.id === item.id);
@@ -1026,6 +1059,7 @@ function VpnSettingsPage({ view, t, api }) {
1026
1059
  const [busy, setBusy] = (0, import_react.useState)(null);
1027
1060
  const [notice, setNotice] = (0, import_react.useState)(null);
1028
1061
  const [links, setLinks] = (0, import_react.useState)(null);
1062
+ const [refreshedAt, setRefreshedAt] = (0, import_react.useState)(null);
1029
1063
  const [regenerate, setRegenerate] = (0, import_react.useState)(false);
1030
1064
  const [confirming, setConfirming] = (0, import_react.useState)(null);
1031
1065
  (0, import_react.useEffect)(() => {
@@ -1081,13 +1115,21 @@ function VpnSettingsPage({ view, t, api }) {
1081
1115
  });
1082
1116
  const install = async () => {
1083
1117
  const r = await op("install", () => api.install(regenerate), (v) => {
1084
- if (v.links) setLinks(v.links);
1118
+ if (v.links) {
1119
+ setLinks(v.links);
1120
+ setRefreshedAt(/* @__PURE__ */ new Date());
1121
+ }
1085
1122
  return { tone: "success", text: `${t("installed")} ${v.links ? portsReminder(v.links) : ""}` };
1086
1123
  });
1087
1124
  if (!r.ok && r.links) setLinks(r.links);
1088
1125
  setRegenerate(false);
1089
1126
  void refreshSettings();
1090
1127
  };
1128
+ const refreshLinks = () => op("links", api.links, (r) => {
1129
+ if (r.links) setLinks(r.links);
1130
+ setRefreshedAt(/* @__PURE__ */ new Date());
1131
+ return null;
1132
+ });
1091
1133
  const disabled = busy !== null;
1092
1134
  const needsSave = dirty || !saved.configured;
1093
1135
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn", children: [
@@ -1163,7 +1205,7 @@ function VpnSettingsPage({ view, t, api }) {
1163
1205
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("protocolHint") }),
1164
1206
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-protocols", children: PROTOCOLS.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-protocol", children: [
1165
1207
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1166
- import_dsh_client_ui_primitives.Checkbox,
1208
+ Checkbox,
1167
1209
  {
1168
1210
  label: p.name,
1169
1211
  checked: form.protocols.includes(p.id),
@@ -1221,7 +1263,7 @@ function VpnSettingsPage({ view, t, api }) {
1221
1263
  ] }),
1222
1264
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1223
1265
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("deploySection") }),
1224
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Checkbox, { label: t("regenerate"), checked: regenerate, disabled, onChange: setRegenerate }),
1266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox, { label: t("regenerate"), checked: regenerate, disabled, onChange: setRegenerate }),
1225
1267
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-actions", children: [
1226
1268
  confirming === "regenerate" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1227
1269
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", onClick: () => {
@@ -1294,34 +1336,51 @@ ${t("certExpires", { date: new Date(cert.expires ?? "").toLocaleString(), hours
1294
1336
  ] })
1295
1337
  ] }),
1296
1338
  (busy !== null || notice) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshvpn-notice dshvpn-${busy !== null ? "info" : notice?.tone}`, role: "status", children: busy !== null ? t("working") : notice?.text }),
1297
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshvpn-qr-section", children: [
1298
1340
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-row", children: [
1299
1341
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("qrSection") }),
1300
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1301
- import_dsh_client_ui_primitives.Button,
1302
- {
1303
- variant: "outline",
1304
- size: "sm",
1305
- disabled: disabled || needsSave,
1306
- onClick: () => {
1307
- void op("links", api.links, (r) => {
1308
- if (r.links) setLinks(r.links);
1309
- return null;
1310
- });
1311
- },
1312
- children: t("refresh")
1313
- }
1314
- )
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshvpn-actions", children: [
1343
+ refreshedAt && busy !== "links" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("refreshedAt", { time: refreshedAt.toLocaleTimeString() }) }),
1344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dsh_client_ui_primitives.Button, { variant: "primary", className: "dshvpn-refresh", disabled: disabled || needsSave, onClick: () => {
1345
+ void refreshLinks();
1346
+ }, children: [
1347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: busy === "links" ? "dshvpn-spin" : void 0, "aria-hidden": "true", children: "\u21BB" }),
1348
+ busy === "links" ? t("refreshing") : t("refresh")
1349
+ ] })
1350
+ ] })
1315
1351
  ] }),
1316
1352
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("qrHint") }),
1317
- links ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-qr-grid", children: links.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QrCard, { item, t }, item.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: t("qrEmpty") })
1353
+ links ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-qr-grid", children: links.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QrCard, { item, t }, item.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-qr-empty", children: [
1354
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshvpn-qr-empty-icon", "aria-hidden": "true", children: "\u25A6" }),
1355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: needsSave ? t("qrNeedsSave") : t("qrEmpty") }),
1356
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dsh_client_ui_primitives.Button, { variant: "primary", className: "dshvpn-refresh dshvpn-refresh-lg", disabled: disabled || needsSave, onClick: () => {
1357
+ void refreshLinks();
1358
+ }, children: [
1359
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: busy === "links" ? "dshvpn-spin" : void 0, "aria-hidden": "true", children: "\u21BB" }),
1360
+ busy === "links" ? t("refreshing") : t("generate")
1361
+ ] })
1362
+ ] })
1318
1363
  ] })
1319
1364
  ] });
1320
1365
  }
1321
1366
 
1367
+ // src/client/VpnSettingsSection.tsx
1368
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1369
+ function VpnSettingsSection(props) {
1370
+ const { t } = props;
1371
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-section", children: [
1372
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dshvpn-section-head", children: [
1373
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("nav") }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dshvpn-muted", children: t("description") })
1375
+ ] }),
1376
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api: props.api, view: "page" })
1377
+ ] });
1378
+ }
1379
+
1322
1380
  // src/client/index.ts
1323
1381
  var PACKAGE_NAME = "dsh-vpn";
1324
1382
  var inject = ["slots", "locale"];
1383
+ var SECTION_ORDER = 25;
1325
1384
  function apply(ctx) {
1326
1385
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-vpn: dictionaries");
1327
1386
  ctx.effect(() => {
@@ -1333,12 +1392,15 @@ function apply(ctx) {
1333
1392
  style.remove();
1334
1393
  };
1335
1394
  }, "dsh-vpn: styles");
1336
- ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
1337
- name: "plugins.bundle.config",
1338
- key: PACKAGE_NAME,
1395
+ const t = ctx.locale.bind(NS);
1396
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1397
+ name: "settings.section",
1398
+ id: PACKAGE_NAME,
1399
+ order: SECTION_ORDER,
1400
+ label: () => t("nav"),
1339
1401
  locale: NS,
1340
1402
  inject: () => ({ api: vpnApi })
1341
- }, VpnSettingsPage));
1403
+ }, VpnSettingsSection));
1342
1404
  }
1343
1405
 
1344
1406
  return module.exports;
package/lib/index.js CHANGED
@@ -20706,7 +20706,7 @@ function uninstallPrompt(lang, target) {
20706
20706
  return lang === "zh" ? `\u5C06\u505C\u6B62\u5E76\u5220\u9664 ${target} \u4E0A\u7684 sing-box \u670D\u52A1\u3001\u914D\u7F6E\u548C\u5BC6\u94A5\u3002\u73B0\u6709\u8282\u70B9\u94FE\u63A5\u5C06\u5168\u90E8\u5931\u6548\u3002` : `Stop and remove the sing-box service, its config and keys from ${target}. All existing node links stop working.`;
20707
20707
  }
20708
20708
  function notConfiguredMessage(lang) {
20709
- return lang === "zh" ? "\u5C1A\u672A\u914D\u7F6E\u670D\u52A1\u5668\u3002\u8BF7\u5148\u5728 dsh\u300C\u63D2\u4EF6\u300D\u9875\u9762\u6253\u5F00 dsh-vpn\uFF0C\u586B\u5199\u670D\u52A1\u5668 IP\u3001\u7AEF\u53E3\u3001\u7528\u6237\u540D\u548C\u5BC6\u7801\u3002" : "No server configured. Open dsh-vpn on the dsh Plugins page and fill in the server IP, port, username and password first.";
20709
+ 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
20710
  }
20711
20711
 
20712
20712
  // src/index.ts
@@ -20751,7 +20751,7 @@ function apply(ctx, config) {
20751
20751
  const service = new VpnService(new CredentialSettingsStore(ctx), { timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider });
20752
20752
  ctx.tools.register(defineTool({
20753
20753
  name: "vpn_install",
20754
- description: `Install or reconfigure sing-box on the VPS configured in the dsh-vpn settings page, then return share links. Idempotent: rerunning keeps existing keys and ports unless \`regenerate\` is set. Protocols: ${PROTOCOL_IDS.join(", ")}; omit to use the ones selected in settings.`,
20754
+ description: `Install or reconfigure sing-box on the VPS configured in dsh Settings \u2192 VPN, then return share links. Idempotent: rerunning keeps existing keys and ports unless \`regenerate\` is set. Protocols: ${PROTOCOL_IDS.join(", ")}; omit to use the ones selected in settings.`,
20755
20755
  parameters: {
20756
20756
  protocols: {
20757
20757
  type: "array",
@@ -20774,7 +20774,7 @@ function apply(ctx, config) {
20774
20774
  }));
20775
20775
  ctx.tools.register(defineTool({
20776
20776
  name: "vpn_links",
20777
- description: "Return the share links (vless://, hysteria2://, \u2026) of the installed server. Tell the user they can scan QR codes on the dsh-vpn settings page.",
20777
+ 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
20778
  parameters: {},
20779
20779
  output: OUTPUT,
20780
20780
  execute: async (_args, exec) => toToolValue(await service.links(exec.signal))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vpn",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "DeepSeek Harness plugin: one-click sing-box deployment to a VPS over SSH, with a settings page and QR codes",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -53,8 +53,7 @@
53
53
  },
54
54
  "packageManager": "pnpm@11.9.0",
55
55
  "publishConfig": {
56
- "access": "public",
57
- "provenance": true
56
+ "access": "public"
58
57
  },
59
58
  "scripts": {
60
59
  "build": "node scripts/build.mjs",