@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/client.js +34 -3
- package/lib/index.d.ts +21 -1
- package/lib/index.js +92 -60
- package/lib/index.js.map +1 -1
- package/package.json +13 -6
- package/scripts/integration.mjs +46 -7
- package/scripts/preview.mjs +16 -1
- package/scripts/probe-route-smoke.mjs +3 -1
- package/scripts/sftplimits-smoke.mjs +3 -1
- package/scripts/windows-smoke.mjs +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyzyn/dsh-tty",
|
|
3
|
-
"version": "0.20.
|
|
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.
|
|
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.
|
|
48
|
-
"@hyzyn/dsh-kit": "0.4.
|
|
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
|
|
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.
|
|
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",
|
package/scripts/integration.mjs
CHANGED
|
@@ -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
|
|
140
|
-
// settings
|
|
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', {
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 会话应存活
|
package/scripts/preview.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|