@hyzyn/dsh-docker 0.7.0 → 0.7.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.
@@ -1981,6 +1981,19 @@
1981
1981
  /* 删除按钮靠右,不跟着列宽被拉长 */
1982
1982
  .dk_targetRow > .dk_btnDanger { justify-self: end; }
1983
1983
 
1984
+ /*
1985
+ * 失效的连接簿引用(引用的条目名不在 tty 连接簿里):标黄整行 + 左侧色条。
1986
+ * 为什么必须可见:book 下拉的候选项来自 ttyBooks,失效的名字没有对应 option,
1987
+ * 下拉会渲染成**空白**——界面上看不出错,直到真去连才报「引用的连接簿条目不存在」。
1988
+ */
1989
+ .dk_targetRow[data-stale] {
1990
+ border-color: var(--dk-warn);
1991
+ box-shadow: inset 2px 0 0 var(--dk-warn);
1992
+ }
1993
+ .dk_hintWarn {
1994
+ color: var(--dk-warn);
1995
+ }
1996
+
1984
1997
  .dk_targetInline {
1985
1998
  display: grid;
1986
1999
  grid-column: 1 / -1;
@@ -17,7 +17,7 @@
17
17
  * (宿主只回 passwordSet / passphraseSet 布尔)。
18
18
  */
19
19
  import dockerCss from './docker.css'
20
- import { bookSessionHost, pickTargetByHost, sessionHostPort } from './session-target.js'
20
+ import { bookSessionHost, pickTargetByHost, sessionHostPort, staleBookRef } from './session-target.js'
21
21
  import { currentSessionIdOf } from './current-session.js'
22
22
 
23
23
  const API = '/api/dsh-docker'
@@ -5958,7 +5958,15 @@ window.__ModuleLoader__.load({
5958
5958
  jsx('span', { className: 'dk_hint', children: 'docker socket 等价于目标主机的 root 权限。开启后,浏览器面板与 agent 都能执行对应操作,请只在可信环境下打开。' }),
5959
5959
 
5960
5960
  sectionTitle('目标'),
5961
- ...form.targets.map((item, index) => jsxs('div', { className: 'dk_targetRow', children: [
5961
+ ...form.targets.map((item, index) => {
5962
+ /*
5963
+ * 失效引用(引用的条目名不在 tty 连接簿里):下拉会**渲染成空白**——没有任何
5964
+ * option 与 item.book 匹配,界面上看不出"这里引用错了",要等真去连才报错。
5965
+ * 这里两件事一起做:给那一行标记 data-stale(标黄),并把失效的名字补成一个
5966
+ * 显式 option,免得用户看到一个空白下拉、以为是"没选"。
5967
+ */
5968
+ const stale = staleBookRef(item, form.ttyBooks)
5969
+ return jsxs('div', { className: 'dk_targetRow', 'data-stale': stale !== undefined ? '1' : undefined, children: [
5962
5970
  jsx('input', { className: 'dk_input', value: item.name, placeholder: '目标名', onChange: (event) => patchTarget(index, { name: event.target.value }) }),
5963
5971
  jsx('select', { className: 'dk_select', value: item.kind, onChange: (event) => patchTarget(index, { kind: event.target.value }), children: [
5964
5972
  jsx('option', { value: 'local', children: '本机' }),
@@ -5971,11 +5979,17 @@ window.__ModuleLoader__.load({
5971
5979
  className: 'dk_select',
5972
5980
  value: item.book ?? '',
5973
5981
  onChange: (event) => patchTarget(index, { book: event.target.value }),
5982
+ title: stale !== undefined ? `引用的连接簿条目「${stale}」不存在——请改选一个已有条目,或清空改为手填` : undefined,
5974
5983
  children: [
5975
5984
  jsx('option', { value: '', children: form.ttyBooks.length === 0 ? '(无 tty 连接簿,请填内联信息)' : '(不用连接簿,手填)' }),
5985
+ // 失效的名字排在最前并显式标注:否则它没有对应 option,下拉显示空白
5986
+ ...(stale !== undefined ? [jsx('option', { value: stale, children: '⚠ 条目已不存在:' + stale }, stale)] : []),
5976
5987
  ...form.ttyBooks.map((name) => jsx('option', { value: name, children: '连接簿:' + name }, name)),
5977
5988
  ],
5978
5989
  }),
5990
+ stale !== undefined
5991
+ ? jsx('span', { className: 'dk_hint dk_hintWarn', children: `引用的条目「${stale}」不在 tty 连接簿里——请改选,或清空后手填` })
5992
+ : null,
5979
5993
  ] }),
5980
5994
  jsx('button', { type: 'button', className: 'dk_btn dk_btnDanger', onClick: () => removeTarget(index), children: '删除' }),
5981
5995
  /*
@@ -5995,7 +6009,8 @@ window.__ModuleLoader__.load({
5995
6009
  (item.auth ?? 'agent') === 'password' ? jsx('input', { className: 'dk_input dk_credential', type: 'password', placeholder: item.passwordSet === true ? '(已设置,留空保持不变)' : 'env:SSH_PASSWORD', value: item.password ?? '', onChange: (event) => patchTarget(index, { password: event.target.value }) }) : null,
5996
6010
  jsx('label', { className: 'dk_check', children: [jsx('input', { type: 'checkbox', checked: item.agentForward === true, onChange: (event) => patchTarget(index, { agentForward: event.target.checked }) }), 'agent forwarding'] }),
5997
6011
  ] }) : null,
5998
- ] }, String(index) + item.name)),
6012
+ ] }, String(index))
6013
+ }),
5999
6014
  jsxs('div', { className: 'dk_row', children: [
6000
6015
  jsx('button', { type: 'button', className: 'dk_btn', onClick: addTarget, children: '添加目标' }),
6001
6016
  jsx('span', { className: 'dk_hint', children: 'SSH 目标推荐直接选 tty 终端面板的连接簿条目(凭证只需维护一处);手填时密码 / 口令建议写 env:NAME(凭据引用:由官方凭据存储解析,缺失时退回环境变量)。' }),
@@ -6627,7 +6642,7 @@ window.__ModuleLoader__.load({
6627
6642
  * 宿主回显的实际连接(tty 挂在 tab.target 上的 `user@host[:port]`)。
6628
6643
  * 从连接簿打开的 SSH 标签,spec 里只有条目名、没有 host;少了这一份,
6629
6644
  * 「按 host 匹配目标」就整条失效——哪怕用户早就按同一台主机配过目标,
6630
- * 只是那条目标引用的是**另一条**连接簿条目(如 HS-248 vs 192.168.80.248)。
6645
+ * 只是那条目标引用的是**另一条**连接簿条目(如 lab-a vs 192.0.2.10)。
6631
6646
  */
6632
6647
  const liveTarget = typeof payload?.tab?.target === 'string' ? payload.tab.target : ''
6633
6648
  const matched = matchTargetForSession(spec, bookName, liveTarget)
@@ -12,7 +12,7 @@
12
12
  * 两个来源共用它:宿主 `/targets` 里每个目标的 `label`,以及宿主在 ready 帧里
13
13
  * 回显、由 tty 挂在 `tab.target` 上的实际连接(`user@host[:port]`)。
14
14
  *
15
- * @param text 形如 `root@192.168.80.248` 或 `root@host:2222`;其它形态返回 undefined。
15
+ * @param text 形如 `root@192.0.2.10` 或 `root@host:2222`;其它形态返回 undefined。
16
16
  */
17
17
  export function parseUserHostPort(text) {
18
18
  const parsed = /^([^@]+)@(.+?)(?::(\d+))?$/.exec(typeof text === 'string' ? text : '')
@@ -27,8 +27,8 @@ export function parseUserHostPort(text) {
27
27
  * 退回宿主回显的 `tab.target` —— **从连接簿打开的 SSH 标签,spec 里只有条目名**
28
28
  * (tty 侧构造:`{t:'ssh', name: bookName, command}`),host 是空串。没有这层兜底,
29
29
  * 按 host 的匹配整条失效:会话主机显示为空、面板判成「该主机尚未配置为 Docker 目标」,
30
- * 哪怕用户早就按同一个主机配了目标(只是连接簿条目名不同,如 `HS-248` vs
31
- * `192.168.80.248`)。
30
+ * 哪怕用户早就按同一个主机配了目标(只是连接簿条目名不同,如 `lab-a` vs
31
+ * `192.0.2.10`)。
32
32
  *
33
33
  * @param spec tty 传过来的会话规格。
34
34
  * @param liveTarget 宿主回显的实际连接(tty 的 `tab.target`)。
@@ -71,7 +71,7 @@ export function pickTargetByHost(rows, session) {
71
71
  * 「该主机没配目标」,只能沿用上一次选的目标 → 屏幕上出现**另一台主机**的容器。
72
72
  *
73
73
  * 有了这一层,只要「会话走的那条连接簿」和「某条 docker 目标」指向同一台主机
74
- * (哪怕两条目标的 book 字段引用的是**不同**条目,如 HS-248 vs 192.168.80.248),
74
+ * (哪怕两条目标的 book 字段引用的是**不同**条目,如 lab-a vs 192.0.2.10),
75
75
  * 连接失败时也能对上目标——而那正是最需要面板的时候。
76
76
  *
77
77
  * @param bookName 会话所属的连接簿条目名(可能为空,如内联连接)。
@@ -90,3 +90,33 @@ export function bookSessionHost(bookName, rows) {
90
90
  }
91
91
  return undefined
92
92
  }
93
+
94
+ /**
95
+ * 目标引用的连接簿条目是否**已失效**(引用的名字在 tty 连接簿里不存在)。
96
+ *
97
+ * 为什么需要它:`book` 那个下拉的候选项来自 `ttyBooks`,一旦引用的名字不在其中,
98
+ * 下拉会**渲染成空白**(没有任何 option 与之匹配)——界面上完全看不出"这里引用错了",
99
+ * 直到真的去连才报「引用的连接簿条目不存在」。用户改了 tty 侧条目名(改名 / 重装 /
100
+ * 换机器)之后就会落到这个状态,且无从发现。
101
+ *
102
+ * 判定只针对 **ssh + 有 book** 的目标:本机目标不看连接簿;没填 book(走内联
103
+ * host/username)也不算失效——那是另一种合法配置。
104
+ *
105
+ * @param target 配置里的目标条目
106
+ * @param ttyBooks `/config.ttyBooks`:tty 连接簿的条目名数组
107
+ * @returns 引用的条目名(失效时),否则 undefined
108
+ */
109
+ export function staleBookRef(target, ttyBooks) {
110
+ if (target === null || typeof target !== 'object') return undefined
111
+ if (target.kind !== 'ssh') return undefined
112
+ const book = typeof target.book === 'string' ? target.book.trim() : ''
113
+ if (book === '') return undefined
114
+ const books = Array.isArray(ttyBooks) ? ttyBooks : []
115
+ // tty 未安装 / 连接簿为空时**不算**引用失效:那是"宿主没装 tty"或"条目还没建",
116
+ // 与"引用了一个不存在的名字"是两回事,标黄会误导(此时整体提示已在卡片顶部给过)。
117
+ if (books.length === 0) return undefined
118
+ for (const name of books) {
119
+ if (name === book) return undefined
120
+ }
121
+ return book
122
+ }