@hyzyn/dsh-docker 0.6.4 → 0.7.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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @hyzyn/dsh-docker — 「当前 SSH 会话 → 已配置 docker 目标」的匹配纯逻辑。
3
+ *
4
+ * 为什么单独成文件:`client-src/index.js` 是浏览器 IIFE 入口,没有导出,逻辑放在
5
+ * 里面就只能靠真机点。这三个函数都是纯函数、不碰任何模块级缓存,抽出来可以直接
6
+ * 用 vitest 钉住(esbuild 打包时按普通本地模块内联,产物形态不变)。
7
+ */
8
+
9
+ /**
10
+ * 解析 `user@host[:port]` 形态的目标文本。
11
+ *
12
+ * 两个来源共用它:宿主 `/targets` 里每个目标的 `label`,以及宿主在 ready 帧里
13
+ * 回显、由 tty 挂在 `tab.target` 上的实际连接(`user@host[:port]`)。
14
+ *
15
+ * @param text 形如 `root@192.168.80.248` 或 `root@host:2222`;其它形态返回 undefined。
16
+ */
17
+ export function parseUserHostPort(text) {
18
+ const parsed = /^([^@]+)@(.+?)(?::(\d+))?$/.exec(typeof text === 'string' ? text : '')
19
+ if (parsed === null) return undefined
20
+ return { host: parsed[2], port: Number(parsed[3] ?? 22) }
21
+ }
22
+
23
+ /**
24
+ * 会话实际的 host:port。
25
+ *
26
+ * 优先用 spec 自带的 host(内联规格 `{t:'ssh', host, port, username}`);spec 没带时
27
+ * 退回宿主回显的 `tab.target` —— **从连接簿打开的 SSH 标签,spec 里只有条目名**
28
+ * (tty 侧构造:`{t:'ssh', name: bookName, command}`),host 是空串。没有这层兜底,
29
+ * 按 host 的匹配整条失效:会话主机显示为空、面板判成「该主机尚未配置为 Docker 目标」,
30
+ * 哪怕用户早就按同一个主机配了目标(只是连接簿条目名不同,如 `HS-248` vs
31
+ * `192.168.80.248`)。
32
+ *
33
+ * @param spec tty 传过来的会话规格。
34
+ * @param liveTarget 宿主回显的实际连接(tty 的 `tab.target`)。
35
+ */
36
+ export function sessionHostPort(spec, liveTarget) {
37
+ const host = typeof spec?.host === 'string' ? spec.host : ''
38
+ const rawPort = Number(spec?.port)
39
+ if (host !== '') return { host, port: Number.isInteger(rawPort) && rawPort > 0 ? rawPort : 22 }
40
+ return parseUserHostPort(liveTarget)
41
+ }
42
+
43
+ /**
44
+ * 在已配置目标里按 host:port 找出匹配的那个(`label` 形如 `user@host:port`)。
45
+ *
46
+ * **用户名不参与比较**:同一台主机常用不同账号配多条连接簿条目(root / hsadmin),
47
+ * 只按账号比会漏配。端口按 22 缺省(label 省略端口时视为 22)。
48
+ *
49
+ * @param rows 宿主 `/targets` 返回的目标行。
50
+ * @param session `sessionHostPort` 的结果;undefined 表示无从判断,直接不匹配。
51
+ */
52
+ export function pickTargetByHost(rows, session) {
53
+ if (session === undefined) return undefined
54
+ for (const row of Array.isArray(rows) ? rows : []) {
55
+ if (row === null || typeof row !== 'object' || row.kind !== 'ssh') continue
56
+ const parsed = parseUserHostPort(row.label)
57
+ if (parsed === undefined) continue
58
+ if (parsed.host === session.host && parsed.port === session.port) return row.name
59
+ }
60
+ return undefined
61
+ }
62
+
63
+ /**
64
+ * 用连接簿条目名解析出 host:port(宿主 `/config` 的 `ttyBookHosts`)。
65
+ *
66
+ * 这是 `sessionHostPort` 的第二层兜底,补的正是「**连接还没建立 / 建立失败**」这段窗口:
67
+ *
68
+ * - 从连接簿打开的 SSH 标签,spec 里只有条目名(没有 host);
69
+ * - 宿主回显的 `tab.target` 要等连接成功才有值;
70
+ * - 于是握手超时 / 主机没开机时,`sessionHostPort` 返回 undefined,面板判成
71
+ * 「该主机没配目标」,只能沿用上一次选的目标 → 屏幕上出现**另一台主机**的容器。
72
+ *
73
+ * 有了这一层,只要「会话走的那条连接簿」和「某条 docker 目标」指向同一台主机
74
+ * (哪怕两条目标的 book 字段引用的是**不同**条目,如 HS-248 vs 192.168.80.248),
75
+ * 连接失败时也能对上目标——而那正是最需要面板的时候。
76
+ *
77
+ * @param bookName 会话所属的连接簿条目名(可能为空,如内联连接)。
78
+ * @param rows `/config.ttyBookHosts`:`[{ name, host, port }]`(宿主只回 name/host/port)。
79
+ * @returns `{ host, port }`;条目不存在或没填 host 时 undefined(不猜)。
80
+ */
81
+ export function bookSessionHost(bookName, rows) {
82
+ if (typeof bookName !== 'string' || bookName === '') return undefined
83
+ for (const row of Array.isArray(rows) ? rows : []) {
84
+ if (row === null || typeof row !== 'object') continue
85
+ if (row.name !== bookName) continue
86
+ const host = typeof row.host === 'string' ? row.host.trim() : ''
87
+ if (host === '') return undefined
88
+ const rawPort = Number(row.port)
89
+ return { host, port: Number.isInteger(rawPort) && rawPort > 0 ? rawPort : 22 }
90
+ }
91
+ return undefined
92
+ }