@linxin666/dsh-ssh 0.4.3 → 0.4.5

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: e477f468d33c808362bab0fd246dc6eb2bfefb62
6
- README.zh.md: d37b585d6e6a18f1a9bb22e8d9e223fb39b91076
5
+ README.md: bd5c0cb0afa9dc65e65b1fdfb6ba5205b66019df
6
+ README.zh.md: 84f564e99c5c7a42ae2a48fce5772b940d18ff3e
package/README.md CHANGED
@@ -1,8 +1,21 @@
1
- # dsh-ssh — Remote SSH operations plugin for DSH
1
+ # dsh-ssh · Web SSH Terminal, SFTP Transfer & Remote Operations for DeepSeek Harness (DSH)
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Built on the capability list of [badseal/ssh-skill](https://github.com/badseal/ssh-skill), a remote SSH plugin tailored for DeepSeek Harness (DSH): a persistent connection pool inside the Host process + a Web GUI host-management panel + a Web terminal + Agent tools, all implemented through the official NPM SDK without modifying DSH source.
5
+ <p align="center">
6
+ <img src="https://img.shields.io/npm/v/@linxin666/dsh-ssh?style=flat-square" alt="Version">
7
+ &nbsp;
8
+ <img src="https://img.shields.io/badge/DSH-%3E%3D0.2.0--rc.2-4c6ef5?style=flat-square&amp;labelColor=454a54" alt="DSH">
9
+ &nbsp;
10
+ <img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="License">
11
+ </p>
12
+
13
+ <p align="center">
14
+ <strong>Remote Linux Server Operations & Web SSH Suite for DeepSeek Harness (DSH)</strong><br>
15
+ <em>xterm.js Web Terminal · Visual SFTP Transfer · Port Forwarding Tunnels · Cluster Execution · 6 Agent Tools</em>
16
+ </p>
17
+
18
+ A dedicated remote Linux server operations and cluster management plugin for DeepSeek Harness (DSH) Web GUI and official desktop client: in-process persistent connection pool, Web GUI host inventory management, xterm.js terminal, visual SFTP file transfer, port forwarding tunnels, and 6 dedicated agent tools implemented on top of official NPM SDKs without modifying DSH core source code.
6
19
 
7
20
  ## Capabilities
8
21
 
package/README.zh.md CHANGED
@@ -1,8 +1,21 @@
1
- # dsh-ssh — 远程 SSH 运维插件(DSH 版 ssh-skill)
1
+ # dsh-ssh · DeepSeek Harness (DSH) 网页版 SSH 终端、SFTP 文件传输与集群运维插件
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- 基于 [badseal/ssh-skill](https://github.com/badseal/ssh-skill) 的能力清单,为 DeepSeek Harness(DSH)定制的远程 SSH 插件:Host 进程内的持久连接池 + Web GUI 主机管理面板 + Web 终端 + Agent 工具,全部通过官方 NPM SDK 实现,不修改 DSH 源码。
5
+ <p align="center">
6
+ <img src="https://img.shields.io/npm/v/@linxin666/dsh-ssh?style=flat-square" alt="Version">
7
+ &nbsp;
8
+ <img src="https://img.shields.io/badge/DSH-%3E%3D0.2.0--rc.2-4c6ef5?style=flat-square&amp;labelColor=454a54" alt="DSH">
9
+ &nbsp;
10
+ <img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="License">
11
+ </p>
12
+
13
+ <p align="center">
14
+ <strong>DeepSeek Harness(DSH)官方 Web GUI 与桌面客户端专属远程 Linux 服务器运维套件</strong><br>
15
+ <em>xterm.js Web 终端 · 可视化 SFTP 传输 · 本地端口转发隧道 · 集群并发命令 · 6 大 Agent 工具</em>
16
+ </p>
17
+
18
+ 专为 DeepSeek Harness(DSH)Web GUI 与官方桌面客户端定制的远程 Linux 服务器运维与集群管理插件:Host 进程内的持久连接池 + Web GUI 主机管理面板 + 基于 xterm.js 的 Web 终端 + 可视化 SFTP 传输 + 6 大专用 Agent 工具,全部通过官方 NPM SDK 实现,零修改侵入 DSH 官方源码。
6
19
 
7
20
  ## 能力
8
21
 
package/lib/client.js CHANGED
@@ -115,6 +115,7 @@ window.__ModuleLoader__.load({
115
115
  "terminal.ready": "终端已连接({alias})",
116
116
  "terminal.exited": "终端已退出({alias})",
117
117
  "terminal.error": "终端错误:{error}",
118
+ "terminal.noWebSocket": "当前页面不支持 WebSocket(应用外壳没有可连接的本地主机地址),请改用浏览器打开的 Web 界面使用终端。",
118
119
  "terminal.auth.title": "二次身份验证 (2FA / 交互式认证)",
119
120
  "terminal.auth.submit": "提交验证码",
120
121
  "terminal.auth.cancel": "取消",
@@ -270,6 +271,7 @@ window.__ModuleLoader__.load({
270
271
  "terminal.ready": "Terminal connected ({alias})",
271
272
  "terminal.exited": "Terminal exited ({alias})",
272
273
  "terminal.error": "Terminal error: {error}",
274
+ "terminal.noWebSocket": "This page cannot carry a WebSocket (the application shell published no local host address to reach). Open the Web UI in a browser to use the terminal.",
273
275
  "terminal.auth.title": "Two-Factor Authentication (2FA)",
274
276
  "terminal.auth.submit": "Submit Code",
275
277
  "terminal.auth.cancel": "Cancel",
@@ -447,6 +449,111 @@ window.__ModuleLoader__.load({
447
449
  const text = search.toString();
448
450
  return text === "" ? "" : "?" + text;
449
451
  }
452
+ /**
453
+ * The WebSocket URL the terminal route is reached at, or undefined when this
454
+ * page cannot carry one.
455
+ *
456
+ * A web page resolves the socket against its own origin, exactly as before.
457
+ *
458
+ * A page delivered by an application on this machine (the official DSH Desktop
459
+ * shell serves its Web GUI from `dsh-app://app/`) cannot carry a socket on its
460
+ * own scheme: the protocol handler forwards HTTP to the local Host but has no
461
+ * upgrade to forward. That is exactly the case the official transport hook
462
+ * covers - the shell publishes `__DSH_TRANSPORT__.streamBaseUrl`, the loopback
463
+ * authority of the Host it owns and already forwards every other request to
464
+ * (issue #1744). The socket is therefore dialed there instead, with the `ws`/
465
+ * `wss` scheme derived from that base, which is what the shell's
466
+ * `onBeforeSendHeaders` hook expects to see before it attaches its
467
+ * authority-bound credential.
468
+ *
469
+ * The base is used only on an application-delivered page. A web page that
470
+ * somehow carries the hook (the remote channel grants `ownsHost` to a paired
471
+ * LAN page, and that page is fenced behind a pairing channel) keeps resolving
472
+ * against its own origin, so this cannot reroute gated traffic onto a host the
473
+ * page is not entitled to reach. A blank authority is treated the same as an
474
+ * application scheme with no base: there is nothing to dial, and the caller
475
+ * gets the actionable reason instead of a socket that can only fail.
476
+ *
477
+ * @param location - the page location to read.
478
+ * @param search - the query string carrying `alias` or `session`.
479
+ * @param streamBaseUrl - `__DSH_TRANSPORT__.streamBaseUrl`, when the shell published one.
480
+ * @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
481
+ */
482
+ function terminalSocketUrl(location, search, streamBaseUrl) {
483
+ if (WEB_PAGE_PROTOCOLS.includes(location.protocol)) return (location.protocol === "https:" ? "wss" : "ws") + "://" + location.host + SSH_API.terminal + search;
484
+ const base = hostAuthorityOf(streamBaseUrl);
485
+ if (base === void 0) return void 0;
486
+ return (base.protocol === "https:" ? "wss" : "ws") + "://" + base.authority + SSH_API.terminal + search;
487
+ }
488
+ /**
489
+ * The authority and scheme of a shell-owned Host base, or undefined when the
490
+ * value is absent, unparsable, or not a network origin this Host can serve
491
+ * (an application scheme could only resolve back to the page that has no
492
+ * socket transport in the first place).
493
+ * @param base - the raw `streamBaseUrl` as published by the transport hook.
494
+ */
495
+ function hostAuthorityOf(base) {
496
+ if (base === void 0 || base === "") return void 0;
497
+ let url;
498
+ try {
499
+ url = new URL(base);
500
+ } catch {
501
+ return;
502
+ }
503
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
504
+ if (url.username !== "" || url.password !== "") return void 0;
505
+ if (url.host === "") return void 0;
506
+ return {
507
+ protocol: url.protocol,
508
+ authority: url.host
509
+ };
510
+ }
511
+ /**
512
+ * The shell-owned Host authority the page is told to reach, read from the
513
+ * official transport hook. The global is written by the shell before any boot
514
+ * entry runs, so it is absent on every page it does not own.
515
+ * @param globals - the global object to read (defaults to the real one).
516
+ * @returns the published base URL, or undefined when the page carries none.
517
+ */
518
+ function transportStreamBaseUrl(globals = globalThis) {
519
+ const published = globals.__DSH_TRANSPORT__?.streamBaseUrl;
520
+ return typeof published === "string" ? published : void 0;
521
+ }
522
+ /**
523
+ * Schemes a network page can be delivered with. Every other scheme belongs to
524
+ * an application on this machine, which is the same classification the remote
525
+ * channel and the update seat use (`isWebPageProtocol` / `isApplicationDeliveredPage`).
526
+ */
527
+ const WEB_PAGE_PROTOCOLS = [
528
+ "http:",
529
+ "https:",
530
+ "blob:",
531
+ "data:",
532
+ "about:",
533
+ "filesystem:"
534
+ ];
535
+ /**
536
+ * A terminal connection that never opened: it reports one failure once the
537
+ * view has attached its `onExit`, so the tab shows why instead of sitting on a
538
+ * spinner, and every later frame is a no-op.
539
+ */
540
+ function failedTerminal(reason) {
541
+ const connection = {
542
+ onReady: void 0,
543
+ onOutput: void 0,
544
+ onExit: void 0,
545
+ onAuthPrompt: void 0,
546
+ send: () => void 0,
547
+ resize: () => void 0,
548
+ sendAuthResponse: () => void 0,
549
+ detach: () => void 0,
550
+ close: () => void 0
551
+ };
552
+ queueMicrotask(() => {
553
+ connection.onExit?.(null, reason);
554
+ });
555
+ return connection;
556
+ }
450
557
  /** The browser half's only data entry point. */
451
558
  var SshApi = class {
452
559
  async listHosts(queryText) {
@@ -674,8 +781,14 @@ window.__ModuleLoader__.load({
674
781
  }
675
782
  /** One terminal socket over either an alias (open) or a session id (attach). */
676
783
  terminalSocket(search) {
677
- const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + search;
678
- const socket = new WebSocket(url);
784
+ const target = terminalSocketUrl(window.location, search, transportStreamBaseUrl());
785
+ if (target === void 0) return failedTerminal(tt("terminal.noWebSocket"));
786
+ let socket;
787
+ try {
788
+ socket = new WebSocket(target);
789
+ } catch (error) {
790
+ return failedTerminal(error instanceof Error ? error.message : String(error));
791
+ }
679
792
  let leaving = false;
680
793
  const sendFrame = (frame) => {
681
794
  if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(frame));
@@ -16302,7 +16415,7 @@ window.__ModuleLoader__.load({
16302
16415
  /** The building package's version, when the bundle carries it. */
16303
16416
  function bakedVersion() {
16304
16417
  try {
16305
- return "0.4.3";
16418
+ return "0.4.5";
16306
16419
  } catch {
16307
16420
  return;
16308
16421
  }
@@ -16379,7 +16492,8 @@ window.__ModuleLoader__.load({
16379
16492
  const inject = [
16380
16493
  "slots",
16381
16494
  "locale",
16382
- "configForms"
16495
+ "configForms",
16496
+ "layout"
16383
16497
  ];
16384
16498
  /**
16385
16499
  * Mount the SSH panel.
@@ -16401,7 +16515,7 @@ window.__ModuleLoader__.load({
16401
16515
  setRuntimeTranslate(ctx.locale.bind(NS));
16402
16516
  } catch {}
16403
16517
  const controller = new PanelController({ panel: { select: (panelId) => {
16404
- ctx.get("layout")?.selectPanel?.(panelId);
16518
+ (ctx.get?.("layout"))?.selectPanel?.(panelId);
16405
16519
  } } });
16406
16520
  const api = new SshApi();
16407
16521
  const settings = bindSettingsReader(ctx, SETTINGS_NS, TERMINAL_FONT_FIELD);
@@ -16418,7 +16532,7 @@ window.__ModuleLoader__.load({
16418
16532
  const disposers = [];
16419
16533
  try {
16420
16534
  disposers.push(registerSshPanel(ctx, controller, api, terminalFont));
16421
- const layoutFace = ctx.get("layout");
16535
+ const layoutFace = ctx.get?.("layout");
16422
16536
  if (layoutFace?.panelInfo !== void 0) {
16423
16537
  const sync = () => {
16424
16538
  const active = layoutFace.panelInfo.getSnapshot().activePanelId;