dsh-ssh-terminal 0.1.0 → 0.1.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/lib/client.js CHANGED
@@ -31,8 +31,13 @@ window.__ModuleLoader__.load({
31
31
  .dshssh-session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;}
32
32
  .dshssh-session-name,.dshssh-session-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
33
33
  .dshssh-session-name{font-weight:600;}
34
+ .dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}
34
35
  .dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}
35
36
  .dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}
37
+ .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);}
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
+ .dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}
40
+ .dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}
36
41
  .dshssh-main{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}
37
42
  .dshssh-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
38
43
  .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;}
@@ -85,8 +90,10 @@ window.__ModuleLoader__.load({
85
90
  const [target, setTarget] = (0, react.useState)("");
86
91
  const [steps, setSteps] = (0, react.useState)([]);
87
92
  const [logEl, setLogEl] = (0, react.useState)(null);
93
+ const [contextMenu, setContextMenu] = (0, react.useState)(null);
88
94
  const refreshSeq = (0, react.useRef)(0);
89
95
  const selectedSession = sessions.find((s) => s.connectionId === connectionId);
96
+ const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId);
90
97
  const canRun = connected && !!selectedSession && !!selectedSession.connected;
91
98
  async function refresh(preferredId) {
92
99
  const seq = ++refreshSeq.current;
@@ -189,11 +196,36 @@ window.__ModuleLoader__.load({
189
196
  setErr(String(e && e.message || e));
190
197
  }
191
198
  }
199
+ async function togglePinned(id) {
200
+ setContextMenu(null);
201
+ try {
202
+ const r = await callApi("pin", { connectionId: id });
203
+ if (r && r.error) setErr(String(r.error));
204
+ await refresh(id);
205
+ } catch (e) {
206
+ setErr(String(e && e.message || e));
207
+ }
208
+ }
209
+ async function deleteRecord(id) {
210
+ const session = sessions.find((s) => s.connectionId === id);
211
+ setContextMenu(null);
212
+ if (!session || !window.confirm("删除该连接记录吗?在线连接不会断开。")) return;
213
+ try {
214
+ const r = await callApi("delete", { connectionId: id });
215
+ if (r && r.error) setErr(String(r.error));
216
+ await refresh(id);
217
+ } catch (e) {
218
+ setErr(String(e && e.message || e));
219
+ }
220
+ }
192
221
  const onKey = (e) => {
193
222
  if (e.key === "Enter" && canRun && !busy) run();
194
223
  };
195
224
  const status = connected ? "● " + target : selectedSession ? selectedSession.exited ? "○ 已退出" : "○ 未连接" : "○ 未选择连接";
196
- return (0, react.createElement)("div", { className: "dshssh-body" }, (0, react.createElement)("div", { className: "dshssh-head" }, (0, react.createElement)("span", { className: "dshssh-title" }, "SSH 远程终端"), (0, react.createElement)("span", { className: "dshssh-status " + (connected ? "ok" : "off") }, status), (0, react.createElement)("button", {
225
+ return (0, react.createElement)("div", {
226
+ className: "dshssh-body",
227
+ onClick: () => setContextMenu(null)
228
+ }, (0, react.createElement)("div", { className: "dshssh-head" }, (0, react.createElement)("span", { className: "dshssh-title" }, "SSH 远程终端"), (0, react.createElement)("span", { className: "dshssh-status " + (connected ? "ok" : "off") }, status), (0, react.createElement)("button", {
197
229
  className: "dshssh-btn dshssh-clear",
198
230
  onClick: clearHistory
199
231
  }, "清空当前")), (0, react.createElement)("div", { className: "dshssh-layout" }, (0, react.createElement)("div", { className: "dshssh-sessions" }, (0, react.createElement)("div", { className: "dshssh-session-title" }, "连接列表 (" + sessions.length + ")"), (0, react.createElement)("div", { className: "dshssh-session-list" }, sessions.length ? sessions.map((s) => (0, react.createElement)("button", {
@@ -202,8 +234,22 @@ window.__ModuleLoader__.load({
202
234
  onClick: () => {
203
235
  setConnectionId(s.connectionId);
204
236
  setErr("");
237
+ },
238
+ onContextMenu: (e) => {
239
+ e.preventDefault();
240
+ e.stopPropagation();
241
+ const x = Math.min(e.clientX, Math.max(4, window.innerWidth - 160));
242
+ const y = Math.min(e.clientY, Math.max(4, window.innerHeight - 80));
243
+ setContextMenu({
244
+ connectionId: s.connectionId,
245
+ x,
246
+ y
247
+ });
205
248
  }
206
- }, (0, react.createElement)("span", { className: "dshssh-session-dot" + (s.connected ? " ok" : "") }), (0, react.createElement)("span", { className: "dshssh-session-copy" }, (0, react.createElement)("span", { className: "dshssh-session-name" }, s.label || s.target || "SSH 连接"), (0, react.createElement)("span", { className: "dshssh-session-meta" }, s.connected ? s.busy ? "执行中" : "已连接" : s.historical ? "历史记录" : "已断开")))) : (0, react.createElement)("div", { className: "dshssh-empty" }, "暂无连接"))), (0, react.createElement)("div", { className: "dshssh-main" }, (0, react.createElement)("div", { className: "dshssh-form" }, (0, react.createElement)("input", {
249
+ }, (0, react.createElement)("span", { className: "dshssh-session-dot" + (s.connected ? " ok" : "") }), (0, react.createElement)("span", { className: "dshssh-session-copy" }, (0, react.createElement)("span", { className: "dshssh-session-name" }, s.pinned ? (0, react.createElement)("span", {
250
+ className: "dshssh-pin",
251
+ title: "已置顶"
252
+ }, "★") : null, s.label || s.target || "SSH 连接"), (0, react.createElement)("span", { className: "dshssh-session-meta" }, s.connected ? s.busy ? "执行中" : "已连接" : s.historical ? "历史记录" : "已断开")))) : (0, react.createElement)("div", { className: "dshssh-empty" }, "暂无连接"))), (0, react.createElement)("div", { className: "dshssh-main" }, (0, react.createElement)("div", { className: "dshssh-form" }, (0, react.createElement)("input", {
207
253
  className: "dshssh-input",
208
254
  placeholder: "主机",
209
255
  value: hostV,
@@ -251,7 +297,21 @@ window.__ModuleLoader__.load({
251
297
  }, steps.length ? steps.map((s, i) => (0, react.createElement)("div", {
252
298
  key: i,
253
299
  className: "dshssh-step dshssh-" + s.kind
254
- }, fmt(s.ts) + " " + s.text)) : (0, react.createElement)("div", { className: "dshssh-step dshssh-system" }, "还没有操作。")), err ? (0, react.createElement)("div", { className: "dshssh-err" }, String(err)) : null)));
300
+ }, fmt(s.ts) + " " + s.text)) : (0, react.createElement)("div", { className: "dshssh-step dshssh-system" }, "还没有操作。")), err ? (0, react.createElement)("div", { className: "dshssh-err" }, String(err)) : null), contextMenu && contextSession ? (0, react.createElement)("div", {
301
+ className: "dshssh-context-menu",
302
+ style: {
303
+ left: contextMenu.x,
304
+ top: contextMenu.y
305
+ },
306
+ onClick: (e) => e.stopPropagation()
307
+ }, (0, react.createElement)("button", {
308
+ type: "button",
309
+ onClick: () => togglePinned(contextSession.connectionId)
310
+ }, contextSession.pinned ? "取消置顶" : "置顶"), (0, react.createElement)("button", {
311
+ type: "button",
312
+ className: "danger",
313
+ onClick: () => deleteRecord(contextSession.connectionId)
314
+ }, "删除该连接记录")) : null));
255
315
  }
256
316
  const inject = ["timer", "betterSidebar"];
257
317
  function apply(ctx) {
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-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-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 refreshSeq = useRef(0)\n\n const selectedSession = sessions.find((s) => s.connectionId === 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 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' },\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 },\n createElement('span', { className: 'dshssh-session-dot' + (s.connected ? ' ok' : '') }),\n createElement('span', { className: 'dshssh-session-copy' },\n createElement('span', { className: 'dshssh-session-name' }, s.label || s.target || 'SSH 连接'),\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 ),\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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"}
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":"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"}
package/lib/index.js CHANGED
@@ -18,11 +18,18 @@ export function apply(ctx) {
18
18
  const MAX_STEPS = 600
19
19
  const LEGACY_SESSION_ID = 'legacy'
20
20
  let history = []
21
+ const pinnedConnectionIds = new Set()
21
22
  try {
22
23
  const raw = readFileSync(HISTORY_PATH, 'utf8')
23
24
  const parsed = JSON.parse(raw)
24
- if (Array.isArray(parsed)) {
25
- history = parsed.map((entry) => ({
25
+ const entries = Array.isArray(parsed) ? parsed : parsed && parsed.events
26
+ if (!Array.isArray(parsed) && parsed && Array.isArray(parsed.pinnedConnectionIds)) {
27
+ for (const connectionId of parsed.pinnedConnectionIds) {
28
+ if (connectionId) pinnedConnectionIds.add(String(connectionId))
29
+ }
30
+ }
31
+ if (Array.isArray(entries)) {
32
+ history = entries.map((entry) => ({
26
33
  connectionId: entry && entry.connectionId ? String(entry.connectionId) : LEGACY_SESSION_ID,
27
34
  kind: entry && entry.kind ? String(entry.kind) : 'system',
28
35
  text: entry && entry.text != null ? String(entry.text) : '',
@@ -39,14 +46,14 @@ export function apply(ctx) {
39
46
  function persist() {
40
47
  try {
41
48
  mkdirSync(dirname(HISTORY_PATH), { recursive: true })
42
- writeFileSync(HISTORY_PATH, JSON.stringify(history))
49
+ writeFileSync(HISTORY_PATH, JSON.stringify({ version: 2, events: history, pinnedConnectionIds: Array.from(pinnedConnectionIds) }))
43
50
  } catch (e) {}
44
51
  }
45
52
 
46
53
  function ensureMeta(connectionId) {
47
54
  let meta = connectionMeta.get(connectionId)
48
55
  if (!meta) {
49
- meta = { connectionId, target: '', label: '', connected: false, lastActivityAt: 0 }
56
+ meta = { connectionId, target: '', label: '', connected: false, pinned: pinnedConnectionIds.has(connectionId), lastActivityAt: 0 }
50
57
  connectionMeta.set(connectionId, meta)
51
58
  }
52
59
  return meta
@@ -323,6 +330,7 @@ export function apply(ctx) {
323
330
  }
324
331
 
325
332
  function sessionSummary(s) {
333
+ const meta = ensureMeta(s.connectionId)
326
334
  return {
327
335
  connectionId: s.connectionId,
328
336
  label: s.label,
@@ -331,6 +339,7 @@ export function apply(ctx) {
331
339
  exited: !!s.exited,
332
340
  busy: !!s.busy,
333
341
  historical: false,
342
+ pinned: !!meta.pinned,
334
343
  lastActivityAt: s.lastActivityAt,
335
344
  stepCount: history.reduce((count, event) => count + (event.connectionId === s.connectionId ? 1 : 0), 0),
336
345
  }
@@ -349,11 +358,12 @@ export function apply(ctx) {
349
358
  exited: true,
350
359
  busy: false,
351
360
  historical: true,
361
+ pinned: !!meta.pinned,
352
362
  lastActivityAt: meta.lastActivityAt,
353
363
  stepCount: history.reduce((count, event) => count + (event.connectionId === meta.connectionId ? 1 : 0), 0),
354
364
  })
355
365
  }
356
- const sessionsList = Array.from(listed.values()).sort((a, b) => b.lastActivityAt - a.lastActivityAt)
366
+ const sessionsList = Array.from(listed.values()).sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.lastActivityAt - a.lastActivityAt)
357
367
  return { sessions: sessionsList, activeConnectionId: activeSessionId && sessions.has(activeSessionId) ? activeSessionId : null }
358
368
  }
359
369
 
@@ -381,15 +391,43 @@ export function apply(ctx) {
381
391
  const connectionId = requestedId == null || requestedId === '' ? null : String(requestedId)
382
392
  if (connectionId) {
383
393
  history = history.filter((event) => event.connectionId !== connectionId)
384
- if (!sessions.has(connectionId)) connectionMeta.delete(connectionId)
394
+ if (!sessions.has(connectionId)) {
395
+ connectionMeta.delete(connectionId)
396
+ pinnedConnectionIds.delete(connectionId)
397
+ }
385
398
  } else {
386
399
  history = []
387
400
  connectionMeta.clear()
401
+ pinnedConnectionIds.clear()
388
402
  }
389
403
  persist()
390
404
  return { ok: true, cleared: true, connectionId }
391
405
  }
392
406
 
407
+ function togglePinned(requestedId) {
408
+ const connectionId = resolveConnectionId(requestedId, true)
409
+ if (!connectionId) throw new Error('connectionId is required')
410
+ const meta = ensureMeta(connectionId)
411
+ meta.pinned = !meta.pinned
412
+ if (meta.pinned) pinnedConnectionIds.add(connectionId)
413
+ else pinnedConnectionIds.delete(connectionId)
414
+ persist()
415
+ return { ok: true, connectionId, pinned: meta.pinned }
416
+ }
417
+
418
+ function deleteHistory(requestedId) {
419
+ const connectionId = requestedId == null || requestedId === '' ? null : String(requestedId)
420
+ if (!connectionId || (!sessions.has(connectionId) && !connectionMeta.has(connectionId))) {
421
+ throw new Error('unknown SSH connection: ' + (connectionId || ''))
422
+ }
423
+ history = history.filter((event) => event.connectionId !== connectionId)
424
+ pinnedConnectionIds.delete(connectionId)
425
+ if (!sessions.has(connectionId)) connectionMeta.delete(connectionId)
426
+ else ensureMeta(connectionId).pinned = false
427
+ persist()
428
+ return { ok: true, deleted: true, connectionId }
429
+ }
430
+
393
431
  function textOut(a, v) {
394
432
  let s = (v && typeof v === 'object') ? ((v.output !== undefined ? String(v.output) : JSON.stringify(v))) : String(v)
395
433
  if (s.length > 6000) s = s.slice(0, 6000) + '\n...[truncated]'
@@ -479,6 +517,8 @@ export function apply(ctx) {
479
517
  const action = data && data.action
480
518
  let out
481
519
  if (action === 'clear') out = clearHistory(data.connectionId)
520
+ else if (action === 'pin') out = togglePinned(data.connectionId)
521
+ else if (action === 'delete') out = deleteHistory(data.connectionId)
482
522
  else if (action === 'list') out = listSessions()
483
523
  else if (action === 'connect') out = await doConnect(data)
484
524
  else if (action === 'exec') out = await doExec(data.command, data)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-terminal",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SSH 远程终端:在 dsh-better-sidebar 侧边栏里连接远程主机,逐步查看命令与输出。",
5
5
  "author": "sunzhyang1616-ui",
6
6
  "license": "MIT",