@linxin666/dsh-ssh 0.3.2 → 0.3.4
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 +2 -2
- package/README.md +7 -3
- package/README.zh.md +7 -3
- package/lib/client.js +71 -0
- package/lib/client.js.map +1 -1
- package/lib/index.js +12 -7
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/telemetry.d.ts +26 -0
- package/lib/types/http.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/mount-once.d.ts +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +6 -1
- package/src/client/panel/SshPanel.tsx +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/dsh-home.ts +9 -3
- package/src/engine/sftp.ts +1 -1
- package/src/http.ts +1 -1
- package/src/index.ts +1 -1
- package/src/loopback.ts +1 -1
- package/src/mount-once.ts +2 -2
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { closeSync, createReadStream, createWriteStream, existsSync, lstatSync,
|
|
|
3
3
|
import { Client } from "ssh2";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
6
7
|
import { createServer } from "node:net";
|
|
7
8
|
import { unlink } from "node:fs/promises";
|
|
8
9
|
import { randomBytes } from "node:crypto";
|
|
@@ -1031,8 +1032,9 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
|
|
|
1031
1032
|
*/
|
|
1032
1033
|
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
1033
1034
|
function expandHome$1(path, home = homedir()) {
|
|
1035
|
+
const j = home.startsWith("/") ? join$1 : join;
|
|
1034
1036
|
if (path === "~") return home;
|
|
1035
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return
|
|
1037
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return j(home, path.slice(2));
|
|
1036
1038
|
return path;
|
|
1037
1039
|
}
|
|
1038
1040
|
/**
|
|
@@ -1042,12 +1044,15 @@ function expandHome$1(path, home = homedir()) {
|
|
|
1042
1044
|
* @returns the absolute DSH home path.
|
|
1043
1045
|
*/
|
|
1044
1046
|
function resolveDshHome(env = process.env, home = homedir()) {
|
|
1047
|
+
const isPosix = home.startsWith("/");
|
|
1048
|
+
const j = isPosix ? join$1 : join;
|
|
1049
|
+
const isAbs = isPosix ? isAbsolute$1 : isAbsolute;
|
|
1045
1050
|
const raw = env.DSH_HOME;
|
|
1046
1051
|
if (raw !== void 0 && raw.trim() !== "") {
|
|
1047
1052
|
const expanded = expandHome$1(raw.trim(), home);
|
|
1048
|
-
return
|
|
1053
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
|
|
1049
1054
|
}
|
|
1050
|
-
return
|
|
1055
|
+
return j(home, ".dsh");
|
|
1051
1056
|
}
|
|
1052
1057
|
/** Resolve the DSH home directory from the live environment. */
|
|
1053
1058
|
function dshHome() {
|
|
@@ -1773,7 +1778,7 @@ function walkLocalDir(root) {
|
|
|
1773
1778
|
const stat = lstatSync(full);
|
|
1774
1779
|
if (stat.isSymbolicLink()) continue;
|
|
1775
1780
|
if (stat.isDirectory()) visit(full);
|
|
1776
|
-
else if (stat.isFile()) files.push(relative(root, full));
|
|
1781
|
+
else if (stat.isFile()) files.push(relative(root, full).replaceAll("\\", "/"));
|
|
1777
1782
|
}
|
|
1778
1783
|
};
|
|
1779
1784
|
visit(root);
|
|
@@ -3455,7 +3460,7 @@ function sshClusterTool(engine) {
|
|
|
3455
3460
|
//#region src/mount-once.ts
|
|
3456
3461
|
/**
|
|
3457
3462
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3458
|
-
* (dsh-web-
|
|
3463
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
3459
3464
|
* the loader accepts a standalone install of the same package side by side;
|
|
3460
3465
|
* without this guard the second instance would still re-register the same
|
|
3461
3466
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
@@ -3468,7 +3473,7 @@ function sshClusterTool(engine) {
|
|
|
3468
3473
|
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
3469
3474
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
3470
3475
|
*/
|
|
3471
|
-
const MOUNTED = Symbol.for("dsh-web
|
|
3476
|
+
const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
|
|
3472
3477
|
function mountedSet() {
|
|
3473
3478
|
const registry = globalThis;
|
|
3474
3479
|
return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
|
|
@@ -3518,7 +3523,7 @@ const DEFAULT_ANNOUNCE = false;
|
|
|
3518
3523
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3519
3524
|
const SECTION_ORDER = 150;
|
|
3520
3525
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3521
|
-
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web
|
|
3526
|
+
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
|
|
3522
3527
|
/**
|
|
3523
3528
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
3524
3529
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
@@ -15,7 +15,7 @@ import { type SshKey } from './locales.ts';
|
|
|
15
15
|
declare module '@deepseek-ai/cordis' {
|
|
16
16
|
interface Context {
|
|
17
17
|
/**
|
|
18
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
18
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
19
19
|
* absent when that group plugin is not installed, so callers fall back to
|
|
20
20
|
* the official settings scope.
|
|
21
21
|
*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous install telemetry shared by the DSH Web UI family plugins.
|
|
3
|
+
*
|
|
4
|
+
* Once per UTC day per browser, each wired plugin sends one heartbeat to
|
|
5
|
+
* the dsh-market edge API listing its package name (and version when known).
|
|
6
|
+
* The payload carries no conversation data and no identifiers beyond a random
|
|
7
|
+
* UUID generated in localStorage; the server hashes it with a deployment salt
|
|
8
|
+
* before storage and never persists IP addresses. Sends are fire-and-forget:
|
|
9
|
+
* failures stay silent and simply retry on a later mount or day. The system
|
|
10
|
+
* is documented in docs/telemetry.md.
|
|
11
|
+
*/
|
|
12
|
+
export type TelemetryChannel = 'market' | 'npm' | 'unknown';
|
|
13
|
+
export interface TelemetryItem {
|
|
14
|
+
/** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Installed version when known; omitted otherwise. */
|
|
17
|
+
version?: string;
|
|
18
|
+
/** Install channel when determinable (market = Workshop install). */
|
|
19
|
+
channel?: TelemetryChannel;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fire the daily heartbeat for the given items at most once per UTC day per
|
|
23
|
+
* browser. Never throws and never blocks the caller. Items without an explicit
|
|
24
|
+
* version inherit the bundle's baked build version.
|
|
25
|
+
*/
|
|
26
|
+
export declare function reportDailyHeartbeat(items: readonly TelemetryItem[]): void;
|
package/lib/types/http.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* edit this shared source and re-run the sync instead of editing a copy.
|
|
11
11
|
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
12
12
|
* belong in the same change as its introduction.
|
|
13
|
-
* @module dsh-web-
|
|
13
|
+
* @module dsh-web-shared/host/http
|
|
14
14
|
*/
|
|
15
15
|
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
16
16
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Config {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const Config: z<Config>;
|
|
40
40
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
41
|
-
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web
|
|
41
|
+
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 $DSH_HOME/dsh-ssh.json\uFF08\u9ED8\u8BA4 ~/.dsh\uFF09\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u8DEF\u5F84\u533A\u5206\uFF1A\u672C\u673A\uFF08dsh host\uFF09\u4E0A\u7684\u6587\u4EF6\u4E0E\u547D\u4EE4\u4E00\u5F8B\u7528\u672C\u5730\u5DE5\u5177\uFF08read / write / edit / bash\uFF09\uFF0Cssh_* \u5DE5\u5177\u53EA\u9488\u5BF9\u8FDC\u7A0B\u4E3B\u673A\u4E0A\u7684\u8DEF\u5F84\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
42
42
|
/**
|
|
43
43
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
44
44
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
package/lib/types/loopback.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
8
8
|
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
9
9
|
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
10
|
-
* @module dsh-web-
|
|
10
|
+
* @module dsh-web-shared/host/loopback
|
|
11
11
|
*/
|
|
12
12
|
import type { IncomingMessage } from 'node:http';
|
|
13
13
|
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3
|
-
* (dsh-web-
|
|
3
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"license": "Apache-2.0",
|
|
90
90
|
"repository": {
|
|
91
91
|
"type": "git",
|
|
92
|
-
"url": "https://github.com/zhu1090093659/dsh-web
|
|
92
|
+
"url": "https://github.com/zhu1090093659/dsh-web.git"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
95
|
"build": "tsc -p tsconfig.build.json && tsdown",
|
package/src/client/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { mountPanel } from './mount.tsx'
|
|
|
23
23
|
import { PanelController } from './panel/controller.ts'
|
|
24
24
|
import type { TerminalFontSource } from './panel/helpers.ts'
|
|
25
25
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
26
|
+
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
26
27
|
|
|
27
28
|
/** Locale namespace this plugin owns. */
|
|
28
29
|
const NS = 'dsh-ssh'
|
|
@@ -39,7 +40,7 @@ interface SshClientSettings {
|
|
|
39
40
|
declare module '@deepseek-ai/cordis' {
|
|
40
41
|
interface Context {
|
|
41
42
|
/**
|
|
42
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
43
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
43
44
|
* absent when that group plugin is not installed, so callers fall back to
|
|
44
45
|
* the official settings scope.
|
|
45
46
|
*/
|
|
@@ -73,6 +74,10 @@ export type { SshKey } from './locales.ts'
|
|
|
73
74
|
* @param ctx - client root context (locale service).
|
|
74
75
|
*/
|
|
75
76
|
export function apply(ctx: ClientContext): void {
|
|
77
|
+
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
78
|
+
// UTC day, package name only, silent failure.
|
|
79
|
+
reportDailyHeartbeat([{ name: '@linxin666/dsh-ssh' }])
|
|
80
|
+
|
|
76
81
|
ctx.effect(() => {
|
|
77
82
|
try {
|
|
78
83
|
return ctx.locale.register(NS, { zh, en })
|
|
@@ -57,7 +57,7 @@ export function SshPanel({ controller, api, terminalFont }: SshPanelProps) {
|
|
|
57
57
|
return (
|
|
58
58
|
<div className={css.panel} data-dsh-plugin="ssh">
|
|
59
59
|
<div className={css.panelHeader}>
|
|
60
|
-
{/* Shared hook: dsh-web-
|
|
60
|
+
{/* Shared hook: dsh-web-all offsets center-view back controls beside the collapsed mobile sidebar. */}
|
|
61
61
|
<button
|
|
62
62
|
type="button"
|
|
63
63
|
className={`${css.ghostButton} ${css.backButton}`}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/telemetry.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Anonymous install telemetry shared by the DSH Web UI family plugins.
|
|
4
|
+
*
|
|
5
|
+
* Once per UTC day per browser, each wired plugin sends one heartbeat to
|
|
6
|
+
* the dsh-market edge API listing its package name (and version when known).
|
|
7
|
+
* The payload carries no conversation data and no identifiers beyond a random
|
|
8
|
+
* UUID generated in localStorage; the server hashes it with a deployment salt
|
|
9
|
+
* before storage and never persists IP addresses. Sends are fire-and-forget:
|
|
10
|
+
* failures stay silent and simply retry on a later mount or day. The system
|
|
11
|
+
* is documented in docs/telemetry.md.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type TelemetryChannel = 'market' | 'npm' | 'unknown'
|
|
15
|
+
|
|
16
|
+
export interface TelemetryItem {
|
|
17
|
+
/** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
|
|
18
|
+
name: string
|
|
19
|
+
/** Installed version when known; omitted otherwise. */
|
|
20
|
+
version?: string
|
|
21
|
+
/** Install channel when determinable (market = Workshop install). */
|
|
22
|
+
channel?: TelemetryChannel
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VISITOR_KEY = 'dsh-web-ui-telemetry-visitor'
|
|
26
|
+
const DAY_KEY_PREFIX = 'dsh-web-ui-telemetry-day:'
|
|
27
|
+
const ENDPOINT = 'https://dsh-market.com/api/telemetry/event'
|
|
28
|
+
|
|
29
|
+
// Baked into each client bundle by shared/tsdown.client.ts (the package's own
|
|
30
|
+
// package.json version at build time). Undefined in dev/test builds.
|
|
31
|
+
declare const __DSH_PKG_VERSION__: string | undefined
|
|
32
|
+
|
|
33
|
+
/** The building package's version, when the bundle carries it. */
|
|
34
|
+
function bakedVersion(): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
return typeof __DSH_PKG_VERSION__ === 'string' && __DSH_PKG_VERSION__ !== ''
|
|
37
|
+
? __DSH_PKG_VERSION__
|
|
38
|
+
: undefined
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
|
|
45
|
+
function visitorId(): string | null {
|
|
46
|
+
try {
|
|
47
|
+
const existing = localStorage.getItem(VISITOR_KEY)
|
|
48
|
+
if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing
|
|
49
|
+
const fresh = crypto.randomUUID().replaceAll('-', '')
|
|
50
|
+
localStorage.setItem(VISITOR_KEY, fresh)
|
|
51
|
+
return fresh
|
|
52
|
+
} catch {
|
|
53
|
+
// Storage unavailable (privacy mode, sandboxed frame): report nothing.
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Drop stale per-day dedup keys so localStorage does not grow forever. */
|
|
59
|
+
function pruneDayKeys(today: string): void {
|
|
60
|
+
try {
|
|
61
|
+
for (let index = localStorage.length - 1; index >= 0; index -= 1) {
|
|
62
|
+
const key = localStorage.key(index)
|
|
63
|
+
if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) {
|
|
64
|
+
localStorage.removeItem(key)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* best effort */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fire the daily heartbeat for the given items at most once per UTC day per
|
|
72
|
+
* browser. Never throws and never blocks the caller. Items without an explicit
|
|
73
|
+
* version inherit the bundle's baked build version.
|
|
74
|
+
*/
|
|
75
|
+
export function reportDailyHeartbeat(items: readonly TelemetryItem[]): void {
|
|
76
|
+
try {
|
|
77
|
+
if (items.length === 0) return
|
|
78
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
79
|
+
// Automation contexts (CDP-driven QA browsers, fresh isolated profiles)
|
|
80
|
+
// mint a new visitor id per run and would inflate instance counts.
|
|
81
|
+
if (navigator.webdriver) return
|
|
82
|
+
if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return
|
|
83
|
+
const visitor = visitorId()
|
|
84
|
+
if (visitor === null) return
|
|
85
|
+
pruneDayKeys(today)
|
|
86
|
+
const payloadItems = items.map((item) => {
|
|
87
|
+
const out: Record<string, string> = { name: item.name }
|
|
88
|
+
const version = item.version ?? bakedVersion()
|
|
89
|
+
if (version !== undefined) out.version = version
|
|
90
|
+
if (item.channel !== undefined) out.channel = item.channel
|
|
91
|
+
return out
|
|
92
|
+
})
|
|
93
|
+
const body = JSON.stringify({ kind: 'heartbeat', visitor, items: payloadItems })
|
|
94
|
+
void fetch(ENDPOINT, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'content-type': 'application/json' },
|
|
97
|
+
body,
|
|
98
|
+
keepalive: true,
|
|
99
|
+
}).then((response) => {
|
|
100
|
+
// Mark the day only after an accepted send, so offline browsers retry
|
|
101
|
+
// on a later mount instead of going dark until tomorrow.
|
|
102
|
+
if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, '1')
|
|
103
|
+
}).catch(() => { /* unreachable endpoint: stay silent */ })
|
|
104
|
+
} catch { /* never break the host UI over telemetry */ }
|
|
105
|
+
}
|
package/src/dsh-home.ts
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
9
|
import { isAbsolute, join } from 'node:path'
|
|
10
|
+
import { isAbsolute as posixIsAbsolute, join as posixJoin } from 'node:path/posix'
|
|
10
11
|
|
|
11
12
|
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
12
13
|
export function expandHome(path: string, home: string = homedir()): string {
|
|
14
|
+
const isPosix = home.startsWith('/')
|
|
15
|
+
const j = isPosix ? posixJoin : join
|
|
13
16
|
if (path === '~') return home
|
|
14
|
-
if (path.startsWith('~/') || path.startsWith('~\\')) return
|
|
17
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return j(home, path.slice(2))
|
|
15
18
|
return path
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -22,12 +25,15 @@ export function expandHome(path: string, home: string = homedir()): string {
|
|
|
22
25
|
* @returns the absolute DSH home path.
|
|
23
26
|
*/
|
|
24
27
|
export function resolveDshHome(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
|
|
28
|
+
const isPosix = home.startsWith('/')
|
|
29
|
+
const j = isPosix ? posixJoin : join
|
|
30
|
+
const isAbs = isPosix ? posixIsAbsolute : isAbsolute
|
|
25
31
|
const raw = env.DSH_HOME
|
|
26
32
|
if (raw !== undefined && raw.trim() !== '') {
|
|
27
33
|
const expanded = expandHome(raw.trim(), home)
|
|
28
|
-
return
|
|
34
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded)
|
|
29
35
|
}
|
|
30
|
-
return
|
|
36
|
+
return j(home, '.dsh')
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/** Resolve the DSH home directory from the live environment. */
|
package/src/engine/sftp.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function walkLocalDir(root: string): string[] {
|
|
|
22
22
|
const stat = lstatSync(full)
|
|
23
23
|
if (stat.isSymbolicLink()) continue
|
|
24
24
|
if (stat.isDirectory()) visit(full)
|
|
25
|
-
else if (stat.isFile()) files.push(relative(root, full))
|
|
25
|
+
else if (stat.isFile()) files.push(relative(root, full).replaceAll('\\', '/'))
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
visit(root)
|
package/src/http.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* edit this shared source and re-run the sync instead of editing a copy.
|
|
12
12
|
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
13
13
|
* belong in the same change as its introduction.
|
|
14
|
-
* @module dsh-web-
|
|
14
|
+
* @module dsh-web-shared/host/http
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http'
|
package/src/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ const DEFAULT_ANNOUNCE = false
|
|
|
64
64
|
const SECTION_ORDER = 150
|
|
65
65
|
|
|
66
66
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
67
|
-
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web
|
|
67
|
+
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Mount the SSH engine, routes, tools, and announcement.
|
package/src/loopback.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
9
9
|
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
10
10
|
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
11
|
-
* @module dsh-web-
|
|
11
|
+
* @module dsh-web-shared/host/loopback
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { IncomingMessage } from 'node:http'
|
package/src/mount-once.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
2
|
/**
|
|
3
3
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
-
* (dsh-web-
|
|
4
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
5
|
* the loader accepts a standalone install of the same package side by side;
|
|
6
6
|
* without this guard the second instance would still re-register the same
|
|
7
7
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const MOUNTED = Symbol.for('dsh-web
|
|
18
|
+
const MOUNTED = Symbol.for('dsh-web.mounted-plugins')
|
|
19
19
|
|
|
20
20
|
interface MountRegistry {
|
|
21
21
|
[MOUNTED]?: Set<string>
|