termdock 1.4.68 → 1.4.70
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/.desktop-dist/main.js +254 -49
- package/README.md +13 -0
- package/dist/client/assets/{ModelPreview-BgnWlhMf.js → ModelPreview-Cs14IQuQ.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-C3RDwPoE.js → abnfDiagram-VRR7QNED-DuDfdv-6.js} +1 -1
- package/dist/client/assets/{arc-n-PrqSp_.js → arc-CSYzGV_2.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D-l8Q2rF.js → architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-D5b5kvhB.js → blockDiagram-677ZJIJ3-IaQMzmYv.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-CsvG6hAc.js → c4Diagram-LMCZKHZV-BwyuIPRL.js} +1 -1
- package/dist/client/assets/channel-BotxtiNE.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-CcTcAD2X.js → chunk-2Q5K7J3B-BtHQYLBT.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-c--MtlS9.js → chunk-32BRIVSS-CXrK07sW.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-BCdLlLPD.js → chunk-5VM5RSS4-BfBshn11.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BXYeFL7Q.js → chunk-EX3LRPZG-BJbilNaf.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-Cakio9E1.js → chunk-JWPE2WC7-wgcmSRlL.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-CR9m_lTv.js → chunk-MOJQB5TN-8VZVUeDI.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-DvPE2m9c.js → chunk-RYQCIY6F-B900YBFa.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BdHIv-k_.js → chunk-V7JOEXUC-BK4_RLz-.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-CNYv3_OD.js → chunk-VR4S4FIN-Kj5_ycjB.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-Ddhpk5bE.js → chunk-XXDRQBXY-BolvPLRz.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-vc2IL5Qo.js → cose-bilkent-JH36ORCC-9_9_SLqv.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-Cr5U2Llm.js → cynefin-VYW2F7L2-yEpoi3vG.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DaSVhxBB.js → cynefinDiagram-TSTJHNR4-Dy-gvup_.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-B8Ao-OKf.js → dagre-VKFMJZFB-CNjJ5pRK.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-a-sl4C9b.js → diagram-FQU43EPY-uZ73Rj3T.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-CmezqIfj.js → diagram-G47NLZAW-Cxt7TYZT.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-rM4l3wBG.js → diagram-NH7WQ7WH-B4TKinuZ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DS3rHIQ7.js → diagram-OA4YK3LP-CGSuenHU.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-BUyvO5qF.js → diagram-WEI45ONY-DqqzDaUZ.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-C6KPNqQ0.js → ebnfDiagram-CCIWWBDH-B2SgXL5w.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-u9pQXlID.js → erDiagram-Q63AITRT-D-ibkr8y.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-h9QkExto.js → flowDiagram-23GEKE2U-BPeyMqr-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-C9AXkH3Y.js → ganttDiagram-NO4QXBWP-DcEqEzzu.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CXYfDg8b.js → gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js} +1 -1
- package/dist/client/assets/index-DhfIiBLW.css +1 -0
- package/dist/client/assets/index-ivnGnbVg.js +451 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-B_Kb0Chj.js → infoDiagram-FWYZ7A6U-BHrzVXeX.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-B2kT_9-T.js → ishikawaDiagram-FXEZZL3T-CwuoQJku.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-DdOISfLz.js → journeyDiagram-5HDEW3XC-ZEcy0rDZ.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-CdIIjjXO.js → kanban-definition-HUTT4EX6-BDBawNbr.js} +1 -1
- package/dist/client/assets/{linear-CWpzI-ns.js → linear-smYQWzDV.js} +1 -1
- package/dist/client/assets/{mermaid.core-BLHPinUj.js → mermaid.core-eJs7Ouvw.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-qFnTyqQA.js → mindmap-definition-LN4V7U3C-BK1IZ9zg.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-Bu7ijeZf.js → pegDiagram-2B236MQR-B3-mVNgX.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-BygAm24V.js → pieDiagram-ENE6RG2P-Bkeeyx5l.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-3-9rkPqr.js → quadrantDiagram-ABIIQ3AL-DouIklEO.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-BAsvFjIm.js → railroadDiagram-RFXS5EU6-BB_jXQd2.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-H-BySumI.js → requirementDiagram-TGXJPOKE-u56IHfiR.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-D2PDv2gA.js → sankeyDiagram-HTMAVEWB-eDi2cs5E.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-C9pvFRUG.js → sequenceDiagram-DBY2YBRQ-DnAA2xYa.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Cx0TrKvi.js → sizeCapture-X5ZJPWSS-BogPui79.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-Ciytmucj.js → stateDiagram-2N3HPSRC--EeXDy2M.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-Dwk151xG.js → swimlanes-5IMT3BWC-DZAw7-kY.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-Bym0jj2o.js → timeline-definition-FHXFAJF6-DkcnpB-Y.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-CToPJelW.js → vennDiagram-L72KCM5P-BStcDGEf.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-BZ6V3CA9.js → wardleyDiagram-EHGQE667-Cj4CQn1M.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-D5NzFpxi.js → xychartDiagram-FW5EYKEG-_uBCjai8.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +118 -0
- package/dist/server/agent/installers.js +11 -10
- package/dist/server/agent/plugins.js +90 -4
- package/dist/server/agent/registry.js +1 -0
- package/dist/server/agent/session.js +18 -2
- package/dist/server/agent/titleNamerCatalog.js +203 -0
- package/dist/server/agentHook.js +2 -1
- package/dist/server/cli.js +255 -30
- package/dist/server/routes/filesystem.js +11 -8
- package/dist/server/routes/terminal.js +351 -10
- package/dist/server/utils/autoUpdate.js +175 -0
- package/dist/server/utils/npmUpdate.js +139 -0
- package/dist/server/utils/settings.js +44 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-DqH6WWOA.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DbsEzwLQ.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DbsEzwLQ.js +0 -1
- package/dist/client/assets/index-ChhyERKX.css +0 -1
- package/dist/client/assets/index-DVo7u2vf.js +0 -442
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-C9GGeb7o.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-BRRLZYN8.js +0 -8
package/.desktop-dist/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, BrowserWindow, dialog, ipcMain, Menu, net, Notification, shell, } from 'electron';
|
|
1
|
+
import { app, BrowserWindow, dialog, ipcMain, Menu, net, Notification, session, shell, } from 'electron';
|
|
2
2
|
import { execFile, spawn } from 'node:child_process';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import fs from 'node:fs';
|
|
@@ -19,6 +19,7 @@ const DEFAULT_LOCAL_URL = 'http://localhost:9834';
|
|
|
19
19
|
const PROTOCOL_VERSION = 1;
|
|
20
20
|
const HEALTH_TIMEOUT_MS = 3_500;
|
|
21
21
|
const START_TIMEOUT_MS = 90_000;
|
|
22
|
+
const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
|
|
22
23
|
/** Delivered-but-unseen desktop notifications, mirrored into the Dock badge. */
|
|
23
24
|
const activeNotifications = new Map();
|
|
24
25
|
let unreadNotificationCount = 0;
|
|
@@ -27,11 +28,29 @@ function clearUnreadNotifications() {
|
|
|
27
28
|
app.setBadgeCount(0);
|
|
28
29
|
}
|
|
29
30
|
let mainWindow = null;
|
|
30
|
-
|
|
31
|
+
const serviceWindows = new Map();
|
|
32
|
+
const windowServiceOrigins = new WeakMap();
|
|
31
33
|
let isQuitting = false;
|
|
34
|
+
function focusedWorkspaceWindow() {
|
|
35
|
+
const focused = BrowserWindow.getFocusedWindow();
|
|
36
|
+
if (focused && windowServiceOrigins.has(focused))
|
|
37
|
+
return focused;
|
|
38
|
+
return [...serviceWindows.values()].find((window) => !window.isDestroyed()) ?? null;
|
|
39
|
+
}
|
|
40
|
+
function serviceLabel(url) {
|
|
41
|
+
const normalized = normalizeServiceUrl(url);
|
|
42
|
+
const saved = readDesktopConfig().connections.find((entry) => entry.url === normalized);
|
|
43
|
+
if (saved?.label.trim())
|
|
44
|
+
return saved.label.trim();
|
|
45
|
+
const parsed = new URL(normalized);
|
|
46
|
+
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
|
|
47
|
+
? '本机'
|
|
48
|
+
: parsed.host;
|
|
49
|
+
}
|
|
32
50
|
function showDesktopMessageBox(options) {
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const parent = BrowserWindow.getFocusedWindow() ?? mainWindow;
|
|
52
|
+
return parent
|
|
53
|
+
? dialog.showMessageBox(parent, options)
|
|
35
54
|
: dialog.showMessageBox(options);
|
|
36
55
|
}
|
|
37
56
|
function openExternalLink(url) {
|
|
@@ -39,6 +58,34 @@ function openExternalLink(url) {
|
|
|
39
58
|
return;
|
|
40
59
|
void shell.openExternal(url);
|
|
41
60
|
}
|
|
61
|
+
function configureLocalServiceCertificateTrust() {
|
|
62
|
+
let localFingerprint = null;
|
|
63
|
+
try {
|
|
64
|
+
localFingerprint = new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// The certificate is created when the local service first enables HTTPS.
|
|
68
|
+
}
|
|
69
|
+
if (!localFingerprint)
|
|
70
|
+
return;
|
|
71
|
+
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
72
|
+
let isLocalTarget = false;
|
|
73
|
+
try {
|
|
74
|
+
isLocalTarget = isLocalNetworkTarget(new URL(`https://${request.hostname}`));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Keep Chromium's default verification for malformed hostnames.
|
|
78
|
+
}
|
|
79
|
+
let presentedFingerprint = null;
|
|
80
|
+
try {
|
|
81
|
+
presentedFingerprint = new crypto.X509Certificate(request.certificate.data).fingerprint256;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Keep Chromium's default verification if the certificate cannot be parsed.
|
|
85
|
+
}
|
|
86
|
+
callback(isLocalTarget && presentedFingerprint === localFingerprint ? 0 : -3);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
42
89
|
function defaultConfig() {
|
|
43
90
|
return { version: 1, connections: [], lastConnectionUrl: null };
|
|
44
91
|
}
|
|
@@ -221,6 +268,26 @@ async function probeService(rawUrl) {
|
|
|
221
268
|
return { ok: false, url, error: message };
|
|
222
269
|
}
|
|
223
270
|
}
|
|
271
|
+
async function probeServiceWithLocalNetworkPermission(rawUrl) {
|
|
272
|
+
let probe = await probeService(rawUrl);
|
|
273
|
+
if (probe.ok)
|
|
274
|
+
return probe;
|
|
275
|
+
let parsedProbeUrl;
|
|
276
|
+
try {
|
|
277
|
+
parsedProbeUrl = new URL(probe.url);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return probe;
|
|
281
|
+
}
|
|
282
|
+
if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
|
|
283
|
+
return probe;
|
|
284
|
+
}
|
|
285
|
+
if (!(await requestLocalNetworkPermissionRetry(probe.url))) {
|
|
286
|
+
return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
|
|
287
|
+
}
|
|
288
|
+
probe = await probeService(rawUrl);
|
|
289
|
+
return probe;
|
|
290
|
+
}
|
|
224
291
|
async function getLocalServiceStatus() {
|
|
225
292
|
const state = readServerState();
|
|
226
293
|
if (!state || !isProcessRunning(state.pid)) {
|
|
@@ -336,6 +403,8 @@ function desktopRuntimeEnv(runtime = runtimePaths()) {
|
|
|
336
403
|
const currentPath = process.env.PATH ?? '/usr/bin:/bin:/usr/sbin:/sbin';
|
|
337
404
|
return {
|
|
338
405
|
...process.env,
|
|
406
|
+
LANG: process.env.LANG || 'en_US.UTF-8',
|
|
407
|
+
LC_CTYPE: process.env.LC_CTYPE || process.env.LANG || 'en_US.UTF-8',
|
|
339
408
|
PATH: [runtime.toolchainBin, '/opt/homebrew/bin', '/usr/local/bin', currentPath]
|
|
340
409
|
.filter(Boolean)
|
|
341
410
|
.join(path.delimiter),
|
|
@@ -364,6 +433,24 @@ async function waitForLocalService(childPid) {
|
|
|
364
433
|
}
|
|
365
434
|
return lastProbe;
|
|
366
435
|
}
|
|
436
|
+
async function ensureTmuxUtf8Environment(runtime) {
|
|
437
|
+
const tmux = path.join(runtime.toolchainBin, 'tmux');
|
|
438
|
+
if (!fs.existsSync(tmux))
|
|
439
|
+
return;
|
|
440
|
+
const locale = desktopRuntimeEnv(runtime).LANG || 'en_US.UTF-8';
|
|
441
|
+
for (const name of ['LANG', 'LC_CTYPE']) {
|
|
442
|
+
try {
|
|
443
|
+
await execFileAsync(tmux, ['set-environment', '-g', name, locale], {
|
|
444
|
+
timeout: 5_000,
|
|
445
|
+
maxBuffer: 128 * 1024,
|
|
446
|
+
env: desktopRuntimeEnv(runtime),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// tmux is optional and may not have a running server yet.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
367
454
|
async function confirmAndStopExisting(status) {
|
|
368
455
|
if (!status.state)
|
|
369
456
|
return true;
|
|
@@ -460,6 +547,7 @@ async function startLocalService() {
|
|
|
460
547
|
if (!probe.ok) {
|
|
461
548
|
throw new Error(`桌面版服务启动失败:${probe.error ?? '未知错误'}。日志:${path.join(termdockDir, 'server.log')}`);
|
|
462
549
|
}
|
|
550
|
+
await ensureTmuxUtf8Environment(runtime);
|
|
463
551
|
await connectWindow(probe.url);
|
|
464
552
|
return probe;
|
|
465
553
|
}
|
|
@@ -504,51 +592,41 @@ async function installCli() {
|
|
|
504
592
|
return snapshot();
|
|
505
593
|
}
|
|
506
594
|
async function connectWindow(rawUrl) {
|
|
507
|
-
let probe = await
|
|
508
|
-
if (!probe.ok) {
|
|
509
|
-
let parsedProbeUrl = null;
|
|
510
|
-
try {
|
|
511
|
-
parsedProbeUrl = new URL(probe.url);
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
// The probe already contains the normalized URL validation error.
|
|
515
|
-
}
|
|
516
|
-
if (parsedProbeUrl
|
|
517
|
-
&& isLocalNetworkTarget(parsedProbeUrl)
|
|
518
|
-
&& looksLikeLocalNetworkPermissionError(probe.error)) {
|
|
519
|
-
const shouldRetry = await requestLocalNetworkPermissionRetry(probe.url);
|
|
520
|
-
if (shouldRetry) {
|
|
521
|
-
probe = await probeService(rawUrl);
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
595
|
+
let probe = await probeServiceWithLocalNetworkPermission(rawUrl);
|
|
528
596
|
if (!probe.ok)
|
|
529
597
|
return probe;
|
|
530
598
|
const parsed = new URL(probe.url);
|
|
531
|
-
|
|
532
|
-
|
|
599
|
+
const key = parsed.origin;
|
|
600
|
+
const existingWindow = serviceWindows.get(key);
|
|
601
|
+
if (existingWindow && !existingWindow.isDestroyed()) {
|
|
602
|
+
existingWindow.show();
|
|
603
|
+
existingWindow.focus();
|
|
604
|
+
return probe;
|
|
605
|
+
}
|
|
606
|
+
const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
|
|
607
|
+
serviceWindows.set(key, workspaceWindow);
|
|
608
|
+
while (!workspaceWindow.isDestroyed()) {
|
|
533
609
|
try {
|
|
534
|
-
await
|
|
610
|
+
await workspaceWindow.loadURL(probe.url);
|
|
535
611
|
const config = readDesktopConfig();
|
|
536
612
|
config.lastConnectionUrl = probe.url;
|
|
537
613
|
const existing = config.connections.find((entry) => entry.url === probe.url);
|
|
538
614
|
if (existing)
|
|
539
615
|
existing.lastConnectedAt = Date.now();
|
|
540
616
|
writeDesktopConfig(config);
|
|
617
|
+
workspaceWindow.show();
|
|
618
|
+
workspaceWindow.focus();
|
|
541
619
|
return probe;
|
|
542
620
|
}
|
|
543
621
|
catch (error) {
|
|
544
|
-
activeServiceOrigin = null;
|
|
545
|
-
await showConnectionCenter();
|
|
546
622
|
const message = networkErrorDetails(error);
|
|
547
623
|
if (isLocalNetworkTarget(parsed) && looksLikeLocalNetworkPermissionError(error)) {
|
|
548
624
|
if (await requestLocalNetworkPermissionRetry(probe.url))
|
|
549
625
|
continue;
|
|
626
|
+
workspaceWindow.destroy();
|
|
550
627
|
return { ...probe, ok: false, error: 'Termdock 尚未获得 macOS 本地网络权限' };
|
|
551
628
|
}
|
|
629
|
+
workspaceWindow.destroy();
|
|
552
630
|
await showDesktopMessageBox({
|
|
553
631
|
type: 'error',
|
|
554
632
|
title: '连接 Termdock 服务失败',
|
|
@@ -561,7 +639,9 @@ async function connectWindow(rawUrl) {
|
|
|
561
639
|
return { ...probe, ok: false, error: 'Termdock 窗口已关闭' };
|
|
562
640
|
}
|
|
563
641
|
async function showConnectionCenter() {
|
|
564
|
-
|
|
642
|
+
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
643
|
+
mainWindow = createDesktopWindow();
|
|
644
|
+
}
|
|
565
645
|
const rendererPath = app.isPackaged
|
|
566
646
|
? path.join(process.resourcesPath, 'renderer', 'index.html')
|
|
567
647
|
: path.join(projectRoot, 'desktop', 'renderer', 'index.html');
|
|
@@ -569,10 +649,14 @@ async function showConnectionCenter() {
|
|
|
569
649
|
mainWindow?.show();
|
|
570
650
|
mainWindow?.focus();
|
|
571
651
|
}
|
|
572
|
-
function
|
|
652
|
+
function createDesktopWindow(options) {
|
|
573
653
|
const window = new BrowserWindow({
|
|
574
|
-
title: 'Termdock',
|
|
575
|
-
|
|
654
|
+
title: options ? `Termdock — ${options.label}` : 'Termdock — 连接中心',
|
|
655
|
+
show: false,
|
|
656
|
+
// The web workspace enables its persistent right inspector at 1440px.
|
|
657
|
+
// Open service windows above that breakpoint so the visible pin action
|
|
658
|
+
// cannot write a pinned state that the initial layout refuses to render.
|
|
659
|
+
width: options ? 1500 : 1280,
|
|
576
660
|
height: 820,
|
|
577
661
|
minWidth: 760,
|
|
578
662
|
minHeight: 520,
|
|
@@ -586,6 +670,14 @@ function createMainWindow() {
|
|
|
586
670
|
sandbox: true,
|
|
587
671
|
},
|
|
588
672
|
});
|
|
673
|
+
if (options)
|
|
674
|
+
windowServiceOrigins.set(window, options.serviceOrigin);
|
|
675
|
+
if (options) {
|
|
676
|
+
window.webContents.on('page-title-updated', (event) => {
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
window.setTitle(`Termdock — ${options.label}`);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
589
681
|
window.webContents.setWindowOpenHandler(({ url }) => {
|
|
590
682
|
if (isExternalLinkStagingUrl(url)) {
|
|
591
683
|
return {
|
|
@@ -614,10 +706,11 @@ function createMainWindow() {
|
|
|
614
706
|
});
|
|
615
707
|
});
|
|
616
708
|
window.webContents.on('will-navigate', (event, url) => {
|
|
617
|
-
|
|
709
|
+
const serviceOrigin = windowServiceOrigins.get(window);
|
|
710
|
+
if (!serviceOrigin)
|
|
618
711
|
return;
|
|
619
712
|
try {
|
|
620
|
-
if (new URL(url).origin ===
|
|
713
|
+
if (new URL(url).origin === serviceOrigin)
|
|
621
714
|
return;
|
|
622
715
|
}
|
|
623
716
|
catch {
|
|
@@ -627,24 +720,83 @@ function createMainWindow() {
|
|
|
627
720
|
openExternalLink(url);
|
|
628
721
|
});
|
|
629
722
|
window.webContents.on('did-finish-load', () => {
|
|
630
|
-
if (!
|
|
723
|
+
if (!windowServiceOrigins.has(window))
|
|
631
724
|
return;
|
|
632
725
|
void window.webContents.insertCSS(`
|
|
633
726
|
html[data-termdock-desktop='true'] {
|
|
634
727
|
--safe-top-inset: max(env(safe-area-inset-top, 0px), 38px) !important;
|
|
635
728
|
}
|
|
729
|
+
html[data-termdock-desktop='true'] #root {
|
|
730
|
+
padding-top: 0 !important;
|
|
731
|
+
}
|
|
732
|
+
html[data-termdock-desktop='true'] body::after {
|
|
733
|
+
display: none !important;
|
|
734
|
+
}
|
|
735
|
+
html[data-termdock-desktop='true'] #root main > div > div:first-child {
|
|
736
|
+
padding-left: 80px !important;
|
|
737
|
+
position: relative;
|
|
738
|
+
-webkit-app-region: no-drag;
|
|
739
|
+
}
|
|
740
|
+
html[data-termdock-desktop='true'] #root main > div > div:first-child::before {
|
|
741
|
+
content: '';
|
|
742
|
+
position: absolute;
|
|
743
|
+
top: 0;
|
|
744
|
+
bottom: 0;
|
|
745
|
+
left: 50%;
|
|
746
|
+
width: min(240px, 25vw);
|
|
747
|
+
transform: translateX(-50%);
|
|
748
|
+
pointer-events: none;
|
|
749
|
+
-webkit-app-region: drag;
|
|
750
|
+
}
|
|
751
|
+
html[data-termdock-desktop='true'] #root .h-full.flex.flex-col.app-chrome-bg.border-r > .shrink-0.border-b {
|
|
752
|
+
padding-left: 80px !important;
|
|
753
|
+
-webkit-app-region: drag;
|
|
754
|
+
}
|
|
755
|
+
html[data-termdock-desktop='true'] #root button,
|
|
756
|
+
html[data-termdock-desktop='true'] #root a,
|
|
757
|
+
html[data-termdock-desktop='true'] #root input,
|
|
758
|
+
html[data-termdock-desktop='true'] #root textarea,
|
|
759
|
+
html[data-termdock-desktop='true'] #root select,
|
|
760
|
+
html[data-termdock-desktop='true'] #root [data-rbd-draggable-id] {
|
|
761
|
+
-webkit-app-region: no-drag;
|
|
762
|
+
}
|
|
763
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'] {
|
|
764
|
+
padding-top: 0 !important;
|
|
765
|
+
}
|
|
766
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'][style*='pointer-events: auto'] {
|
|
767
|
+
transform: none !important;
|
|
768
|
+
}
|
|
769
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child {
|
|
770
|
+
-webkit-app-region: no-drag;
|
|
771
|
+
}
|
|
772
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child > div:first-child > .min-w-0.flex-1 {
|
|
773
|
+
-webkit-app-region: drag;
|
|
774
|
+
}
|
|
775
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) {
|
|
776
|
+
width: fit-content !important;
|
|
777
|
+
margin-left: auto !important;
|
|
778
|
+
}
|
|
779
|
+
html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) > button {
|
|
780
|
+
flex: none !important;
|
|
781
|
+
padding: 0.125rem 0.5rem !important;
|
|
782
|
+
font-size: 10px !important;
|
|
783
|
+
}
|
|
636
784
|
`);
|
|
637
785
|
});
|
|
638
786
|
window.on('closed', () => {
|
|
639
787
|
if (mainWindow === window)
|
|
640
788
|
mainWindow = null;
|
|
789
|
+
const serviceOrigin = windowServiceOrigins.get(window);
|
|
790
|
+
if (serviceOrigin && serviceWindows.get(serviceOrigin) === window) {
|
|
791
|
+
serviceWindows.delete(serviceOrigin);
|
|
792
|
+
}
|
|
641
793
|
});
|
|
642
794
|
window.on('focus', () => {
|
|
643
795
|
// User is looking at the app — the Dock badge has served its purpose.
|
|
644
796
|
clearUnreadNotifications();
|
|
645
797
|
});
|
|
646
798
|
window.on('close', (event) => {
|
|
647
|
-
if (process.platform === 'darwin' && !isQuitting) {
|
|
799
|
+
if (window === mainWindow && process.platform === 'darwin' && !isQuitting) {
|
|
648
800
|
event.preventDefault();
|
|
649
801
|
window.hide();
|
|
650
802
|
}
|
|
@@ -653,7 +805,7 @@ function createMainWindow() {
|
|
|
653
805
|
}
|
|
654
806
|
function installIpcHandlers() {
|
|
655
807
|
ipcMain.handle('desktop:snapshot', () => snapshot());
|
|
656
|
-
ipcMain.handle('desktop:probe', (_event, url) =>
|
|
808
|
+
ipcMain.handle('desktop:probe', (_event, url) => probeServiceWithLocalNetworkPermission(url));
|
|
657
809
|
ipcMain.handle('desktop:save-connection', async (_event, input) => {
|
|
658
810
|
const url = normalizeServiceUrl(input.url);
|
|
659
811
|
const config = readDesktopConfig();
|
|
@@ -669,12 +821,14 @@ function installIpcHandlers() {
|
|
|
669
821
|
});
|
|
670
822
|
}
|
|
671
823
|
writeDesktopConfig(config);
|
|
824
|
+
installMenu();
|
|
672
825
|
return snapshot();
|
|
673
826
|
});
|
|
674
827
|
ipcMain.handle('desktop:remove-connection', async (_event, id) => {
|
|
675
828
|
const config = readDesktopConfig();
|
|
676
829
|
config.connections = config.connections.filter((entry) => entry.id !== id);
|
|
677
830
|
writeDesktopConfig(config);
|
|
831
|
+
installMenu();
|
|
678
832
|
return snapshot();
|
|
679
833
|
});
|
|
680
834
|
ipcMain.handle('desktop:connect', (_event, url) => connectWindow(url));
|
|
@@ -687,7 +841,7 @@ function installIpcHandlers() {
|
|
|
687
841
|
if (error)
|
|
688
842
|
throw new Error(error);
|
|
689
843
|
});
|
|
690
|
-
ipcMain.handle('desktop:show-notification', (
|
|
844
|
+
ipcMain.handle('desktop:show-notification', (event, payload) => {
|
|
691
845
|
if (!Notification.isSupported() || typeof payload?.title !== 'string')
|
|
692
846
|
return false;
|
|
693
847
|
// Web Notification tag semantics: a new notification with the same tag
|
|
@@ -712,11 +866,15 @@ function installIpcHandlers() {
|
|
|
712
866
|
activeNotifications.delete(tag);
|
|
713
867
|
});
|
|
714
868
|
}
|
|
869
|
+
const sourceWindow = BrowserWindow.fromWebContents(event.sender);
|
|
715
870
|
notification.on('click', () => {
|
|
716
|
-
|
|
717
|
-
|
|
871
|
+
const targetWindow = sourceWindow && !sourceWindow.isDestroyed()
|
|
872
|
+
? sourceWindow
|
|
873
|
+
: focusedWorkspaceWindow();
|
|
874
|
+
targetWindow?.show();
|
|
875
|
+
targetWindow?.focus();
|
|
718
876
|
if (typeof payload.sessionId === 'string') {
|
|
719
|
-
|
|
877
|
+
targetWindow?.webContents.send('desktop:focus-session', payload.sessionId);
|
|
720
878
|
}
|
|
721
879
|
});
|
|
722
880
|
notification.show();
|
|
@@ -724,7 +882,7 @@ function installIpcHandlers() {
|
|
|
724
882
|
app.setBadgeCount(unreadNotificationCount);
|
|
725
883
|
// macOS banners auto-dismiss even for "persistent" alerts; bounce the Dock
|
|
726
884
|
// icon so the signal survives until the user looks at the app.
|
|
727
|
-
if (payload.persistent === true && !
|
|
885
|
+
if (payload.persistent === true && !sourceWindow?.isFocused()) {
|
|
728
886
|
app.dock?.bounce('informational');
|
|
729
887
|
}
|
|
730
888
|
return true;
|
|
@@ -732,8 +890,37 @@ function installIpcHandlers() {
|
|
|
732
890
|
}
|
|
733
891
|
function installMenu() {
|
|
734
892
|
const sendCommand = (command) => {
|
|
735
|
-
|
|
893
|
+
focusedWorkspaceWindow()?.webContents.send('desktop:command', command);
|
|
894
|
+
};
|
|
895
|
+
const openService = (url) => {
|
|
896
|
+
void connectWindow(url).then((probe) => {
|
|
897
|
+
if (!probe.ok) {
|
|
898
|
+
void showDesktopMessageBox({
|
|
899
|
+
type: 'error',
|
|
900
|
+
title: '连接 Termdock 服务失败',
|
|
901
|
+
message: `无法打开 ${probe.url}`,
|
|
902
|
+
detail: probe.error,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
};
|
|
907
|
+
const openLocalService = () => {
|
|
908
|
+
void getLocalServiceStatus().then((status) => {
|
|
909
|
+
if (status.running && status.state) {
|
|
910
|
+
openService(stateUrl(status.state));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
void showConnectionCenter();
|
|
914
|
+
});
|
|
736
915
|
};
|
|
916
|
+
const config = readDesktopConfig();
|
|
917
|
+
const serviceItems = [
|
|
918
|
+
{ label: '本机', click: openLocalService },
|
|
919
|
+
...config.connections.map((connection) => ({
|
|
920
|
+
label: connection.label || new URL(connection.url).host,
|
|
921
|
+
click: () => openService(connection.url),
|
|
922
|
+
})),
|
|
923
|
+
];
|
|
737
924
|
const menu = Menu.buildFromTemplate([
|
|
738
925
|
{
|
|
739
926
|
label: 'Termdock',
|
|
@@ -743,13 +930,14 @@ function installMenu() {
|
|
|
743
930
|
{
|
|
744
931
|
label: '设置…',
|
|
745
932
|
accelerator: 'CmdOrCtrl+,',
|
|
746
|
-
click: () =>
|
|
933
|
+
click: () => focusedWorkspaceWindow()?.webContents.send('desktop:open-settings'),
|
|
747
934
|
},
|
|
748
935
|
{
|
|
749
936
|
label: '连接中心',
|
|
750
|
-
accelerator: 'CmdOrCtrl+
|
|
937
|
+
accelerator: 'CmdOrCtrl+N',
|
|
751
938
|
click: () => void showConnectionCenter(),
|
|
752
939
|
},
|
|
940
|
+
{ label: '打开服务', submenu: serviceItems },
|
|
753
941
|
{
|
|
754
942
|
label: '安装或修复 CLI…',
|
|
755
943
|
click: () => void installCli().catch((error) => {
|
|
@@ -855,6 +1043,13 @@ function installMenu() {
|
|
|
855
1043
|
{
|
|
856
1044
|
label: '窗口',
|
|
857
1045
|
submenu: [
|
|
1046
|
+
{
|
|
1047
|
+
label: '连接中心',
|
|
1048
|
+
accelerator: 'CmdOrCtrl+Shift+O',
|
|
1049
|
+
click: () => void showConnectionCenter(),
|
|
1050
|
+
},
|
|
1051
|
+
{ label: '打开服务', submenu: serviceItems },
|
|
1052
|
+
{ type: 'separator' },
|
|
858
1053
|
{ role: 'minimize' },
|
|
859
1054
|
{ role: 'zoom' },
|
|
860
1055
|
{ role: 'front' },
|
|
@@ -862,11 +1057,17 @@ function installMenu() {
|
|
|
862
1057
|
},
|
|
863
1058
|
]);
|
|
864
1059
|
Menu.setApplicationMenu(menu);
|
|
1060
|
+
app.dock?.setMenu(Menu.buildFromTemplate([
|
|
1061
|
+
{ label: '连接中心', click: () => void showConnectionCenter() },
|
|
1062
|
+
{ type: 'separator' },
|
|
1063
|
+
...serviceItems,
|
|
1064
|
+
]));
|
|
865
1065
|
}
|
|
866
1066
|
app.whenReady().then(async () => {
|
|
1067
|
+
configureLocalServiceCertificateTrust();
|
|
867
1068
|
installIpcHandlers();
|
|
868
1069
|
installMenu();
|
|
869
|
-
mainWindow =
|
|
1070
|
+
mainWindow = createDesktopWindow();
|
|
870
1071
|
configureDesktopUpdater(showDesktopMessageBox);
|
|
871
1072
|
await showConnectionCenter();
|
|
872
1073
|
const lastConnectionUrl = readDesktopConfig().lastConnectionUrl;
|
|
@@ -878,9 +1079,13 @@ app.whenReady().then(async () => {
|
|
|
878
1079
|
}
|
|
879
1080
|
app.on('activate', () => {
|
|
880
1081
|
if (!mainWindow) {
|
|
881
|
-
mainWindow =
|
|
1082
|
+
mainWindow = createDesktopWindow();
|
|
882
1083
|
void showConnectionCenter();
|
|
883
1084
|
}
|
|
1085
|
+
else {
|
|
1086
|
+
mainWindow.show();
|
|
1087
|
+
mainWindow.focus();
|
|
1088
|
+
}
|
|
884
1089
|
});
|
|
885
1090
|
}).catch((error) => {
|
|
886
1091
|
void dialog.showErrorBox('Termdock 启动失败', error instanceof Error ? error.message : String(error));
|
package/README.md
CHANGED
|
@@ -82,8 +82,21 @@ npx termdock --host 127.0.0.1 --port 4000
|
|
|
82
82
|
termdock --foreground # 前台运行
|
|
83
83
|
termdock --status # 查看后台状态
|
|
84
84
|
termdock --stop # 停止后台服务
|
|
85
|
+
td update # 从 npm 官方源升级全局 CLI
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
`td update`(也可写作 `td upgrade`)会优先使用 npm 官方源
|
|
89
|
+
`https://registry.npmjs.org` 查询并安装最新正式版;仅当官方源查询或安装失败时,
|
|
90
|
+
才会回退到本机 `.npmrc` 配置的源(npm 命令不再传入 `--registry`)。
|
|
91
|
+
如果后台服务正在运行,升级不会打断现有终端会话;可在方便时执行
|
|
92
|
+
`td --stop && td`,让服务切换到新版本。
|
|
93
|
+
|
|
94
|
+
npm CLI 服务也会自动更新:启动 15 秒后检查一次,之后每 6 小时检查。
|
|
95
|
+
新版会在后台安装,但无论是否有网页连接都绝不会自动重启。更新状态会持久保存,
|
|
96
|
+
用户打开网页后可在左侧边栏“更多”按钮看到更新圆点和提醒;只有用户明确确认后
|
|
97
|
+
才会重启,未确认的提醒会一直保留。tmux 和由 PTY host 托管的会话可跨此次重启继续使用。
|
|
98
|
+
macOS 桌面版仍使用独立的签名更新机制,不运行 npm 自动更新器。
|
|
99
|
+
|
|
87
100
|
### 设置访问密码(强烈推荐)
|
|
88
101
|
|
|
89
102
|
如果服务暴露在 LAN 上,**务必先设置密码**,否则任何能访问到主机/端口的人都能执行 shell 命令:
|