@hyzyn/dsh-tty 0.20.1 → 0.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyzyn/dsh-tty",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "description": "DSH Web GUI 的终端面板插件:侧边栏「终端」大弹窗,xterm.js + PTY 全交互终端(路线1),ssh2 原生 SSH 远程连接(方案 C),tmux 会话持久化(0.10.0),连接栏扩展点 ttyConnbar(0.13.0)、终端服务 ttyTerminal(0.14.0 open / 0.15.0 mount 就地嵌入)、面板内挂载位 ttyPanel(0.16.0 右侧 dock)、服务器状态条(0.17.0:按标签可见性采端主机 CPU / 内存 / 磁盘 / 在线时长 / TCP / 网速 / 温度,SSH 走同连接的非 PTY exec channel、本地走 os/proc,best-effort、不碰 PTY 流)、连接密码存入官方凭据存储(0.18.0:字段里只留 env: 引用、引用名按资源身份派生、候选来自凭据存储 ∪ 连接簿;设置卡片的连接编辑与连接对话框共用同一套表单)、Windows 宿主支持(0.18.1:默认 shell = %COMSPEC%、不做 POSIX 包装层、PowerShell 走 -NoLogo/-Command;shell 集成 / tmux / 状态条三项在 Windows 本地不适用,README 有完整边界)。",
5
5
  "keywords": [
6
6
  "dsh",
@@ -33,28 +33,30 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dsh": {
36
+ "manifestVersion": 1,
36
37
  "bundle": {
37
38
  "patch": "./cordis.patch.yml"
38
39
  },
39
40
  "engines": {
40
- "dsh": ">=0.1.2-rc.1"
41
+ "dsh": ">=0.1.7-rc.1"
41
42
  },
42
43
  "client": {
43
44
  "platform": "web"
44
45
  }
45
46
  },
46
47
  "dependencies": {
47
- "@deepseek-ai/schemastery": "^3.18.2",
48
- "@hyzyn/dsh-kit": "0.4.1",
48
+ "@deepseek-ai/schemastery": "^3.18.4",
49
+ "@hyzyn/dsh-kit": "0.4.2",
49
50
  "@xterm/headless": "^5.5.0",
50
51
  "ssh2": "^1.17.0",
51
52
  "ws": "^8.18.0"
52
53
  },
53
54
  "peerDependencies": {
54
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1"
55
+ "@deepseek-ai/dsh": "^0.1.7-rc.1",
56
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1"
55
57
  },
