dsh-ssh-terminal 0.1.1 → 0.1.3
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/CHANGELOG.md +27 -0
- package/lib/client.js +23 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +78 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Desktop freeze (renderer spin)**: the log container used an inline
|
|
10
|
+
`ref={(el) => setLogEl(el)}` callback. React re-attaches an inline ref on
|
|
11
|
+
every commit, so the component re-rendered itself in a tight loop and
|
|
12
|
+
pinned the renderer main thread at 100%, freezing the whole DSH desktop
|
|
13
|
+
UI. The DOM node is now held in a stable `useRef`.
|
|
14
|
+
- **Unbounded history poisoning**: single command outputs were stored
|
|
15
|
+
without a cap (a real-world history file grew to 31 MB), and every
|
|
16
|
+
appended step re-serialized and rewrote the whole file synchronously.
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
|
|
20
|
+
- Per-event text is capped at 4000 chars; legacy oversized entries are
|
|
21
|
+
clipped at load. The interactive terminal stream is ring-trimmed to a
|
|
22
|
+
~400 KB tail.
|
|
23
|
+
- History persistence is throttled to at most one write per second and
|
|
24
|
+
flushed when the plugin stops.
|
|
25
|
+
- `status` polling is rev-gated: when nothing changed the host answers a
|
|
26
|
+
tiny `unchanged` response instead of resending the transcript; the poll
|
|
27
|
+
interval is 1500 ms (was 600 ms) and the busy spinner is a CSS animation
|
|
28
|
+
instead of a 50 ms re-render loop.
|
|
29
|
+
- Per-connection step counts are cached per history revision instead of
|
|
30
|
+
scanning the whole history on every `list` call.
|
|
31
|
+
|
|
5
32
|
## [0.1.0] - 2026-08-29
|
|
6
33
|
|
|
7
34
|
- Initial release.
|
package/lib/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
|
|
14
14
|
}
|
|
15
15
|
const SSH_CSS = `
|
|
16
|
-
.dshssh-body{display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}
|
|
16
|
+
.dshssh-body{position:relative;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}
|
|
17
17
|
.dshssh-head{display:flex;align-items:center;gap:8px;}
|
|
18
18
|
.dshssh-title{font-weight:700;flex:1;}
|
|
19
19
|
.dshssh-status{font-weight:600;}
|
|
@@ -34,7 +34,7 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
.dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}
|
|
35
35
|
.dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}
|
|
36
36
|
.dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}
|
|
37
|
-
.dshssh-context-menu{position:
|
|
37
|
+
.dshssh-context-menu{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}
|
|
38
38
|
.dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}
|
|
39
39
|
.dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}
|
|
40
40
|
.dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}
|
|
@@ -89,9 +89,10 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
const [connected, setConnected] = (0, react.useState)(false);
|
|
90
90
|
const [target, setTarget] = (0, react.useState)("");
|
|
91
91
|
const [steps, setSteps] = (0, react.useState)([]);
|
|
92
|
-
const
|
|
92
|
+
const logRef = (0, react.useRef)(null);
|
|
93
93
|
const [contextMenu, setContextMenu] = (0, react.useState)(null);
|
|
94
94
|
const refreshSeq = (0, react.useRef)(0);
|
|
95
|
+
const statusRevRef = (0, react.useRef)({});
|
|
95
96
|
const selectedSession = sessions.find((s) => s.connectionId === connectionId);
|
|
96
97
|
const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId);
|
|
97
98
|
const canRun = connected && !!selectedSession && !!selectedSession.connected;
|
|
@@ -112,8 +113,13 @@ window.__ModuleLoader__.load({
|
|
|
112
113
|
setSteps([]);
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
|
-
const t = await callApi("status", {
|
|
116
|
+
const t = await callApi("status", {
|
|
117
|
+
connectionId: selected,
|
|
118
|
+
rev: statusRevRef.current[selected] || 0
|
|
119
|
+
});
|
|
116
120
|
if (seq !== refreshSeq.current) return;
|
|
121
|
+
if (t && t.unchanged) return;
|
|
122
|
+
if (t && typeof t.rev === "number") statusRevRef.current[selected] = t.rev;
|
|
117
123
|
setConnected(!!(t && t.connected));
|
|
118
124
|
setTarget(t && t.target || "");
|
|
119
125
|
setSteps(t && t.steps || []);
|
|
@@ -126,7 +132,7 @@ window.__ModuleLoader__.load({
|
|
|
126
132
|
return;
|
|
127
133
|
}
|
|
128
134
|
refresh(connectionId);
|
|
129
|
-
const stop = ctx.interval(() => refresh(connectionId),
|
|
135
|
+
const stop = ctx.interval(() => refresh(connectionId), 1500);
|
|
130
136
|
return () => {
|
|
131
137
|
refreshSeq.current += 1;
|
|
132
138
|
try {
|
|
@@ -135,10 +141,10 @@ window.__ModuleLoader__.load({
|
|
|
135
141
|
};
|
|
136
142
|
}, [visible, connectionId]);
|
|
137
143
|
(0, react.useEffect)(() => {
|
|
138
|
-
const el =
|
|
144
|
+
const el = logRef.current;
|
|
139
145
|
if (!el) return;
|
|
140
146
|
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) el.scrollTop = el.scrollHeight;
|
|
141
|
-
}, [steps
|
|
147
|
+
}, [steps]);
|
|
142
148
|
async function connect() {
|
|
143
149
|
setBusy(true);
|
|
144
150
|
setErr("");
|
|
@@ -238,8 +244,15 @@ window.__ModuleLoader__.load({
|
|
|
238
244
|
onContextMenu: (e) => {
|
|
239
245
|
e.preventDefault();
|
|
240
246
|
e.stopPropagation();
|
|
241
|
-
const
|
|
242
|
-
const
|
|
247
|
+
const body = e.currentTarget.closest(".dshssh-body");
|
|
248
|
+
const rect = body ? body.getBoundingClientRect() : {
|
|
249
|
+
left: 0,
|
|
250
|
+
top: 0,
|
|
251
|
+
width: window.innerWidth,
|
|
252
|
+
height: window.innerHeight
|
|
253
|
+
};
|
|
254
|
+
const x = Math.max(4, Math.min(e.clientX - rect.left, rect.width - 160));
|
|
255
|
+
const y = Math.max(4, Math.min(e.clientY - rect.top, rect.height - 80));
|
|
243
256
|
setContextMenu({
|
|
244
257
|
connectionId: s.connectionId,
|
|
245
258
|
x,
|
|
@@ -293,7 +306,7 @@ window.__ModuleLoader__.load({
|
|
|
293
306
|
onClick: disconnect
|
|
294
307
|
}, "断开")), (0, react.createElement)("div", {
|
|
295
308
|
className: "dshssh-log",
|
|
296
|
-
ref:
|
|
309
|
+
ref: logRef
|
|
297
310
|
}, steps.length ? steps.map((s, i) => (0, react.createElement)("div", {
|
|
298
311
|
key: i,
|
|
299
312
|
className: "dshssh-step dshssh-" + s.kind
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useState","useRef","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["import { createElement, useState, useEffect, useRef } from 'react'\n\nconst API = '/ssh-terminal/api'\n\nfunction fmt(ts) {\n const d = new Date(ts)\n const p = (n) => String(n).padStart(2, '0')\n return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())\n}\n\nconst SSH_CSS = `\n.dshssh-body{display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}\n.dshssh-head{display:flex;align-items:center;gap:8px;}\n.dshssh-title{font-weight:700;flex:1;}\n.dshssh-status{font-weight:600;}\n.dshssh-status.ok{color:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-status.off{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-layout{display:flex;gap:8px;flex:1;min-height:0;}\n.dshssh-sessions{width:150px;min-width:125px;display:flex;flex-direction:column;gap:6px;min-height:0;}\n.dshssh-session-title{font-size:11px;font-weight:700;color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-session-list{display:flex;flex-direction:column;gap:5px;overflow:auto;min-height:0;}\n.dshssh-session{display:flex;align-items:center;gap:6px;width:100%;padding:6px 7px;text-align:left;border-radius:7px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2,rgba(148,163,184,.08));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:11px;}\n.dshssh-session:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-session.selected{border-color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-session-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-session-dot.ok{background:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;}\n.dshssh-session-name,.dshssh-session-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.dshssh-session-name{font-weight:600;}\n.dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}\n.dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}\n.dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}\n.dshssh-context-menu{position:fixed;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}\n.dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}\n.dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-main{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}\n.dshssh-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}\n.dshssh-input{padding:4px 7px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-bg-layer-3,rgba(2,6,23,.5));color:var(--dsw-alias-label-primary,inherit);font-size:12px;min-width:70px;}\n.dshssh-input::placeholder{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-btn{padding:4px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:12px;}\n.dshssh-btn:hover{background:var(--dsw-alias-interactive-bg-active,rgba(148,163,184,.28));}\n.dshssh-btn:disabled{opacity:.5;cursor:default;}\n.dshssh-log{flex:1;min-height:120px;overflow:auto;background:var(--dsw-alias-bg-layer-1,rgba(2,6,23,.55));border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.25));border-radius:8px;padding:8px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;}\n.dshssh-step{margin:0 0 6px;}\n.dshssh-cmd{color:var(--dsw-alias-accent,#93c5fd);}\n.dshssh-cmd::before{content:\"❯ \";color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-out{color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-connect{color:var(--dsw-alias-state-success-primary,#86efac);}\n.dshssh-disconnect{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-system{color:var(--dsw-alias-label-tertiary,#94a3b8);font-style:italic;}\n.dshssh-err{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n`\n\nasync function callApi(action, extra = undefined) {\n const res = await fetch(API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action, ...(extra || {}) }),\n })\n return await res.json()\n}\n\nfunction injectCss() {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"dsh-ssh-terminal\"]')) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-ssh-terminal'\n tag.dataset.pluginCss = 'dsh-ssh-terminal'\n tag.textContent = SSH_CSS\n document.head.appendChild(tag)\n}\n\nfunction SshBody(props) {\n const ctx = props.ctx\n const visible = props.visible === undefined ? true : props.visible\n const [busy, setBusy] = useState(false)\n const [err, setErr] = useState('')\n const [hostV, setHostV] = useState('')\n const [userV, setUserV] = useState('')\n const [portV, setPortV] = useState('22')\n const [passV, setPassV] = useState('')\n const [cmdV, setCmdV] = useState('')\n const [sessions, setSessions] = useState([])\n const [connectionId, setConnectionId] = useState('')\n const [connected, setConnected] = useState(false)\n const [target, setTarget] = useState('')\n const [steps, setSteps] = useState([])\n const [logEl, setLogEl] = useState(null)\n const [contextMenu, setContextMenu] = useState(null)\n const refreshSeq = useRef(0)\n\n const selectedSession = sessions.find((s) => s.connectionId === connectionId)\n const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId)\n const canRun = connected && !!selectedSession && !!selectedSession.connected\n\n async function refresh(preferredId) {\n const seq = ++refreshSeq.current\n try {\n const list = await callApi('list')\n if (seq !== refreshSeq.current) return\n const nextSessions = Array.isArray(list && list.sessions) ? list.sessions : []\n setSessions(nextSessions)\n const preferred = preferredId || connectionId\n const selected = nextSessions.some((s) => s.connectionId === preferred)\n ? preferred\n : (list && list.activeConnectionId && nextSessions.some((s) => s.connectionId === list.activeConnectionId)\n ? list.activeConnectionId\n : ((nextSessions.find((s) => s.connected) || nextSessions[0] || {}).connectionId || ''))\n if (selected !== connectionId) setConnectionId(selected)\n if (!selected) {\n if (seq !== refreshSeq.current) return\n setConnected(false)\n setTarget('')\n setSteps([])\n return\n }\n const t = await callApi('status', { connectionId: selected })\n if (seq !== refreshSeq.current) return\n setConnected(!!(t && t.connected))\n setTarget((t && t.target) || '')\n setSteps((t && t.steps) || [])\n } catch (e) {}\n }\n\n useEffect(() => {\n injectCss()\n if (visible === false) { refreshSeq.current += 1; return }\n refresh(connectionId)\n const stop = ctx.interval(() => refresh(connectionId), 600)\n return () => {\n refreshSeq.current += 1\n try { stop() } catch (e) {}\n }\n }, [visible, connectionId])\n\n useEffect(() => {\n const el = logEl\n if (!el) return\n const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40\n if (nearBottom) el.scrollTop = el.scrollHeight\n }, [steps, logEl])\n\n async function connect() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('connect', { host: hostV, user: userV, port: Number(portV) || 22, password: passV || undefined })\n if (r && r.error) setErr(String(r.error))\n await refresh(r && r.connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function run() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('exec', { connectionId, command: cmdV, waitMs: 15000 })\n if (r && r.error) setErr(String(r.error))\n await refresh(connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function disconnect() {\n setBusy(true); setErr('')\n try { await callApi('disconnect', { connectionId }); await refresh(connectionId) }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function clearHistory() {\n try {\n if (window.confirm('确定清空当前连接的 SSH 操作记录吗?')) {\n await callApi('clear', connectionId ? { connectionId } : undefined)\n await refresh(connectionId)\n }\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function togglePinned(id) {\n setContextMenu(null)\n try {\n const r = await callApi('pin', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function deleteRecord(id) {\n const session = sessions.find((s) => s.connectionId === id)\n setContextMenu(null)\n if (!session || !window.confirm('删除该连接记录吗?在线连接不会断开。')) return\n try {\n const r = await callApi('delete', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n const onKey = (e) => { if (e.key === 'Enter' && canRun && !busy) run() }\n const status = connected ? ('● ' + target) : (selectedSession ? (selectedSession.exited ? '○ 已退出' : '○ 未连接') : '○ 未选择连接')\n\n return createElement('div', { className: 'dshssh-body', onClick: () => setContextMenu(null) },\n createElement('div', { className: 'dshssh-head' },\n createElement('span', { className: 'dshssh-title' }, 'SSH 远程终端'),\n createElement('span', { className: 'dshssh-status ' + (connected ? 'ok' : 'off') }, status),\n createElement('button', { className: 'dshssh-btn dshssh-clear', onClick: clearHistory }, '清空当前'),\n ),\n createElement('div', { className: 'dshssh-layout' },\n createElement('div', { className: 'dshssh-sessions' },\n createElement('div', { className: 'dshssh-session-title' }, '连接列表 (' + sessions.length + ')'),\n createElement('div', { className: 'dshssh-session-list' },\n sessions.length\n ? sessions.map((s) => createElement('button', {\n key: s.connectionId,\n className: 'dshssh-session' + (s.connectionId === connectionId ? ' selected' : ''),\n onClick: () => { setConnectionId(s.connectionId); setErr('') },\n onContextMenu: (e) => {\n e.preventDefault()\n e.stopPropagation()\n const x = Math.min(e.clientX, Math.max(4, window.innerWidth - 160))\n const y = Math.min(e.clientY, Math.max(4, window.innerHeight - 80))\n setContextMenu({ connectionId: s.connectionId, x, y })\n },\n },\n createElement('span', { className: 'dshssh-session-dot' + (s.connected ? ' ok' : '') }),\n createElement('span', { className: 'dshssh-session-copy' },\n createElement('span', { className: 'dshssh-session-name' },\n s.pinned ? createElement('span', { className: 'dshssh-pin', title: '已置顶' }, '★') : null,\n s.label || s.target || 'SSH 连接',\n ),\n createElement('span', { className: 'dshssh-session-meta' }, s.connected ? (s.busy ? '执行中' : '已连接') : (s.historical ? '历史记录' : '已断开')),\n )))\n : createElement('div', { className: 'dshssh-empty' }, '暂无连接'),\n ),\n ),\n createElement('div', { className: 'dshssh-main' },\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input', placeholder: '主机', value: hostV, onChange: (e) => setHostV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', placeholder: '用户', value: userV, onChange: (e) => setUserV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 54 }, placeholder: '端口', value: portV, onChange: (e) => setPortV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 96 }, type: 'password', placeholder: '密码(可选)', value: passV, onChange: (e) => setPassV(e.target.value) }),\n createElement('button', { className: 'dshssh-btn', disabled: busy, onClick: connect }, '新建连接'),\n ),\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input dshssh-cmdline', style: { flex: 1 }, placeholder: '输入命令后回车或点执行', value: cmdV, onChange: (e) => setCmdV(e.target.value), onKeyDown: onKey }),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: run }, '执行'),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: disconnect }, '断开'),\n ),\n createElement('div', { className: 'dshssh-log', ref: (el) => setLogEl(el) },\n steps.length\n ? steps.map((s, i) => createElement('div', { key: i, className: 'dshssh-step dshssh-' + s.kind }, (fmt(s.ts) + ' ' + s.text)))\n : createElement('div', { className: 'dshssh-step dshssh-system' }, '还没有操作。'),\n ),\n err ? createElement('div', { className: 'dshssh-err' }, String(err)) : null,\n ),\n contextMenu && contextSession\n ? createElement('div', {\n className: 'dshssh-context-menu',\n style: { left: contextMenu.x, top: contextMenu.y },\n onClick: (e) => e.stopPropagation(),\n },\n createElement('button', { type: 'button', onClick: () => togglePinned(contextSession.connectionId) }, contextSession.pinned ? '取消置顶' : '置顶'),\n createElement('button', { type: 'button', className: 'danger', onClick: () => deleteRecord(contextSession.connectionId) }, '删除该连接记录'),\n )\n : null,\n ),\n )\n}\n\nexport const inject = ['timer', 'betterSidebar']\n\nexport function apply(ctx) {\n const bs = ctx.get('betterSidebar')\n if (!bs) return\n ctx.effect(() => bs.registerTab({\n id: 'ssh-remote:terminal',\n title: 'SSH 终端',\n order: 60,\n single: true,\n component: (p) => createElement(SshBody, { ctx, visible: p.visible }),\n }))\n}\n"],"mappings":";;;;;;;;EAEA,MAAM,MAAM;EAEZ,SAAS,IAAI,IAAI;GACf,MAAM,IAAI,IAAI,KAAK,EAAE;GACrB,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;GAC1C,OAAO,EAAE,EAAE,SAAS,CAAC,IAAI,MAAM,EAAE,EAAE,WAAW,CAAC,IAAI,MAAM,EAAE,EAAE,WAAW,CAAC;EAC3E;EAEA,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4ChB,eAAe,QAAQ,QAAQ,QAAQ,KAAA,GAAW;GAMhD,OAAO,OAAM,MALK,MAAM,KAAK;IAC3B,QAAQ;IACR,SAAS,EAAE,gBAAgB,mBAAmB;IAC9C,MAAM,KAAK,UAAU;KAAE;KAAQ,GAAI,SAAS,CAAC;IAAG,CAAC;GACnD,CAAC,EAAA,CACgB,KAAK;EACxB;EAEA,SAAS,YAAY;GACnB,IAAI,OAAO,aAAa,aAAa;GACrC,IAAI,SAAS,cAAc,6CAA2C,GAAG;GACzE,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,SAAS;GACrB,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc;GAClB,SAAS,KAAK,YAAY,GAAG;EAC/B;EAEA,SAAS,QAAQ,OAAO;GACtB,MAAM,MAAM,MAAM;GAClB,MAAM,UAAU,MAAM,YAAY,KAAA,IAAY,OAAO,MAAM;GAC3D,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,CAAS,EAAE;GACjC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,EAAE;GACrC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,EAAE;GACrC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,IAAI;GACvC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,EAAE;GACrC,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,EAAE;GACnC,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAAS,CAAC,CAAC;GAC3C,MAAM,CAAC,cAAc,oBAAA,GAAmBA,MAAAA,SAAAA,CAAS,EAAE;GACnD,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAS,KAAK;GAChD,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAAS,EAAE;GACvC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,CAAC,CAAC;GACrC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,IAAI;GACvC,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAS,IAAI;GACnD,MAAM,cAAA,GAAaC,MAAAA,OAAAA,CAAO,CAAC;GAE3B,MAAM,kBAAkB,SAAS,MAAM,MAAM,EAAE,iBAAiB,YAAY;GAC5E,MAAM,iBAAiB,eAAe,SAAS,MAAM,MAAM,EAAE,iBAAiB,YAAY,YAAY;GACtG,MAAM,SAAS,aAAa,CAAC,CAAC,mBAAmB,CAAC,CAAC,gBAAgB;GAEnE,eAAe,QAAQ,aAAa;IAClC,MAAM,MAAM,EAAE,WAAW;IACzB,IAAI;KACF,MAAM,OAAO,MAAM,QAAQ,MAAM;KACjC,IAAI,QAAQ,WAAW,SAAS;KAChC,MAAM,eAAe,MAAM,QAAQ,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;KAC7E,YAAY,YAAY;KACxB,MAAM,YAAY,eAAe;KACjC,MAAM,WAAW,aAAa,MAAM,MAAM,EAAE,iBAAiB,SAAS,IAClE,YACC,QAAQ,KAAK,sBAAsB,aAAa,MAAM,MAAM,EAAE,iBAAiB,KAAK,kBAAkB,IACrG,KAAK,sBACH,aAAa,MAAM,MAAM,EAAE,SAAS,KAAK,aAAa,MAAM,CAAC,EAAA,CAAG,gBAAgB;KACxF,IAAI,aAAa,cAAc,gBAAgB,QAAQ;KACvD,IAAI,CAAC,UAAU;MACb,IAAI,QAAQ,WAAW,SAAS;MAChC,aAAa,KAAK;MAClB,UAAU,EAAE;MACZ,SAAS,CAAC,CAAC;MACX;KACF;KACA,MAAM,IAAI,MAAM,QAAQ,UAAU,EAAE,cAAc,SAAS,CAAC;KAC5D,IAAI,QAAQ,WAAW,SAAS;KAChC,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU;KACjC,UAAW,KAAK,EAAE,UAAW,EAAE;KAC/B,SAAU,KAAK,EAAE,SAAU,CAAC,CAAC;IAC/B,SAAS,GAAG,CAAC;GACf;GAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,UAAU;IACV,IAAI,YAAY,OAAO;KAAE,WAAW,WAAW;KAAG;IAAO;IACzD,QAAQ,YAAY;IACpB,MAAM,OAAO,IAAI,eAAe,QAAQ,YAAY,GAAG,GAAG;IAC1D,aAAa;KACX,WAAW,WAAW;KACtB,IAAI;MAAE,KAAK;KAAE,SAAS,GAAG,CAAC;IAC5B;GACF,GAAG,CAAC,SAAS,YAAY,CAAC;GAE1B,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,KAAK;IACX,IAAI,CAAC,IAAI;IAET,IADmB,GAAG,YAAY,GAAG,gBAAgB,GAAG,eAAe,IACvD,GAAG,YAAY,GAAG;GACpC,GAAG,CAAC,OAAO,KAAK,CAAC;GAEjB,eAAe,UAAU;IACvB,QAAQ,IAAI;IAAG,OAAO,EAAE;IACxB,IAAI;KACF,MAAM,IAAI,MAAM,QAAQ,WAAW;MAAE,MAAM;MAAO,MAAM;MAAO,MAAM,OAAO,KAAK,KAAK;MAAI,UAAU,SAAS,KAAA;KAAU,CAAC;KACxH,IAAI,KAAK,EAAE,OAAO,OAAO,OAAO,EAAE,KAAK,CAAC;KACxC,MAAM,QAAQ,KAAK,EAAE,YAAY;IACnC,SACO,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE,UAAU;KAAE,QAAQ,KAAK;IAAE;GAC/E;GACA,eAAe,MAAM;IACnB,QAAQ,IAAI;IAAG,OAAO,EAAE;IACxB,IAAI;KACF,MAAM,IAAI,MAAM,QAAQ,QAAQ;MAAE;MAAc,SAAS;MAAM,QAAQ;KAAM,CAAC;KAC9E,IAAI,KAAK,EAAE,OAAO,OAAO,OAAO,EAAE,KAAK,CAAC;KACxC,MAAM,QAAQ,YAAY;IAC5B,SACO,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE,UAAU;KAAE,QAAQ,KAAK;IAAE;GAC/E;GACA,eAAe,aAAa;IAC1B,QAAQ,IAAI;IAAG,OAAO,EAAE;IACxB,IAAI;KAAE,MAAM,QAAQ,cAAc,EAAE,aAAa,CAAC;KAAG,MAAM,QAAQ,YAAY;IAAE,SAC1E,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE,UAAU;KAAE,QAAQ,KAAK;IAAE;GAC/E;GACA,eAAe,eAAe;IAC5B,IAAI;KACF,IAAI,OAAO,QAAQ,sBAAsB,GAAG;MAC1C,MAAM,QAAQ,SAAS,eAAe,EAAE,aAAa,IAAI,KAAA,CAAS;MAClE,MAAM,QAAQ,YAAY;KAC5B;IACF,SAAS,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE;GACtD;GAEA,eAAe,aAAa,IAAI;IAC9B,eAAe,IAAI;IACnB,IAAI;KACF,MAAM,IAAI,MAAM,QAAQ,OAAO,EAAE,cAAc,GAAG,CAAC;KACnD,IAAI,KAAK,EAAE,OAAO,OAAO,OAAO,EAAE,KAAK,CAAC;KACxC,MAAM,QAAQ,EAAE;IAClB,SAAS,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE;GACtD;GAEA,eAAe,aAAa,IAAI;IAC9B,MAAM,UAAU,SAAS,MAAM,MAAM,EAAE,iBAAiB,EAAE;IAC1D,eAAe,IAAI;IACnB,IAAI,CAAC,WAAW,CAAC,OAAO,QAAQ,oBAAoB,GAAG;IACvD,IAAI;KACF,MAAM,IAAI,MAAM,QAAQ,UAAU,EAAE,cAAc,GAAG,CAAC;KACtD,IAAI,KAAK,EAAE,OAAO,OAAO,OAAO,EAAE,KAAK,CAAC;KACxC,MAAM,QAAQ,EAAE;IAClB,SAAS,GAAG;KAAE,OAAO,OAAQ,KAAK,EAAE,WAAY,CAAC,CAAC;IAAE;GACtD;GAEA,MAAM,SAAS,MAAM;IAAE,IAAI,EAAE,QAAQ,WAAW,UAAU,CAAC,MAAM,IAAI;GAAE;GACvE,MAAM,SAAS,YAAa,OAAO,SAAW,kBAAmB,gBAAgB,SAAS,UAAU,UAAW;GAE/G,QAAA,GAAOC,MAAAA,cAAAA,CAAc,OAAO;IAAE,WAAW;IAAe,eAAe,eAAe,IAAI;GAAE,IAAA,GAC1FA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,cAAc,IAAA,GAC9CA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,eAAe,GAAG,UAAU,IAAA,GAC/DA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,oBAAoB,YAAY,OAAO,OAAO,GAAG,MAAM,IAAA,GAC1FA,MAAAA,cAAAA,CAAc,UAAU;IAAE,WAAW;IAA2B,SAAS;GAAa,GAAG,MAAM,CACjG,IAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,gBAAgB,IAAA,GAChDA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,kBAAkB,IAAA,GAClDA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,uBAAuB,GAAG,WAAW,SAAS,SAAS,GAAG,IAAA,GAC5FA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,sBAAsB,GACtD,SAAS,SACL,SAAS,KAAK,OAAA,GAAMA,MAAAA,cAAAA,CAAc,UAAU;IAC5C,KAAK,EAAE;IACP,WAAW,oBAAoB,EAAE,iBAAiB,eAAe,cAAc;IAC/E,eAAe;KAAE,gBAAgB,EAAE,YAAY;KAAG,OAAO,EAAE;IAAE;IAC7D,gBAAgB,MAAM;KACpB,EAAE,eAAe;KACjB,EAAE,gBAAgB;KAClB,MAAM,IAAI,KAAK,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,aAAa,GAAG,CAAC;KAClE,MAAM,IAAI,KAAK,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,cAAc,EAAE,CAAC;KAClE,eAAe;MAAE,cAAc,EAAE;MAAc;MAAG;KAAE,CAAC;IACvD;GACF,IAAA,GACAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,wBAAwB,EAAE,YAAY,QAAQ,IAAI,CAAC,IAAA,GACtFA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,sBAAsB,IAAA,GACvDA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,sBAAsB,GACvD,EAAE,UAAA,GAASA,MAAAA,cAAAA,CAAc,QAAQ;IAAE,WAAW;IAAc,OAAO;GAAM,GAAG,GAAG,IAAI,MACnF,EAAE,SAAS,EAAE,UAAU,QACzB,IAAA,GACAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,sBAAsB,GAAG,EAAE,YAAa,EAAE,OAAO,QAAQ,QAAU,EAAE,aAAa,SAAS,KAAM,CACtI,CAAC,CAAC,KAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,eAAe,GAAG,MAAM,CAChE,CACF,IAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,cAAc,IAAA,GAC9CA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,cAAc,IAAA,GAC9CA,MAAAA,cAAAA,CAAc,SAAS;IAAE,WAAW;IAAgB,aAAa;IAAM,OAAO;IAAO,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;GAAE,CAAC,IAAA,GAChIA,MAAAA,cAAAA,CAAc,SAAS;IAAE,WAAW;IAAgB,aAAa;IAAM,OAAO;IAAO,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;GAAE,CAAC,IAAA,GAChIA,MAAAA,cAAAA,CAAc,SAAS;IAAE,WAAW;IAAgB,OAAO,EAAE,OAAO,GAAG;IAAG,aAAa;IAAM,OAAO;IAAO,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;GAAE,CAAC,IAAA,GACtJA,MAAAA,cAAAA,CAAc,SAAS;IAAE,WAAW;IAAgB,OAAO,EAAE,OAAO,GAAG;IAAG,MAAM;IAAY,aAAa;IAAU,OAAO;IAAO,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;GAAE,CAAC,IAAA,GAC5KA,MAAAA,cAAAA,CAAc,UAAU;IAAE,WAAW;IAAc,UAAU;IAAM,SAAS;GAAQ,GAAG,MAAM,CAC/F,IAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,cAAc,IAAA,GAC9CA,MAAAA,cAAAA,CAAc,SAAS;IAAE,WAAW;IAA+B,OAAO,EAAE,MAAM,EAAE;IAAG,aAAa;IAAe,OAAO;IAAM,WAAW,MAAM,QAAQ,EAAE,OAAO,KAAK;IAAG,WAAW;GAAM,CAAC,IAAA,GAC5LA,MAAAA,cAAAA,CAAc,UAAU;IAAE,WAAW;IAAc,UAAU,QAAQ,CAAC;IAAQ,SAAS;GAAI,GAAG,IAAI,IAAA,GAClGA,MAAAA,cAAAA,CAAc,UAAU;IAAE,WAAW;IAAc,UAAU,QAAQ,CAAC;IAAQ,SAAS;GAAW,GAAG,IAAI,CAC3G,IAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO;IAAE,WAAW;IAAc,MAAM,OAAO,SAAS,EAAE;GAAE,GACxE,MAAM,SACF,MAAM,KAAK,GAAG,OAAA,GAAMA,MAAAA,cAAAA,CAAc,OAAO;IAAE,KAAK;IAAG,WAAW,wBAAwB,EAAE;GAAK,GAAI,IAAI,EAAE,EAAE,IAAI,OAAO,EAAE,IAAK,CAAC,KAAA,GAC5HA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,4BAA4B,GAAG,QAAQ,CAC/E,GACA,OAAA,GAAMA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,aAAa,GAAG,OAAO,GAAG,CAAC,IAAI,IACzE,GACA,eAAe,kBAAA,GACXA,MAAAA,cAAAA,CAAc,OAAO;IACrB,WAAW;IACX,OAAO;KAAE,MAAM,YAAY;KAAG,KAAK,YAAY;IAAE;IACjD,UAAU,MAAM,EAAE,gBAAgB;GACpC,IAAA,GACAA,MAAAA,cAAAA,CAAc,UAAU;IAAE,MAAM;IAAU,eAAe,aAAa,eAAe,YAAY;GAAE,GAAG,eAAe,SAAS,SAAS,IAAI,IAAA,GAC3IA,MAAAA,cAAAA,CAAc,UAAU;IAAE,MAAM;IAAU,WAAW;IAAU,eAAe,aAAa,eAAe,YAAY;GAAE,GAAG,SAAS,CACpI,IACE,IACN,CACF;EACF;EAEA,MAAa,SAAS,CAAC,SAAS,eAAe;EAE/C,SAAgB,MAAM,KAAK;GACzB,MAAM,KAAK,IAAI,IAAI,eAAe;GAClC,IAAI,CAAC,IAAI;GACT,IAAI,aAAa,GAAG,YAAY;IAC9B,IAAI;IACJ,OAAO;IACP,OAAO;IACP,QAAQ;IACR,YAAY,OAAA,GAAMA,MAAAA,cAAAA,CAAc,SAAS;KAAE;KAAK,SAAS,EAAE;IAAQ,CAAC;GACtE,CAAC,CAAC;EACJ"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState","useRef","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["import { createElement, useState, useEffect, useRef } from 'react'\n\nconst API = '/ssh-terminal/api'\n\nfunction fmt(ts) {\n const d = new Date(ts)\n const p = (n) => String(n).padStart(2, '0')\n return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())\n}\n\nconst SSH_CSS = `\n.dshssh-body{position:relative;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}\n.dshssh-head{display:flex;align-items:center;gap:8px;}\n.dshssh-title{font-weight:700;flex:1;}\n.dshssh-status{font-weight:600;}\n.dshssh-status.ok{color:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-status.off{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-layout{display:flex;gap:8px;flex:1;min-height:0;}\n.dshssh-sessions{width:150px;min-width:125px;display:flex;flex-direction:column;gap:6px;min-height:0;}\n.dshssh-session-title{font-size:11px;font-weight:700;color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-session-list{display:flex;flex-direction:column;gap:5px;overflow:auto;min-height:0;}\n.dshssh-session{display:flex;align-items:center;gap:6px;width:100%;padding:6px 7px;text-align:left;border-radius:7px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2,rgba(148,163,184,.08));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:11px;}\n.dshssh-session:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-session.selected{border-color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-session-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-session-dot.ok{background:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;}\n.dshssh-session-name,.dshssh-session-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.dshssh-session-name{font-weight:600;}\n.dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}\n.dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}\n.dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}\n.dshssh-context-menu{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}\n.dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}\n.dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-main{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}\n.dshssh-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}\n.dshssh-input{padding:4px 7px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-bg-layer-3,rgba(2,6,23,.5));color:var(--dsw-alias-label-primary,inherit);font-size:12px;min-width:70px;}\n.dshssh-input::placeholder{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-btn{padding:4px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:12px;}\n.dshssh-btn:hover{background:var(--dsw-alias-interactive-bg-active,rgba(148,163,184,.28));}\n.dshssh-btn:disabled{opacity:.5;cursor:default;}\n.dshssh-log{flex:1;min-height:120px;overflow:auto;background:var(--dsw-alias-bg-layer-1,rgba(2,6,23,.55));border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.25));border-radius:8px;padding:8px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;}\n.dshssh-step{margin:0 0 6px;}\n.dshssh-cmd{color:var(--dsw-alias-accent,#93c5fd);}\n.dshssh-cmd::before{content:\"❯ \";color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-out{color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-connect{color:var(--dsw-alias-state-success-primary,#86efac);}\n.dshssh-disconnect{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-system{color:var(--dsw-alias-label-tertiary,#94a3b8);font-style:italic;}\n.dshssh-err{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n`\n\nasync function callApi(action, extra = undefined) {\n const res = await fetch(API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action, ...(extra || {}) }),\n })\n return await res.json()\n}\n\nfunction injectCss() {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"dsh-ssh-terminal\"]')) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-ssh-terminal'\n tag.dataset.pluginCss = 'dsh-ssh-terminal'\n tag.textContent = SSH_CSS\n document.head.appendChild(tag)\n}\n\nfunction SshBody(props) {\n const ctx = props.ctx\n const visible = props.visible === undefined ? true : props.visible\n const [busy, setBusy] = useState(false)\n const [err, setErr] = useState('')\n const [hostV, setHostV] = useState('')\n const [userV, setUserV] = useState('')\n const [portV, setPortV] = useState('22')\n const [passV, setPassV] = useState('')\n const [cmdV, setCmdV] = useState('')\n const [sessions, setSessions] = useState([])\n const [connectionId, setConnectionId] = useState('')\n const [connected, setConnected] = useState(false)\n const [target, setTarget] = useState('')\n const [steps, setSteps] = useState([])\n const logRef = useRef(null)\n const [contextMenu, setContextMenu] = useState(null)\n const refreshSeq = useRef(0)\n const statusRevRef = useRef({})\n\n const selectedSession = sessions.find((s) => s.connectionId === connectionId)\n const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId)\n const canRun = connected && !!selectedSession && !!selectedSession.connected\n\n async function refresh(preferredId) {\n const seq = ++refreshSeq.current\n try {\n const list = await callApi('list')\n if (seq !== refreshSeq.current) return\n const nextSessions = Array.isArray(list && list.sessions) ? list.sessions : []\n setSessions(nextSessions)\n const preferred = preferredId || connectionId\n const selected = nextSessions.some((s) => s.connectionId === preferred)\n ? preferred\n : (list && list.activeConnectionId && nextSessions.some((s) => s.connectionId === list.activeConnectionId)\n ? list.activeConnectionId\n : ((nextSessions.find((s) => s.connected) || nextSessions[0] || {}).connectionId || ''))\n if (selected !== connectionId) setConnectionId(selected)\n if (!selected) {\n if (seq !== refreshSeq.current) return\n setConnected(false)\n setTarget('')\n setSteps([])\n return\n }\n // Rev-gated status: the host answers with a tiny unchanged marker\n // unless new steps appeared, so idle polling stays cheap.\n const echoed = statusRevRef.current[selected] || 0\n const t = await callApi('status', { connectionId: selected, rev: echoed })\n if (seq !== refreshSeq.current) return\n if (t && t.unchanged) return\n if (t && typeof t.rev === 'number') statusRevRef.current[selected] = t.rev\n setConnected(!!(t && t.connected))\n setTarget((t && t.target) || '')\n setSteps((t && t.steps) || [])\n } catch (e) {}\n }\n\n useEffect(() => {\n injectCss()\n if (visible === false) { refreshSeq.current += 1; return }\n refresh(connectionId)\n const stop = ctx.interval(() => refresh(connectionId), 1500)\n return () => {\n refreshSeq.current += 1\n try { stop() } catch (e) {}\n }\n }, [visible, connectionId])\n\n useEffect(() => {\n const el = logRef.current\n if (!el) return\n const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40\n if (nearBottom) el.scrollTop = el.scrollHeight\n }, [steps])\n\n async function connect() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('connect', { host: hostV, user: userV, port: Number(portV) || 22, password: passV || undefined })\n if (r && r.error) setErr(String(r.error))\n await refresh(r && r.connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function run() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('exec', { connectionId, command: cmdV, waitMs: 15000 })\n if (r && r.error) setErr(String(r.error))\n await refresh(connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function disconnect() {\n setBusy(true); setErr('')\n try { await callApi('disconnect', { connectionId }); await refresh(connectionId) }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function clearHistory() {\n try {\n if (window.confirm('确定清空当前连接的 SSH 操作记录吗?')) {\n await callApi('clear', connectionId ? { connectionId } : undefined)\n await refresh(connectionId)\n }\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function togglePinned(id) {\n setContextMenu(null)\n try {\n const r = await callApi('pin', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function deleteRecord(id) {\n const session = sessions.find((s) => s.connectionId === id)\n setContextMenu(null)\n if (!session || !window.confirm('删除该连接记录吗?在线连接不会断开。')) return\n try {\n const r = await callApi('delete', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n const onKey = (e) => { if (e.key === 'Enter' && canRun && !busy) run() }\n const status = connected ? ('● ' + target) : (selectedSession ? (selectedSession.exited ? '○ 已退出' : '○ 未连接') : '○ 未选择连接')\n\n return createElement('div', { className: 'dshssh-body', onClick: () => setContextMenu(null) },\n createElement('div', { className: 'dshssh-head' },\n createElement('span', { className: 'dshssh-title' }, 'SSH 远程终端'),\n createElement('span', { className: 'dshssh-status ' + (connected ? 'ok' : 'off') }, status),\n createElement('button', { className: 'dshssh-btn dshssh-clear', onClick: clearHistory }, '清空当前'),\n ),\n createElement('div', { className: 'dshssh-layout' },\n createElement('div', { className: 'dshssh-sessions' },\n createElement('div', { className: 'dshssh-session-title' }, '连接列表 (' + sessions.length + ')'),\n createElement('div', { className: 'dshssh-session-list' },\n sessions.length\n ? sessions.map((s) => createElement('button', {\n key: s.connectionId,\n className: 'dshssh-session' + (s.connectionId === connectionId ? ' selected' : ''),\n onClick: () => { setConnectionId(s.connectionId); setErr('') },\n onContextMenu: (e) => {\n e.preventDefault()\n e.stopPropagation()\n const body = e.currentTarget.closest('.dshssh-body')\n const rect = body ? body.getBoundingClientRect() : { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }\n const x = Math.max(4, Math.min(e.clientX - rect.left, rect.width - 160))\n const y = Math.max(4, Math.min(e.clientY - rect.top, rect.height - 80))\n setContextMenu({ connectionId: s.connectionId, x, y })\n },\n },\n createElement('span', { className: 'dshssh-session-dot' + (s.connected ? ' ok' : '') }),\n createElement('span', { className: 'dshssh-session-copy' },\n createElement('span', { className: 'dshssh-session-name' },\n s.pinned ? createElement('span', { className: 'dshssh-pin', title: '已置顶' }, '★') : null,\n s.label || s.target || 'SSH 连接',\n ),\n createElement('span', { className: 'dshssh-session-meta' }, s.connected ? (s.busy ? '执行中' : '已连接') : (s.historical ? '历史记录' : '已断开')),\n )))\n : createElement('div', { className: 'dshssh-empty' }, '暂无连接'),\n ),\n ),\n createElement('div', { className: 'dshssh-main' },\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input', placeholder: '主机', value: hostV, onChange: (e) => setHostV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', placeholder: '用户', value: userV, onChange: (e) => setUserV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 54 }, placeholder: '端口', value: portV, onChange: (e) => setPortV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 96 }, type: 'password', placeholder: '密码(可选)', value: passV, onChange: (e) => setPassV(e.target.value) }),\n createElement('button', { className: 'dshssh-btn', disabled: busy, onClick: connect }, '新建连接'),\n ),\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input dshssh-cmdline', style: { flex: 1 }, placeholder: '输入命令后回车或点执行', value: cmdV, onChange: (e) => setCmdV(e.target.value), onKeyDown: onKey }),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: run }, '执行'),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: disconnect }, '断开'),\n ),\n createElement('div', { className: 'dshssh-log', ref: logRef },\n steps.length\n ? steps.map((s, i) => createElement('div', { key: i, className: 'dshssh-step dshssh-' + s.kind }, (fmt(s.ts) + ' ' + s.text)))\n : createElement('div', { className: 'dshssh-step dshssh-system' }, '还没有操作。'),\n ),\n err ? createElement('div', { className: 'dshssh-err' }, String(err)) : null,\n ),\n contextMenu && contextSession\n ? createElement('div', {\n className: 'dshssh-context-menu',\n style: { left: contextMenu.x, top: contextMenu.y },\n onClick: (e) => e.stopPropagation(),\n },\n createElement('button', { type: 'button', onClick: () => togglePinned(contextSession.connectionId) }, contextSession.pinned ? '取消置顶' : '置顶'),\n createElement('button', { type: 'button', className: 'danger', onClick: () => deleteRecord(contextSession.connectionId) }, '删除该连接记录'),\n )\n : null,\n ),\n )\n}\n\nexport const inject = ['timer', 'betterSidebar']\n\nexport function apply(ctx) {\n const bs = ctx.get('betterSidebar')\n if (!bs) return\n ctx.effect(() => bs.registerTab({\n id: 'ssh-remote:terminal',\n title: 'SSH 终端',\n order: 60,\n single: true,\n component: (p) => createElement(SshBody, { ctx, visible: p.visible }),\n }))\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -16,8 +16,16 @@ export function apply(ctx) {
|
|
|
16
16
|
const HISTORY_PATH = join(homedir(), '.dsh', 'ssh-terminal-history.json')
|
|
17
17
|
const MAX_HISTORY = 1200
|
|
18
18
|
const MAX_STEPS = 600
|
|
19
|
+
// One event's text cap: a single huge command output must not poison the
|
|
20
|
+
// history file, persist() and every status poll with megabytes of JSON.
|
|
21
|
+
const MAX_TEXT = 4000
|
|
22
|
+
// Interactive terminal log ring: trim s.log once it grows past the keep
|
|
23
|
+
// size plus a buffer, so prompt detection keeps working.
|
|
24
|
+
const LOG_KEEP = 400000
|
|
19
25
|
const LEGACY_SESSION_ID = 'legacy'
|
|
26
|
+
const clipText = (t) => (t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '\n…[truncated]' : t)
|
|
20
27
|
let history = []
|
|
28
|
+
let loadedDirty = false
|
|
21
29
|
const pinnedConnectionIds = new Set()
|
|
22
30
|
try {
|
|
23
31
|
const raw = readFileSync(HISTORY_PATH, 'utf8')
|
|
@@ -29,26 +37,46 @@ export function apply(ctx) {
|
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
if (Array.isArray(entries)) {
|
|
32
|
-
history = entries.map((entry) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
text
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
history = entries.map((entry) => {
|
|
41
|
+
const rawText = entry && entry.text != null ? String(entry.text) : ''
|
|
42
|
+
const text = clipText(rawText)
|
|
43
|
+
if (text.length !== rawText.length) loadedDirty = true
|
|
44
|
+
return {
|
|
45
|
+
connectionId: entry && entry.connectionId ? String(entry.connectionId) : LEGACY_SESSION_ID,
|
|
46
|
+
kind: entry && entry.kind ? String(entry.kind) : 'system',
|
|
47
|
+
text,
|
|
48
|
+
ts: entry && Number(entry.ts) ? Number(entry.ts) : Date.now(),
|
|
49
|
+
...(entry && entry.target ? { target: String(entry.target) } : {}),
|
|
50
|
+
...(entry && entry.label ? { label: String(entry.label) } : {}),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
40
53
|
}
|
|
41
54
|
} catch (e) {}
|
|
42
55
|
if (history.length > MAX_HISTORY) history = history.slice(-MAX_HISTORY)
|
|
43
56
|
|
|
44
57
|
let activeSessionId = null
|
|
45
58
|
let lastTarget = ''
|
|
59
|
+
let persistScheduled = false
|
|
60
|
+
let lastPersistAt = 0
|
|
46
61
|
function persist() {
|
|
47
62
|
try {
|
|
48
63
|
mkdirSync(dirname(HISTORY_PATH), { recursive: true })
|
|
49
64
|
writeFileSync(HISTORY_PATH, JSON.stringify({ version: 2, events: history, pinnedConnectionIds: Array.from(pinnedConnectionIds) }))
|
|
50
65
|
} catch (e) {}
|
|
51
66
|
}
|
|
67
|
+
// persist() stringifies the whole history; appendStep fires per output
|
|
68
|
+
// chunk, so coalesce writes to at most one per second and flush on stop.
|
|
69
|
+
function schedulePersist() {
|
|
70
|
+
if (persistScheduled) return
|
|
71
|
+
persistScheduled = true
|
|
72
|
+
void (async () => {
|
|
73
|
+
await ctx.timeout(Math.max(0, 1000 - (Date.now() - lastPersistAt)))
|
|
74
|
+
persistScheduled = false
|
|
75
|
+
lastPersistAt = Date.now()
|
|
76
|
+
persist()
|
|
77
|
+
})()
|
|
78
|
+
}
|
|
79
|
+
if (loadedDirty) schedulePersist()
|
|
52
80
|
|
|
53
81
|
function ensureMeta(connectionId) {
|
|
54
82
|
let meta = connectionMeta.get(connectionId)
|
|
@@ -80,7 +108,7 @@ export function apply(ctx) {
|
|
|
80
108
|
const event = {
|
|
81
109
|
connectionId: connectionId || LEGACY_SESSION_ID,
|
|
82
110
|
kind,
|
|
83
|
-
text: String(text),
|
|
111
|
+
text: clipText(String(text)),
|
|
84
112
|
ts: Date.now(),
|
|
85
113
|
...(extra && extra.target ? { target: String(extra.target) } : {}),
|
|
86
114
|
...(extra && extra.label ? { label: String(extra.label) } : {}),
|
|
@@ -94,7 +122,8 @@ export function apply(ctx) {
|
|
|
94
122
|
meta.lastActivityAt = event.ts
|
|
95
123
|
if (kind === 'connect') meta.connected = true
|
|
96
124
|
if (kind === 'disconnect') meta.connected = false
|
|
97
|
-
|
|
125
|
+
bumpRev()
|
|
126
|
+
schedulePersist()
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
function startDrain(s) {
|
|
@@ -106,6 +135,9 @@ export function apply(ctx) {
|
|
|
106
135
|
const text = cleanStream(decoder.decode(chunk, { stream: true }))
|
|
107
136
|
if (!text) continue
|
|
108
137
|
s.log += text
|
|
138
|
+
// Ring-trim the interactive log so a chatty remote cannot grow
|
|
139
|
+
// host memory without bound; keep the tail for prompt detection.
|
|
140
|
+
if (s.log.length > LOG_KEEP + 100000) s.log = s.log.slice(-LOG_KEEP)
|
|
109
141
|
s.lastActivityAt = Date.now()
|
|
110
142
|
tryPassword(s)
|
|
111
143
|
}
|
|
@@ -329,6 +361,22 @@ export function apply(ctx) {
|
|
|
329
361
|
return { ok: true, connectionId, disconnected: true, target: s.target }
|
|
330
362
|
}
|
|
331
363
|
|
|
364
|
+
// Cheap change detection for status polling: every history or session
|
|
365
|
+
// state mutation bumps a global rev; pollers echo back the rev they last
|
|
366
|
+
// saw and get a tiny "same" response instead of the full transcript.
|
|
367
|
+
let historyRev = 1
|
|
368
|
+
function bumpRev() { historyRev++ }
|
|
369
|
+
let countsAtRev = 0
|
|
370
|
+
const stepCounts = new Map()
|
|
371
|
+
function stepCountOf(connectionId) {
|
|
372
|
+
if (countsAtRev !== historyRev) {
|
|
373
|
+
stepCounts.clear()
|
|
374
|
+
for (const event of history) stepCounts.set(event.connectionId, (stepCounts.get(event.connectionId) || 0) + 1)
|
|
375
|
+
countsAtRev = historyRev
|
|
376
|
+
}
|
|
377
|
+
return stepCounts.get(connectionId) || 0
|
|
378
|
+
}
|
|
379
|
+
|
|
332
380
|
function sessionSummary(s) {
|
|
333
381
|
const meta = ensureMeta(s.connectionId)
|
|
334
382
|
return {
|
|
@@ -341,7 +389,7 @@ export function apply(ctx) {
|
|
|
341
389
|
historical: false,
|
|
342
390
|
pinned: !!meta.pinned,
|
|
343
391
|
lastActivityAt: s.lastActivityAt,
|
|
344
|
-
stepCount:
|
|
392
|
+
stepCount: stepCountOf(s.connectionId),
|
|
345
393
|
}
|
|
346
394
|
}
|
|
347
395
|
|
|
@@ -360,31 +408,39 @@ export function apply(ctx) {
|
|
|
360
408
|
historical: true,
|
|
361
409
|
pinned: !!meta.pinned,
|
|
362
410
|
lastActivityAt: meta.lastActivityAt,
|
|
363
|
-
stepCount:
|
|
411
|
+
stepCount: stepCountOf(meta.connectionId),
|
|
364
412
|
})
|
|
365
413
|
}
|
|
366
414
|
const sessionsList = Array.from(listed.values()).sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.lastActivityAt - a.lastActivityAt)
|
|
367
415
|
return { sessions: sessionsList, activeConnectionId: activeSessionId && sessions.has(activeSessionId) ? activeSessionId : null }
|
|
368
416
|
}
|
|
369
417
|
|
|
370
|
-
function transcriptFor(connectionId) {
|
|
418
|
+
function transcriptFor(connectionId, clientRev, sameOk) {
|
|
371
419
|
const s = sessions.get(connectionId)
|
|
372
420
|
const meta = connectionMeta.get(connectionId)
|
|
373
|
-
|
|
421
|
+
const base = {
|
|
374
422
|
connectionId,
|
|
375
423
|
label: s ? s.label : (meta && meta.label) || (connectionId === LEGACY_SESSION_ID ? '历史记录' : ''),
|
|
376
424
|
connected: !!(s && s.connected && !s.exited),
|
|
377
425
|
exited: !!(!s || s.exited),
|
|
378
426
|
busy: !!(s && s.busy),
|
|
379
427
|
target: s ? s.target : (meta && meta.target) || (connectionId === LEGACY_SESSION_ID ? '历史记录' : lastTarget),
|
|
380
|
-
|
|
428
|
+
rev: historyRev,
|
|
381
429
|
}
|
|
430
|
+
// Echoed rev matches → steps unchanged: skip the history scan entirely.
|
|
431
|
+
if (sameOk && clientRev === historyRev) return { ...base, unchanged: true }
|
|
432
|
+
const steps = []
|
|
433
|
+
for (let i = history.length - 1; i >= 0 && steps.length < MAX_STEPS; i--) {
|
|
434
|
+
if (history[i].connectionId === connectionId) steps.push(history[i])
|
|
435
|
+
}
|
|
436
|
+
steps.reverse()
|
|
437
|
+
return { ...base, steps }
|
|
382
438
|
}
|
|
383
439
|
|
|
384
|
-
function transcript(requestedId) {
|
|
440
|
+
function transcript(requestedId, clientRev) {
|
|
385
441
|
const connectionId = resolveConnectionId(requestedId, true)
|
|
386
|
-
if (connectionId) return transcriptFor(connectionId)
|
|
387
|
-
return { connectionId: null, connected: false, target: lastTarget, steps: history.slice(-MAX_STEPS) }
|
|
442
|
+
if (connectionId) return transcriptFor(connectionId, clientRev, true)
|
|
443
|
+
return { connectionId: null, connected: false, target: lastTarget, rev: historyRev, steps: history.slice(-MAX_STEPS) }
|
|
388
444
|
}
|
|
389
445
|
|
|
390
446
|
function clearHistory(requestedId) {
|
|
@@ -400,6 +456,7 @@ export function apply(ctx) {
|
|
|
400
456
|
connectionMeta.clear()
|
|
401
457
|
pinnedConnectionIds.clear()
|
|
402
458
|
}
|
|
459
|
+
bumpRev()
|
|
403
460
|
persist()
|
|
404
461
|
return { ok: true, cleared: true, connectionId }
|
|
405
462
|
}
|
|
@@ -424,6 +481,7 @@ export function apply(ctx) {
|
|
|
424
481
|
pinnedConnectionIds.delete(connectionId)
|
|
425
482
|
if (!sessions.has(connectionId)) connectionMeta.delete(connectionId)
|
|
426
483
|
else ensureMeta(connectionId).pinned = false
|
|
484
|
+
bumpRev()
|
|
427
485
|
persist()
|
|
428
486
|
return { ok: true, deleted: true, connectionId }
|
|
429
487
|
}
|
|
@@ -523,7 +581,7 @@ export function apply(ctx) {
|
|
|
523
581
|
else if (action === 'connect') out = await doConnect(data)
|
|
524
582
|
else if (action === 'exec') out = await doExec(data.command, data)
|
|
525
583
|
else if (action === 'disconnect') out = await doDisconnect(data.connectionId)
|
|
526
|
-
else out = transcript(data.connectionId)
|
|
584
|
+
else out = transcript(data.connectionId, data.rev)
|
|
527
585
|
send(res, out)
|
|
528
586
|
} catch (e) {
|
|
529
587
|
send(res, { ok: false, error: ((e && e.message) || String(e)) })
|
|
@@ -533,5 +591,6 @@ export function apply(ctx) {
|
|
|
533
591
|
|
|
534
592
|
ctx.effect(() => () => {
|
|
535
593
|
for (const connectionId of Array.from(sessions.keys())) void doDisconnect(connectionId)
|
|
594
|
+
persist()
|
|
536
595
|
})
|
|
537
596
|
}
|