@hyzyn/dsh-tty 0.18.3 → 0.19.1
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.en.md +61 -30
- package/README.md +49 -27
- package/client.js +39 -32
- package/lib/index.d.ts +367 -1
- package/lib/index.js +417 -135
- package/lib/index.js.map +1 -1
- package/lib/known-hosts.d.ts +7 -1
- package/lib/known-hosts.js +26 -12
- package/lib/known-hosts.js.map +1 -1
- package/lib/probe.d.ts +2 -2
- package/lib/probe.js +7 -35
- package/lib/probe.js.map +1 -1
- package/lib/sftp.d.ts +63 -10
- package/lib/sftp.js +244 -44
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.js +35 -15
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh.d.ts +21 -7
- package/lib/ssh.js +113 -18
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.js +67 -13
- package/lib/tunnels.js.map +1 -1
- package/package.json +8 -3
- package/scripts/build-client.mjs +30 -0
- package/scripts/integration.mjs +1691 -0
- package/scripts/lib/test-sshd.mjs +314 -0
- package/scripts/live.mjs +68 -0
- package/scripts/preview/harness.html +75 -0
- package/scripts/preview/harness.js +875 -0
- package/scripts/preview/mock-host.js +365 -0
- package/scripts/preview.mjs +502 -0
- package/scripts/probe-route-smoke.mjs +111 -0
- package/scripts/probe-smoke.mjs +143 -0
- package/scripts/probe.mjs +240 -0
- package/scripts/sftplimits-smoke.mjs +92 -0
- package/scripts/ssh-smoke.mjs +593 -0
- package/scripts/tui-smoke.mjs +112 -0
- package/scripts/windows-smoke.mjs +179 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* dsh-tty 预览夹具:在纯静态页面里伪造 DSH 宿主环境(module loader / fetch /
|
|
4
|
+
* WebSocket),让 packages/tty/client.js 能在浏览器中独立渲染,用于视觉走查
|
|
5
|
+
* 与截图回归。仅供 scripts/preview.mjs 使用,不随 npm 包发布。
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
const React = window.React
|
|
11
|
+
|
|
12
|
+
/* ---------- react/jsx-runtime 垫片(React 18 UMD 不含该入口) ---------- */
|
|
13
|
+
const jsx = (type, props, key) => {
|
|
14
|
+
const p = {}
|
|
15
|
+
let children
|
|
16
|
+
if (props !== null && props !== undefined) {
|
|
17
|
+
for (const k in props) {
|
|
18
|
+
if (k === 'children') children = props[k]
|
|
19
|
+
else p[k] = props[k]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (key !== undefined) p.key = key
|
|
23
|
+
if (children === undefined) return React.createElement(type, p)
|
|
24
|
+
return Array.isArray(children) ? React.createElement(type, p, ...children) : React.createElement(type, p, children)
|
|
25
|
+
}
|
|
26
|
+
window.__jsxRuntime = { jsx, jsxs: jsx, Fragment: React.Fragment }
|
|
27
|
+
|
|
28
|
+
/* ---------- 假配置(结构与宿主 /api/dsh-tty/config 一致) ---------- */
|
|
29
|
+
const CONFIG = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
announceToAgent: true,
|
|
32
|
+
maxSessions: 4,
|
|
33
|
+
shell: '/bin/zsh',
|
|
34
|
+
term: 'xterm-256color',
|
|
35
|
+
colorTerm: 'truecolor',
|
|
36
|
+
cwd: window.__PREVIEW_CWD || '/home/user/project',
|
|
37
|
+
reconnectGraceSec: 120,
|
|
38
|
+
shellIntegration: true,
|
|
39
|
+
sftpStyle: window.__PREVIEW_SFTP_STYLE || 'dialog',
|
|
40
|
+
persistence: window.__PREVIEW_PERSISTENCE || 'off',
|
|
41
|
+
endOnPageClose: false,
|
|
42
|
+
statsEnabled: true,
|
|
43
|
+
sftpLimits: { maxDownloadMb: 1024, maxUploadMb: 2048, maxUploadFiles: 1000 },
|
|
44
|
+
toolsRegistered: true,
|
|
45
|
+
sshHosts: [
|
|
46
|
+
{ name: 'prod-web-01', host: '198.51.100.20', port: 22, username: 'deploy', auth: 'key', keyPath: '~/.ssh/id_ed25519', passphrase: '', password: '', agentForward: true, persist: false },
|
|
47
|
+
{ name: 'staging-db', host: 'db.staging.internal', port: 2222, username: 'root', auth: 'password', keyPath: '', passphrase: '', password: 'env:STAGING_DB_PASSWORD', agentForward: false, persist: false },
|
|
48
|
+
],
|
|
49
|
+
hostKeys: [
|
|
50
|
+
{ host: '198.51.100.20', port: 22, fingerprint: 'SHA256:9xQ2mVr7Kp3sTf1cZbLd8uYwAe4nHjR6gPoXkMvQ2Bc' },
|
|
51
|
+
{ host: 'db.staging.internal', port: 2222, fingerprint: 'SHA256:3fLp8Rt2Wq9yUvC4dXzN6mBsAe1hKjG7oPwXiMnQ5Bd' },
|
|
52
|
+
],
|
|
53
|
+
tunnels: [
|
|
54
|
+
{ name: 'staging-pg', bookName: 'staging-db', direction: 'local', localPort: 15432, remoteHost: '127.0.0.1', remotePort: 5432, localTargetHost: '127.0.0.1', localTargetPort: 5432, enabled: true },
|
|
55
|
+
{ name: 'prod-redis', bookName: 'prod-web-01', direction: 'local', localPort: 16379, remoteHost: 'redis.internal', remotePort: 6379, localTargetHost: '127.0.0.1', localTargetPort: 6379, enabled: true },
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
window.__PREVIEW_CONFIG = CONFIG
|
|
59
|
+
|
|
60
|
+
const json = (body) => Promise.resolve({ ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) })
|
|
61
|
+
|
|
62
|
+
// 字段名与宿主 /api/dsh-tty/sftp/list 的 entry 一致(isDir / isSymlink / size / mtime)
|
|
63
|
+
const SFTP_FILES = [
|
|
64
|
+
{ name: 'app', isDir: true, size: 4096, mtime: 1757300000000 },
|
|
65
|
+
{ name: 'logs', isDir: true, size: 4096, mtime: 1757312000000 },
|
|
66
|
+
{ name: 'releases', isDir: true, size: 4096, mtime: 1757290000000 },
|
|
67
|
+
{ name: '.env', isDir: false, size: 812, mtime: 1757311000000 },
|
|
68
|
+
{ name: 'docker-compose.yml', isDir: false, size: 2481, mtime: 1757309000000 },
|
|
69
|
+
{ name: 'nginx.conf', isDir: false, size: 1743, mtime: 1757301000000 },
|
|
70
|
+
{ name: 'deploy.sh', isDir: false, size: 5120, mtime: 1757311500000 },
|
|
71
|
+
{ name: 'current', isDir: false, isSymlink: true, size: 17, mtime: 1757311800000 },
|
|
72
|
+
]
|
|
73
|
+
const LOCAL_FILES = [
|
|
74
|
+
{ name: 'docs', isDir: true, size: 4096, mtime: 1757300000000 },
|
|
75
|
+
{ name: 'packages', isDir: true, size: 4096, mtime: 1757312000000 },
|
|
76
|
+
{ name: 'scripts', isDir: true, size: 4096, mtime: 1757290000000 },
|
|
77
|
+
{ name: 'README.md', isDir: false, size: 23194, mtime: 1757311000000 },
|
|
78
|
+
{ name: 'package.json', isDir: false, size: 1450, mtime: 1757309000000 },
|
|
79
|
+
{ name: 'pnpm-lock.yaml', isDir: false, size: 53560, mtime: 1757301000000 },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
const route = (url, body) => {
|
|
83
|
+
if (url.indexOf('/api/dsh-tty/config') === 0) return { ok: true, config: CONFIG }
|
|
84
|
+
if (url.indexOf('/api/dsh-tty/shells') === 0) return { ok: true, shells: ['/bin/zsh', '/bin/bash', '/bin/sh', '/opt/homebrew/bin/fish'] }
|
|
85
|
+
if (url.indexOf('/api/dsh-tty/env-vars') === 0) return { ok: true, names: ['STAGING_DB_PASSWORD', 'PROD_DEPLOY_KEY', 'GITHUB_TOKEN', 'OPENAI_API_KEY', 'SSH_PASSPHRASE'] }
|
|
86
|
+
if (url.indexOf('/api/dsh-tty/tunnels') === 0) return { ok: true, tunnels: [
|
|
87
|
+
{ name: 'staging-pg', state: 'active', connections: 2, totalConnections: 17, error: '', lastForwardError: '' },
|
|
88
|
+
{ name: 'prod-redis', state: 'connecting', connections: 0, totalConnections: 3, error: '', lastForwardError: '' },
|
|
89
|
+
] }
|
|
90
|
+
if (url.indexOf('/api/dsh-tty/sftp/list') === 0) return { ok: true, path: body && body.path ? body.path : '/srv/app', entries: SFTP_FILES }
|
|
91
|
+
if (url.indexOf('/api/dsh-tty/sftp/') === 0) return { ok: true }
|
|
92
|
+
if (url.indexOf('/api/dsh-tty/local-fs/list') === 0) return { ok: true, path: body && body.path ? body.path : (window.__PREVIEW_CWD || '/home/user/project'), entries: LOCAL_FILES }
|
|
93
|
+
if (url.indexOf('/api/dsh-tty/local-fs') === 0) return { ok: true, home: window.__PREVIEW_HOME || '/home/user' }
|
|
94
|
+
// 形状对齐 probeSummary():{ tcp:{ok,ms}, banner:{ok}, hostkey:{state}, auth:{ok,ms} }
|
|
95
|
+
if (url.indexOf('/api/dsh-tty/probe') === 0) return { ok: true, result: {
|
|
96
|
+
tcp: { ok: true, ms: 8 },
|
|
97
|
+
banner: { ok: true },
|
|
98
|
+
hostkey: { state: 'matched' },
|
|
99
|
+
auth: { ok: true, ms: 296 },
|
|
100
|
+
} }
|
|
101
|
+
if (url.indexOf('/api/dsh-tty/ssh-config') === 0) return { ok: true, hosts: [] }
|
|
102
|
+
if (url.indexOf('/api/dsh-tty/known-hosts') === 0) return { ok: true, keys: [] }
|
|
103
|
+
if (url.indexOf('/api/dsh-docker/') === 0) return dockerRoute(url.slice('/api/dsh-docker'.length), body)
|
|
104
|
+
return { ok: true }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ---------- 假 dsh-docker API:让 docker 面板与 tty 的 ttyTerminal 端到端联调 ---------- */
|
|
108
|
+
const DOCKER_TARGETS = [
|
|
109
|
+
{ name: '目标1', kind: 'ssh', label: 'root@192.0.2.10' },
|
|
110
|
+
{ name: '目标2', kind: 'local', label: '本机' },
|
|
111
|
+
// 与 tty 连接簿的 prod-web-01 对齐:连接栏「容器」按钮的命中路径(docker-dock 场景)
|
|
112
|
+
{ name: 'prod-web-01', kind: 'ssh', label: 'deploy@198.51.100.20' },
|
|
113
|
+
]
|
|
114
|
+
// 形状照搬宿主 /config:设置卡片会读 ttyBooks / hostKeys / ttyAvailable,
|
|
115
|
+
// 少一个字段就是「渲染期 undefined.length」——离线场景直接崩
|
|
116
|
+
window.__PREVIEW_DOCKER_CONFIG = {
|
|
117
|
+
enabled: true,
|
|
118
|
+
announceToAgent: true,
|
|
119
|
+
dockerBin: 'docker',
|
|
120
|
+
allowMutations: false,
|
|
121
|
+
allowExec: true,
|
|
122
|
+
pollIntervalSec: 5,
|
|
123
|
+
logTailDefault: 200,
|
|
124
|
+
maxOutputKb: 512,
|
|
125
|
+
execTimeoutSec: 30,
|
|
126
|
+
hostKeys: [],
|
|
127
|
+
ttyBooks: ['prod-web-01'],
|
|
128
|
+
ttyAvailable: true,
|
|
129
|
+
toolsRegistered: [],
|
|
130
|
+
targets: [
|
|
131
|
+
{ name: '目标1', kind: 'ssh', book: '', host: '192.0.2.10', port: 22, username: 'root', auth: 'agent', agentForward: false },
|
|
132
|
+
{ name: '目标2', kind: 'local', book: '', host: '', port: 22, username: '', auth: 'agent', agentForward: false },
|
|
133
|
+
{ name: 'prod-web-01', kind: 'ssh', book: 'prod-web-01', host: '', port: 22, username: '', auth: 'agent', agentForward: false },
|
|
134
|
+
],
|
|
135
|
+
}
|
|
136
|
+
window.__PREVIEW_DOCKER_CONTAINERS = [
|
|
137
|
+
{ id: 'a1b2c3d4e5f6a7b8c9d0', shortId: 'a1b2c3d4e5f6', name: 'app-web-1', image: 'app:2026.09.07', state: 'running', health: 'healthy', status: 'Up 3 days', ports: [{ hostPort: 8080, containerPort: 8080, protocol: 'tcp' }], createdAt: null, runningFor: 'Up 3 days', composeProject: 'app', composeService: 'web' },
|
|
138
|
+
{ id: 'b2c3d4e5f6a7b8c9d0e1', shortId: 'b2c3d4e5f6a7', name: 'app-worker-1', image: 'app:2026.09.07', state: 'running', health: null, status: 'Up 3 days', ports: [], createdAt: null, runningFor: 'Up 3 days', composeProject: 'app', composeService: 'worker' },
|
|
139
|
+
{ id: 'c3d4e5f6a7b8c9d0e1f2', shortId: 'c3d4e5f6a7b8', name: 'postgres', image: 'postgres:16-alpine', state: 'running', health: 'healthy', status: 'Up 5 days', ports: [{ hostPort: 5432, containerPort: 5432, protocol: 'tcp' }], createdAt: null, runningFor: 'Up 5 days', composeProject: null, composeService: null },
|
|
140
|
+
{ id: 'd4e5f6a7b8c9d0e1f2a3', shortId: 'd4e5f6a7b8c9', name: 'redis', image: 'redis:7-alpine', state: 'exited', health: null, status: 'Exited (0) 2 hours ago', ports: [], createdAt: null, runningFor: '', composeProject: null, composeService: null },
|
|
141
|
+
]
|
|
142
|
+
const dockerRoute = (path, body) => {
|
|
143
|
+
if (path === '/config') return { ok: true, config: window.__PREVIEW_DOCKER_CONFIG }
|
|
144
|
+
if (path === '/targets') return { ok: true, targets: DOCKER_TARGETS }
|
|
145
|
+
if (path === '/containers') return { ok: true, containers: window.__PREVIEW_DOCKER_CONTAINERS }
|
|
146
|
+
if (path === '/images') return { ok: true, images: [
|
|
147
|
+
{ id: 'sha256:11aa', shortId: '11aa22bb33cc', tags: ['app:2026.09.07'], size: 184320000, createdAt: null, createdSince: '2 days ago' },
|
|
148
|
+
{ id: 'sha256:22bb', shortId: '22bb33cc44dd', tags: ['postgres:16-alpine'], size: 42000000, createdAt: null, createdSince: '3 weeks ago' },
|
|
149
|
+
] }
|
|
150
|
+
// 详情形状按宿主 /inspect 的真实返回补齐:缺字段会让「概览」页在渲染期崩掉
|
|
151
|
+
// (detail.mounts.length 之类),预览也就看不到日志/概览页
|
|
152
|
+
if (path === '/inspect') {
|
|
153
|
+
const base = window.__PREVIEW_DOCKER_CONTAINERS[0]
|
|
154
|
+
return { ok: true, details: [{
|
|
155
|
+
...base,
|
|
156
|
+
startedAt: '2026-09-06 04:11:22 +0800 CST',
|
|
157
|
+
finishedAt: null,
|
|
158
|
+
exitCode: null,
|
|
159
|
+
restartCount: 0,
|
|
160
|
+
restartPolicy: 'unless-stopped',
|
|
161
|
+
pid: 48213,
|
|
162
|
+
healthLogTail: null,
|
|
163
|
+
mounts: [{ source: '/srv/app', destination: '/app', readWrite: true }],
|
|
164
|
+
networks: [{ name: 'app_default', ip: '172.20.0.7' }],
|
|
165
|
+
entrypoint: 'docker-entrypoint.sh',
|
|
166
|
+
command: 'node server.js',
|
|
167
|
+
workingDir: '/app',
|
|
168
|
+
user: 'node',
|
|
169
|
+
}] }
|
|
170
|
+
}
|
|
171
|
+
// /logs 的真实响应形状是 { id, text, truncated }(见 dsh-docker 宿主 /logs 路由);
|
|
172
|
+
// 早先这里回的是裸字符串,客户端 logs.text 拿到 undefined 直接崩掉整面板
|
|
173
|
+
if (path === '/logs') return { ok: true, logs: {
|
|
174
|
+
id: body && body.id ? body.id : 'app-web-1',
|
|
175
|
+
truncated: false,
|
|
176
|
+
text: '2026-09-09T12:00:00Z INFO server listening on :8080\n2026-09-09T12:00:01Z INFO connected to postgres\n2026-09-09T12:00:02Z WARN slow query 412ms\n2026-09-09T12:00:03Z INFO request GET /health 200 3ms\n',
|
|
177
|
+
} }
|
|
178
|
+
if (path === '/stats') return { ok: true, stats: [null] }
|
|
179
|
+
return { ok: true }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const realFetch = window.fetch ? window.fetch.bind(window) : null
|
|
183
|
+
window.fetch = (url, init) => {
|
|
184
|
+
const target = typeof url === 'string' ? url : String(url && url.url ? url.url : url)
|
|
185
|
+
if (target.indexOf('/api/dsh-tty/') === -1 && target.indexOf('/api/dsh-docker/') === -1) {
|
|
186
|
+
return realFetch ? realFetch(url, init) : json({ ok: true })
|
|
187
|
+
}
|
|
188
|
+
let body = null
|
|
189
|
+
try {
|
|
190
|
+
body = init && typeof init.body === 'string' ? JSON.parse(init.body) : null
|
|
191
|
+
} catch {
|
|
192
|
+
body = null
|
|
193
|
+
}
|
|
194
|
+
return json(route(target, body))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ---------- 假 WebSocket:本地 spawn/ssh 回 ready,并投喂演示输出 ---------- */
|
|
198
|
+
const SESSIONS = []
|
|
199
|
+
class MockSocket extends EventTarget {
|
|
200
|
+
static CONNECTING = 0
|
|
201
|
+
static OPEN = 1
|
|
202
|
+
static CLOSING = 2
|
|
203
|
+
static CLOSED = 3
|
|
204
|
+
constructor(url) {
|
|
205
|
+
super()
|
|
206
|
+
this.url = url
|
|
207
|
+
this.readyState = MockSocket.CONNECTING
|
|
208
|
+
this.onopen = null
|
|
209
|
+
this.onmessage = null
|
|
210
|
+
this.onclose = null
|
|
211
|
+
this.onerror = null
|
|
212
|
+
window.__mockSockets.push(this)
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (this.readyState !== MockSocket.CONNECTING) return
|
|
215
|
+
this.readyState = MockSocket.OPEN
|
|
216
|
+
if (typeof this.onopen === 'function') this.onopen({})
|
|
217
|
+
}, 0)
|
|
218
|
+
}
|
|
219
|
+
addEventListener(type, fn, opts) {
|
|
220
|
+
super.addEventListener(type, fn, opts)
|
|
221
|
+
}
|
|
222
|
+
removeEventListener(type, fn, opts) {
|
|
223
|
+
super.removeEventListener(type, fn, opts)
|
|
224
|
+
}
|
|
225
|
+
_deliver(msg) {
|
|
226
|
+
window.__mockLog.push('out:' + msg.t + (msg.d ? '(' + msg.d.length + ')' : ''))
|
|
227
|
+
const event = new MessageEvent('message', { data: JSON.stringify(msg) })
|
|
228
|
+
if (typeof this.onmessage === 'function') this.onmessage(event)
|
|
229
|
+
this.dispatchEvent(event)
|
|
230
|
+
}
|
|
231
|
+
send(raw) {
|
|
232
|
+
let msg
|
|
233
|
+
try {
|
|
234
|
+
msg = JSON.parse(raw)
|
|
235
|
+
} catch {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
window.__mockLog.push('in:' + msg.t)
|
|
239
|
+
const sid = msg.sid
|
|
240
|
+
if (msg.t === 'spawn' || msg.t === 'ssh') {
|
|
241
|
+
if (!SESSIONS.some((s) => s.sid === sid)) SESSIONS.push({ sid, attachable: true, kind: msg.t === 'ssh' ? 'ssh' : 'local', tmux: msg.persistName })
|
|
242
|
+
const target = msg.t === 'ssh' ? (msg.username || 'deploy') + '@' + (msg.host || '198.51.100.20') + (msg.port && Number(msg.port) !== 22 ? ':' + msg.port : '') : undefined
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
this._deliver(msg.t === 'ssh'
|
|
245
|
+
? { t: 'ready', sid, kind: 'ssh', pid: null, target: msg.name ? target : target, persist: msg.persist === true }
|
|
246
|
+
: { t: 'ready', sid, kind: 'local', pid: 48213, persist: msg.persist === true })
|
|
247
|
+
setTimeout(() => this._deliver({ t: 'data', sid, d: msg.t === 'ssh' ? window.__PREVIEW_SSH_BANNER : window.__PREVIEW_BANNER }), 10)
|
|
248
|
+
}, 20)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (msg.t === 'attach') {
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
this._deliver({ t: 'ready', sid, kind: 'local', pid: 48213, reattached: true })
|
|
254
|
+
setTimeout(() => this._deliver({ t: 'data', sid, d: window.__PREVIEW_BANNER }), 10)
|
|
255
|
+
}, 20)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
if (msg.t === 'sessions') {
|
|
259
|
+
const list = window.__PREVIEW_AT_LIMIT
|
|
260
|
+
? [{ sid: 'other-1', attachable: true }, { sid: 'other-2', attachable: true }, { sid: 'other-3', attachable: true }, { sid: 'other-4', attachable: true }]
|
|
261
|
+
: SESSIONS.slice()
|
|
262
|
+
setTimeout(() => this._deliver({ t: 'sessions', list }), 5)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (msg.t === 'input') {
|
|
266
|
+
// 演示用:回车回一个新提示符,普通字符回显
|
|
267
|
+
if (typeof msg.d === 'string') {
|
|
268
|
+
if (msg.d === '\r' || msg.d === '\n') setTimeout(() => this._deliver({ t: 'data', sid, d: '\r\n\x1b[38;2;122;162;247m❯\x1b[0m ' }), 15)
|
|
269
|
+
else if (!msg.d.includes('\u0003')) setTimeout(() => this._deliver({ t: 'data', sid, d: msg.d }), 5)
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (msg.t === 'statsOn') {
|
|
274
|
+
// 与真实宿主同语义:宿主的订阅路由要求本连接上已有该 sid(会话已 ready)。
|
|
275
|
+
// 客户端若在 spawn 之前/之后就绪没重发,这里就什么都不回——状态条不出来。
|
|
276
|
+
if (!SESSIONS.some((entry) => entry.sid === sid)) {
|
|
277
|
+
window.__mockLog.push('in:statsOn(unknown)')
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
// 服务器状态条(0.17.0):立刻回一帧再每秒推一帧,数值轻微浮动(截图里能看出"在动")
|
|
281
|
+
this._statsSid = sid
|
|
282
|
+
this._statsTick = 0
|
|
283
|
+
if (this._statsTimer) clearInterval(this._statsTimer)
|
|
284
|
+
const push = () => {
|
|
285
|
+
this._statsTick += 1
|
|
286
|
+
const tick = this._statsTick
|
|
287
|
+
// 坏帧注入(stats-broken 场景):字符串 / 数组 / null / 越界值轮流来
|
|
288
|
+
const junk = window.__PREVIEW_STATS_PAYLOAD
|
|
289
|
+
if (Array.isArray(junk) && junk.length > 0) {
|
|
290
|
+
this._deliver({ t: 'stats', sid, stats: junk[(tick - 1) % junk.length] })
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
this._deliver({ t: 'stats', sid, stats: {
|
|
294
|
+
cpuPct: 18 + (tick % 5) * 9,
|
|
295
|
+
cores: 16,
|
|
296
|
+
memUsed: Math.round(53.3e9 + (tick % 3) * 0.9e9),
|
|
297
|
+
memTotal: 64e9,
|
|
298
|
+
memPct: Math.round((83.2 + (tick % 3) * 1.4) * 10) / 10,
|
|
299
|
+
diskUsed: 40.8e9,
|
|
300
|
+
diskTotal: 68.8e9,
|
|
301
|
+
diskPct: 59.3,
|
|
302
|
+
uptimeSec: 2 * 604800 + 4 * 86400 + 7 * 3600 + 16 * 60 + tick,
|
|
303
|
+
tcpConns: 570 + (tick % 7),
|
|
304
|
+
rxRate: 92.3 * 1024,
|
|
305
|
+
txRate: 93.1 * 1024,
|
|
306
|
+
// tempC 缺席:走「CPU温度 无」的降级渲染(与 macOS 本机一致)
|
|
307
|
+
} })
|
|
308
|
+
}
|
|
309
|
+
push()
|
|
310
|
+
this._statsTimer = setInterval(push, 1000)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
if (msg.t === 'statsOff') {
|
|
314
|
+
if (this._statsTimer) {
|
|
315
|
+
clearInterval(this._statsTimer)
|
|
316
|
+
this._statsTimer = null
|
|
317
|
+
}
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (msg.t === 'kill') {
|
|
321
|
+
const idx = SESSIONS.findIndex((s) => s.sid === sid)
|
|
322
|
+
if (idx >= 0) SESSIONS.splice(idx, 1)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
close() {
|
|
326
|
+
if (this._statsTimer) {
|
|
327
|
+
clearInterval(this._statsTimer)
|
|
328
|
+
this._statsTimer = null
|
|
329
|
+
}
|
|
330
|
+
this.readyState = MockSocket.CLOSED
|
|
331
|
+
if (typeof this.onclose === 'function') this.onclose({ code: 1000, wasClean: true })
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
window.__mockSockets = []
|
|
335
|
+
window.__mockLog = []
|
|
336
|
+
window.WebSocket = MockSocket
|
|
337
|
+
|
|
338
|
+
/* ---------- module loader ---------- */
|
|
339
|
+
window.__modules = new Map()
|
|
340
|
+
window.__ModuleLoader__ = {
|
|
341
|
+
load(def) {
|
|
342
|
+
window.__modules.set(def.id, def)
|
|
343
|
+
// 兼容:夹具最早只跑 tty 一个插件
|
|
344
|
+
if (def.id === '@hyzyn/dsh-tty') window.__ttyModule = def
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
window.__PREVIEW_SSH_BANNER =
|
|
349
|
+
'\x1b[38;2;110;118;129mWelcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-45-generic aarch64)\x1b[0m\r\n' +
|
|
350
|
+
'\x1b[38;2;110;118;129mLast login: Mon Sep 8 12:38:52 2026 from 198.51.100.7\x1b[0m\r\n' +
|
|
351
|
+
'\x1b[38;2;126;206;153mdeploy@prod-web-01\x1b[0m:\x1b[38;2;122;162;247m~/app\x1b[0m$ docker compose ps\r\n' +
|
|
352
|
+
'\x1b[38;2;110;118;129mNAME IMAGE STATUS PORTS\x1b[0m\r\n' +
|
|
353
|
+
'app-web-1 app:2026.09.07 \x1b[38;2;126;206;153mUp 3 days\x1b[0m 0.0.0.0:8080->8080/tcp\r\n' +
|
|
354
|
+
'app-worker-1 app:2026.09.07 \x1b[38;2;126;206;153mUp 3 days\x1b[0m\r\n' +
|
|
355
|
+
'\x1b[38;2;126;206;153mdeploy@prod-web-01\x1b[0m:\x1b[38;2;122;162;247m~/app\x1b[0m$ '
|
|
356
|
+
|
|
357
|
+
window.__PREVIEW_BANNER =
|
|
358
|
+
'\x1b[38;2;110;118;129mLast login: Mon Sep 8 12:41:07 on ttys003\x1b[0m\r\n' +
|
|
359
|
+
'\x1b[38;2;122;162;247m~/projects/demo-app\x1b[0m \x1b[38;2;158;206;106mpnpm\x1b[0m -r build\r\n' +
|
|
360
|
+
'\x1b[38;2;97;175;239mpackages/tty build$\x1b[0m tsc -p tsconfig.json && node scripts/build-client.mjs\r\n' +
|
|
361
|
+
' client-src/index.js \x1b[38;2;229;192;123m486.2kb\x1b[0m \x1b[38;2;110;118;129m⟳\x1b[0m 200ms\r\n' +
|
|
362
|
+
'\x1b[38;2;152;195;121m[dsh-tty] client.js built\x1b[0m\r\n' +
|
|
363
|
+
'\x1b[38;2;110;118;129mDone in 1.8s\x1b[0m\r\n' +
|
|
364
|
+
'\x1b[38;2;122;162;247m~/projects/demo-app\x1b[0m \x1b[38;2;122;162;247m❯\x1b[0m '
|
|
365
|
+
})()
|