56
58
  "devDependencies": {
57
- "@deepseek-ai/cordis": "^4.0.2",
59
+ "@deepseek-ai/cordis": "^4.0.4",
58
60
  "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
59
61
  "@deepseek-ai/dsh-subprocess": "0.1.2-rc.1",
60
62
  "@deepseek-ai/dsh-subprocess-local": "0.1.2-rc.1",
@@ -90,6 +92,11 @@
90
92
  "publishConfig": {
91
93
  "access": "public"
92
94
  },
95
+ "peerDependenciesMeta": {
96
+ "@deepseek-ai/dsh": {
97
+ "optional": true
98
+ }
99
+ },
93
100
  "scripts": {
94
101
  "build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
95
102
  "typecheck": "tsc --noEmit && node ../../scripts/client-lint.mjs",
@@ -136,8 +136,15 @@ async function run() {
136
136
  const app = new Context()
137
137
  const wsFiber = app.plugin(WebServerRuntime, { host: '127.0.0.1', port: 0 })
138
138
  const subFiber = app.plugin(LocalSubprocessRuntime)
139
- // 最小 settings 服务 stub:让插件的 settings 注入回调触发,并可手动派发
140
- // settings/updated 事件(与 dsh-settings 的 dispatch 方式一致)来测配置热生效。
139
+ // 最小 settings 服务 stub(DSH ≥0.1.7 的 SettingsForms:describe / update /
140
+ // configure):让插件的 settings 注入回调触发;配置热生效经 loader 的
141
+ // volatile-update 事件派发(见 emitSettingsUpdated)。
142
+ let settingsValue = {}
143
+ /** 派发 loader 的 volatile-update:真实宿主在 settings.update 之后由 loader 发出。 */
144
+ const emitVolatile = () => {
145
+ const args = ['loader/volatile-update', [], {}, 'test']
146
+ for (const cb of app.events.dispatch('emit', args)) cb(...args)
147
+ }
141
148
  const toolDefs = []
142
149
  /** 输出契约违约(§B33:返回值与声明的 output.schema 不符,真实宿主会直接判工具错)。 */
143
150
  const outputViolations = []
@@ -145,7 +152,15 @@ async function run() {
145
152
  const stubFiber = app.plugin({
146
153
  name: 'settings-stub',
147
154
  apply: (ctx) => {
148
- ctx.provide('settings', { register: () => ({ get: () => ({}), update: async () => {} }) })
155
+ ctx.provide('settings', {
156
+ describe: () => [{ ns: 'tty', value: settingsValue }],
157
+ update: async (ns, patch) => {
158
+ if (ns !== 'tty') throw new Error('未知的 entry: ' + ns)
159
+ settingsValue = { ...settingsValue, ...patch }
160
+ emitVolatile()
161
+ },
162
+ configure: () => () => {},
163
+ })
149
164
  ctx.provide('tools', {
150
165
  register: (definition) => {
151
166
  // 输出契约看门狗(§B33):把 execute 的返回值按**声明的 output.schema**
@@ -202,9 +217,10 @@ async function run() {
202
217
  },
203
218
  })
204
219
  await stubFiber.await()
220
+ /** 模拟「外部改了本 entry 的配置」:更新 stub 值再派发 loader 的 volatile-update。 */
205
221
  const emitSettingsUpdated = (ns, next) => {
206
- const args = ['settings/updated', ns, next, {}, 'test']
207
- for (const cb of app.events.dispatch('emit', args)) cb(...args)
222
+ if (ns === 'tty') settingsValue = { ...next }
223
+ emitVolatile()
208
224
  }
209
225
  const pluginFiber = app.plugin({ name, inject, apply }, { maxSessions: 2, term: 'xterm-256color', colorTerm: 'truecolor' })
210
226
  await wsFiber.await()
@@ -278,7 +294,18 @@ async function run() {
278
294
  if (/sid 已存在/.test(s.state.errors[0])) pass('B3 同 sid 二次 spawn 被拒')
279
295
  else fail('B3 同 sid 二次 spawn 被拒', s.state.errors[0])
280
296
  s.client.send(JSON.stringify({ t: 'kill', sid: 'tab-a' }))
281
- await s.waitFor(() => s.state.exited !== null, 10000, 'exit')
297
+ try {
298
+ await s.waitFor(() => s.state.exited !== null, 10000, 'exit')
299
+ } catch (error) {
300
+ // 诊断(CI 上 Linux 专属复现):区分「kill 没生效(服务端仍列着它)」与
301
+ // 「会话已消失、只是 exit 帧没发(PTY 句柄的 done 未兑现)」。
302
+ s.client.send(JSON.stringify({ t: 'sessions' }))
303
+ await sleep(500)
304
+ const sessionsFrame = [...s.state.frames].reverse().find((f) => f.t === 'sessions')
305
+ console.error(' [diag] 收到的帧类型=' + JSON.stringify(s.state.frames.map((f) => f.t)))
306
+ console.error(' [diag] 服务端会话清单=' + JSON.stringify(sessionsFrame?.list ?? null))
307
+ throw error
308
+ }
282
309
  s.client.close()
283
310
  }
284
311
 
@@ -1187,9 +1214,11 @@ async function run() {
1187
1214
  execFile(file, args, { timeout: 5000 }, (error, stdout, stderr) => resolve({ error, stdout: String(stdout), stderr: String(stderr) }))
1188
1215
  })
1189
1216
  })
1217
+ let tmuxListError = null
1190
1218
  const tmuxList = async () => {
1191
1219
  const out = await execFileP('tmux', ['-L', 'dsh-tty', 'list-sessions', '-F', '#{session_name}'])
1192
- return out.error !== null && out.error !== undefined ? [] : out.stdout.trim().split('\n').filter(Boolean)
1220
+ tmuxListError = out.error === null || out.error === undefined ? null : (out.stderr || String(out.error))
1221
+ return tmuxListError !== null ? [] : out.stdout.trim().split('\n').filter(Boolean)
1193
1222
  }
1194
1223
  const post = async (body) => {
1195
1224
  const res = await fetch(`http://127.0.0.1:${port}/api/dsh-tty/config`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
@@ -1274,8 +1303,18 @@ async function run() {
1274
1303
  await post({ reconnectGraceSec: 1 })
1275
1304
  const w1 = openSession(port)
1276
1305
  await w1.open()
1306
+ const cfgBefore = await (await fetch(`http://127.0.0.1:${port}/api/dsh-tty/config`)).json()
1277
1307
  w1.client.send(JSON.stringify({ t: 'spawn', sid: 'b26a', cols: 80, rows: 24, persist: true, persistName: 'b26resume' }))
1278
1308
  await w1.waitFor(() => w1.state.ready, 10000, 'b26a ready')
1309
+ {
1310
+ const readyFrame = w1.state.frames.find((f) => f.t === 'ready' && f.sid === 'b26a')
1311
+ const listedAfterSpawn = await tmuxList()
1312
+ console.error(' [diag] persistence=' + String(cfgBefore?.config?.persistence)
1313
+ + ' ready.persist=' + String(readyFrame?.persist)
1314
+ + ' tmuxAfterSpawn=' + JSON.stringify(listedAfterSpawn)
1315
+ + ' TMPDIR=' + JSON.stringify(process.env.TMPDIR ?? null)
1316
+ + ' tmuxListError=' + JSON.stringify(tmuxListError))
1317
+ }
1279
1318
  w1.client.send(JSON.stringify({ t: 'input', sid: 'b26a', d: 'printf "B26MARK-%s\\n" resume\n' }))
1280
1319
  await w1.waitFor(() => /B26MARK-resume/.test(w1.state.text), 10000, '标记输出')
1281
1320
  // 异常断开 → 孤儿 → 保活 1s + 回收器扫描(≤10s)只杀 PTY;tmux 会话应存活
@@ -12,6 +12,8 @@
12
12
  * node scripts/preview.mjs --list # 列出场景
13
13
  * node scripts/preview.mjs --build # 顺带重建 client.js(默认不重建,产物落后直接报错)
14
14
  * node scripts/preview.mjs --theme=light # 浅色主题
15
+ * node scripts/preview.mjs --width=560 # 压窄视口走查窄容器排版(默认 1680)
16
+ * PREVIEW_CHROME_ARGS=--no-sandbox node scripts/preview.mjs # 受限沙箱 / CI 里 Chrome 起不来时
15
17
  *
16
18
  * 需要本机有 Chrome/Chromium(默认找 playwright 缓存的 Chrome for Testing,
17
19
  * 也可用 CHROME_PATH 指定)。
@@ -37,6 +39,18 @@ const wantBuild = flags.has('--build')
37
39
  /** 输出子目录:--out=shots-light 之类,便于并排比较明暗主题。 */
38
40
  const outName = (argv.find((a) => a.startsWith('--out=')) || '--out=shots').split('=')[1]
39
41
  const shotsDir = join(previewDir, outName)
42
+ /**
43
+ * 视口宽度(默认 1680)。真实宿主里这张卡片常挂在窄容器里(插件管理页 / 侧栏),
44
+ * 1680px 的宽视口会把「一行塞不塞得下」这类排版问题盖掉——走查窄排版时用
45
+ * `--width=560` 之类显式压窄。
46
+ */
47
+ const viewportWidth = Number((argv.find((a) => a.startsWith('--width=')) || '--width=1680').split('=')[1]) || 1680
48
+ /**
49
+ * 额外 Chrome 参数(空格分隔)。受限环境(文件沙箱 / CI 容器)里 Chrome 自己的
50
+ * sandbox 起不来,CDP 会一直卡在 `Page.enable` 直到超时——此时用
51
+ * `PREVIEW_CHROME_ARGS=--no-sandbox`。刻意不做默认:那会削弱所有调用方的隔离。
52
+ */
53
+ const extraChromeArgs = (process.env.PREVIEW_CHROME_ARGS ?? '').split(' ').filter((a) => a !== '')
40
54
 
41
55
  /* ------------------------------- 场景清单 ------------------------------- */
42
56
 
@@ -331,6 +345,7 @@ async function launchChrome() {
331
345
  '--disable-webgl',
332
346
  '--force-color-profile=srgb',
333
347
  '--font-render-hinting=none',
348
+ ...extraChromeArgs,
334
349
  'about:blank',
335
350
  ], { stdio: ['ignore', 'ignore', 'pipe'] })
336
351
 
@@ -475,7 +490,7 @@ async function main() {
475
490
  await ensureVendor()
476
491
  log('启动 headless Chrome(' + theme + ' 主题)')
477
492
  const { child, cdp, userDataDir } = await launchChrome()
478
- await cdp.send('Emulation.setDeviceMetricsOverride', { width: 1680, height: 1050, deviceScaleFactor: 2, mobile: false })
493
+ await cdp.send('Emulation.setDeviceMetricsOverride', { width: viewportWidth, height: 1050, deviceScaleFactor: 2, mobile: false })
479
494
  let failed = 0
480
495
  try {
481
496
  for (const name of wanted) {
@@ -24,7 +24,9 @@ async function run() {
24
24
  const stubFiber = app.plugin({
25
25
  name: 'settings-stub',
26
26
  apply: (ctx) => {
27
- ctx.provide('settings', { register: () => ({ get: () => ({}), update: async () => {} }) })
27
+ // DSH ≥0.1.7 的 settings 服务(SettingsForms):describe / update / configure。
28
+ // 本用例不测配置持久化,空 describe 即可(插件的 settings effect 拿到空值)。
29
+ ctx.provide('settings', { describe: () => [], update: async () => {}, configure: () => () => {} })
28
30
  ctx.provide('tools', { register: () => () => {} })
29
31
  // 与真实契约同形(section/context 返回注销器):插件热应用时会调用重建
30
32
  ctx.provide('systemPrompt', {
@@ -18,7 +18,9 @@ async function run() {
18
18
  const stubFiber = app.plugin({
19
19
  name: 'settings-stub',
20
20
  apply: (ctx) => {
21
- ctx.provide('settings', { register: () => ({ get: () => ({}), update: async () => {} }) })
21
+ // DSH ≥0.1.7 的 settings 服务(SettingsForms):describe / update / configure。
22
+ // 本用例不测配置持久化,空 describe 即可(插件的 settings effect 拿到空值)。
23
+ ctx.provide('settings', { describe: () => [], update: async () => {}, configure: () => () => {} })
22
24
  ctx.provide('tools', { register: () => () => {} })
23
25
  // 与真实契约同形(section/context 返回注销器):插件热应用时会调用重建
24
26
  ctx.provide('systemPrompt', {
@@ -94,7 +94,9 @@ async function run() {
94
94
  const stubFiber = app.plugin({
95
95
  name: 'windows-smoke-stub',
96
96
  apply: (ctx) => {
97
- ctx.provide('settings', { register: () => ({ get: () => ({}), update: async () => {} }) })
97
+ // DSH ≥0.1.7 的 settings 服务(SettingsForms):describe / update / configure。
98
+ // 本用例不测配置持久化,空 describe 即可(插件的 settings effect 拿到空值)。
99
+ ctx.provide('settings', { describe: () => [], update: async () => {}, configure: () => () => {} })
98
100
  ctx.provide('tools', { register: () => () => {} })
99
101
  },
100
102
  })