zen-gitsync 2.12.2 → 2.12.4
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/LICENSE +190 -21
- package/index.js +25 -11
- package/package.json +18 -5
- package/scripts/convert-colors-to-vars.cjs +286 -272
- package/scripts/convert-fontsize-to-vars.cjs +221 -207
- package/scripts/convert-spacing-to-vars.cjs +256 -242
- package/scripts/convert-to-standard-vars.cjs +282 -268
- package/scripts/release.js +599 -585
- package/src/config.js +350 -336
- package/src/gitCommit.js +455 -440
- package/src/ui/public/assets/{EditorView-bnJmBq-i.js → EditorView-BZaOzahT.js} +2 -2
- package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
- package/src/ui/public/assets/{SourceMapView-Rz5SD0A0.js → SourceMapView-D_8mnVt2.js} +3 -3
- package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
- package/src/ui/public/assets/{index-Bo3tntQh.js → index-BgFwmXzV.js} +11 -11
- package/src/ui/public/assets/{index-bOs5P8fz.css → index-Dsi6tg7k.css} +1 -1
- package/src/ui/public/index.html +2 -2
- package/src/ui/server/index.js +410 -396
- package/src/ui/server/middleware/requestLogger.js +51 -37
- package/src/ui/server/routes/branchStatus.js +101 -87
- package/src/ui/server/routes/code.js +110 -96
- package/src/ui/server/routes/codeAnalysis.js +995 -981
- package/src/ui/server/routes/config.js +1190 -1158
- package/src/ui/server/routes/exec.js +272 -258
- package/src/ui/server/routes/fileOpen.js +279 -265
- package/src/ui/server/routes/fs.js +701 -687
- package/src/ui/server/routes/git/diff.js +352 -338
- package/src/ui/server/routes/git/diffUtils.js +128 -114
- package/src/ui/server/routes/git/stash.js +552 -538
- package/src/ui/server/routes/git/tags.js +172 -158
- package/src/ui/server/routes/git.js +190 -176
- package/src/ui/server/routes/gitOps.js +1179 -1165
- package/src/ui/server/routes/instances.js +14 -0
- package/src/ui/server/routes/npm.js +1023 -1009
- package/src/ui/server/routes/process.js +82 -68
- package/src/ui/server/routes/status.js +67 -53
- package/src/ui/server/routes/terminal.js +319 -305
- package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
- package/src/ui/server/utils/createSavePortToFile.js +46 -32
- package/src/ui/server/utils/instanceRegistry.js +14 -0
- package/src/ui/server/utils/pathGuard.js +14 -0
- package/src/ui/server/utils/pathGuard.test.js +14 -0
- package/src/ui/server/utils/randomStartPort.js +14 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
- package/src/utils/index.js +14 -0
- package/src/ui/public/assets/EditorView-CHBjgiZc.css +0 -1
- package/src/ui/public/assets/SourceMapView-DhQX0K7t.css +0 -1
|
@@ -1,305 +1,319 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
import { spawn, exec } from 'child_process';
|
|
16
|
+
|
|
17
|
+
export function registerTerminalRoutes({
|
|
18
|
+
app,
|
|
19
|
+
getCurrentProjectPath,
|
|
20
|
+
nextTerminalSessionId,
|
|
21
|
+
terminalSessions
|
|
22
|
+
}) {
|
|
23
|
+
async function startTerminalProcess({ command, workingDirectory }) {
|
|
24
|
+
const targetDir = workingDirectory || getCurrentProjectPath();
|
|
25
|
+
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
const cmdToRun = String(command || '').trim();
|
|
28
|
+
const safeWorkingDir = String(targetDir).replace(/"/g, '""');
|
|
29
|
+
|
|
30
|
+
const splitArgs = (input) => {
|
|
31
|
+
const s = String(input || '');
|
|
32
|
+
const out = [];
|
|
33
|
+
let cur = '';
|
|
34
|
+
let inQuotes = false;
|
|
35
|
+
for (let i = 0; i < s.length; i++) {
|
|
36
|
+
const ch = s[i];
|
|
37
|
+
if (ch === '"') {
|
|
38
|
+
if (inQuotes && s[i + 1] === '"') {
|
|
39
|
+
cur += '"';
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
inQuotes = !inQuotes;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!inQuotes && /\s/.test(ch)) {
|
|
47
|
+
if (cur) out.push(cur);
|
|
48
|
+
cur = '';
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
cur += ch;
|
|
52
|
+
}
|
|
53
|
+
if (cur) out.push(cur);
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isUrl = (v) => /^https?:\/\//i.test(String(v || '').trim());
|
|
58
|
+
|
|
59
|
+
const tokens = splitArgs(cmdToRun);
|
|
60
|
+
const isStartCommand = tokens.length > 0 && String(tokens[0]).toLowerCase() === 'start';
|
|
61
|
+
|
|
62
|
+
let psScript;
|
|
63
|
+
if (isStartCommand) {
|
|
64
|
+
const args = tokens.slice(1);
|
|
65
|
+
const first = args[0];
|
|
66
|
+
const second = args[1];
|
|
67
|
+
const candidateExe = first === '' ? second : first;
|
|
68
|
+
const candidateUrl = isUrl(first) ? first : isUrl(second) ? second : null;
|
|
69
|
+
const exe = candidateExe && !isUrl(candidateExe) ? String(candidateExe) : null;
|
|
70
|
+
|
|
71
|
+
if (!exe && candidateUrl) {
|
|
72
|
+
const safeUrl = String(candidateUrl).replace(/"/g, '""');
|
|
73
|
+
psScript = `$p = Start-Process -FilePath "${safeUrl}" -WorkingDirectory "${safeWorkingDir}" -PassThru; Write-Output $p.Id`;
|
|
74
|
+
} else if (exe && candidateUrl) {
|
|
75
|
+
const safeExe = String(exe).replace(/"/g, '""');
|
|
76
|
+
const safeUrl = String(candidateUrl).replace(/"/g, '""');
|
|
77
|
+
psScript = `$p = Start-Process -FilePath "${safeExe}" -ArgumentList "${safeUrl}" -WorkingDirectory "${safeWorkingDir}" -PassThru; Write-Output $p.Id`;
|
|
78
|
+
} else {
|
|
79
|
+
const safeCmd = String(cmdToRun).replace(/"/g, '""');
|
|
80
|
+
psScript = `$p = Start-Process -FilePath "cmd.exe" -ArgumentList "/C", "${safeCmd}" -WorkingDirectory "${safeWorkingDir}" -WindowStyle Hidden -PassThru; Write-Output $p.Id`;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const safeCmd = String(cmdToRun).replace(/"/g, '""');
|
|
84
|
+
psScript = `$p = Start-Process -FilePath "cmd.exe" -ArgumentList "/K", "${safeCmd}" -WorkingDirectory "${safeWorkingDir}" -PassThru; Write-Output $p.Id`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await new Promise((resolve, reject) => {
|
|
88
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', psScript], {
|
|
89
|
+
windowsHide: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let out = '';
|
|
93
|
+
let err = '';
|
|
94
|
+
|
|
95
|
+
child.stdout?.on('data', (d) => {
|
|
96
|
+
out += d.toString('utf8');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child.stderr?.on('data', (d) => {
|
|
100
|
+
err += d.toString('utf8');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on('error', (e) => reject(e));
|
|
104
|
+
|
|
105
|
+
child.on('close', (code) => {
|
|
106
|
+
if (code !== 0) {
|
|
107
|
+
return reject(new Error(err || `Start-Process 失败,退出码: ${code}`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const pid = parseInt(String(out).trim(), 10);
|
|
111
|
+
if (!Number.isFinite(pid)) {
|
|
112
|
+
return reject(new Error(`无法获取终端 PID: ${out || err || 'unknown'}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resolve({ pid });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (process.platform === 'darwin') {
|
|
121
|
+
const script = `tell application "Terminal" to do script "cd ${targetDir} && ${command.trim()}"`;
|
|
122
|
+
exec(`osascript -e '${script}'`, (error) => {
|
|
123
|
+
if (error) {
|
|
124
|
+
console.error('打开终端失败:', error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return { pid: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const terminalCommand = `gnome-terminal -- bash -c "cd ${targetDir} && ${command.trim()}; exec bash" || xterm -e "cd ${targetDir} && ${command.trim()}; bash"`;
|
|
131
|
+
exec(terminalCommand, (error) => {
|
|
132
|
+
if (error) {
|
|
133
|
+
console.error('打开终端失败:', error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return { pid: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 在新终端中执行自定义命令
|
|
140
|
+
app.post('/api/exec-in-terminal', async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const { command, workingDirectory } = req.body || {};
|
|
143
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
144
|
+
return res.status(400).json({ success: false, error: 'command 不能为空' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targetDir = workingDirectory || getCurrentProjectPath();
|
|
148
|
+
console.log(`在终端中执行命令: ${command}`);
|
|
149
|
+
console.log(`工作目录: ${targetDir}`);
|
|
150
|
+
|
|
151
|
+
const terminalSessionId = nextTerminalSessionId();
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
terminalSessions.set(terminalSessionId, {
|
|
154
|
+
id: terminalSessionId,
|
|
155
|
+
command: command.trim(),
|
|
156
|
+
workingDirectory: targetDir,
|
|
157
|
+
pid: null,
|
|
158
|
+
createdAt: now,
|
|
159
|
+
lastStartedAt: now
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { pid } = await startTerminalProcess({ command, workingDirectory: targetDir });
|
|
163
|
+
const session = terminalSessions.get(terminalSessionId);
|
|
164
|
+
if (session) {
|
|
165
|
+
session.pid = pid;
|
|
166
|
+
session.lastStartedAt = Date.now();
|
|
167
|
+
terminalSessions.set(terminalSessionId, session);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
res.json({
|
|
171
|
+
success: true,
|
|
172
|
+
message: `已在新终端中执行命令`,
|
|
173
|
+
session: terminalSessions.get(terminalSessionId)
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('在终端中执行命令失败:', error);
|
|
177
|
+
res.status(500).json({
|
|
178
|
+
success: false,
|
|
179
|
+
error: `在终端中执行命令失败: ${error.message}`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.get('/api/terminal-sessions', async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const sessions = Array.from(terminalSessions.values()).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
187
|
+
res.json({ success: true, sessions });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
res.status(500).json({ success: false, error: error.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function killTerminalPid(pid) {
|
|
194
|
+
if (!pid) return;
|
|
195
|
+
|
|
196
|
+
if (process.platform === 'win32') {
|
|
197
|
+
exec(`taskkill /pid ${pid} /T /F`, (error) => {
|
|
198
|
+
if (error) {
|
|
199
|
+
console.warn(`[终端会话] taskkill 失败(可忽略): ${error?.message || error}`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
process.kill(pid, 'SIGTERM');
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// ignore
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function isPidAlive(pid) {
|
|
213
|
+
if (!pid) return false;
|
|
214
|
+
|
|
215
|
+
if (process.platform === 'win32') {
|
|
216
|
+
const script = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Out-Null; if ($?) { exit 0 } else { exit 1 }`;
|
|
217
|
+
return await new Promise((resolve) => {
|
|
218
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', script], { windowsHide: true });
|
|
219
|
+
child.on('error', () => resolve(false));
|
|
220
|
+
child.on('close', (code) => resolve(code === 0));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
process.kill(pid, 0);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
app.post('/api/terminal-sessions/:id/restart', async (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const id = parseInt(req.params.id, 10);
|
|
235
|
+
if (!Number.isFinite(id)) {
|
|
236
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const session = terminalSessions.get(id);
|
|
240
|
+
if (!session) {
|
|
241
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const oldPid = session.pid;
|
|
245
|
+
if (oldPid) {
|
|
246
|
+
killTerminalPid(oldPid);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { pid } = await startTerminalProcess({ command: session.command, workingDirectory: session.workingDirectory });
|
|
250
|
+
session.pid = pid;
|
|
251
|
+
session.lastStartedAt = Date.now();
|
|
252
|
+
terminalSessions.set(id, session);
|
|
253
|
+
|
|
254
|
+
res.json({ success: true, session });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
res.status(500).json({ success: false, error: error.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.get('/api/terminal-sessions/status', async (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const cleanup = String(req.query.cleanup || 'false') === 'true';
|
|
263
|
+
const sessions = Array.from(terminalSessions.values());
|
|
264
|
+
const results = await Promise.all(sessions.map(async (s) => {
|
|
265
|
+
const alive = s?.pid ? await isPidAlive(s.pid) : false;
|
|
266
|
+
return { ...s, alive };
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
if (cleanup) {
|
|
270
|
+
const PROTECT_DURATION_MS = 10000; // 刚启动的进程保护期 10 秒
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
for (const s of results) {
|
|
273
|
+
if (s.pid && !s.alive) {
|
|
274
|
+
// 如果会话刚刚启动(10秒内),即使检测不到进程也不删除,给进程启动留时间
|
|
275
|
+
const lastStarted = s.lastStartedAt || s.createdAt || 0;
|
|
276
|
+
if (now - lastStarted < PROTECT_DURATION_MS) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
terminalSessions.delete(s.id);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const output = cleanup
|
|
285
|
+
? Array.from(terminalSessions.values()).map((s) => {
|
|
286
|
+
const found = results.find(r => r.id === s.id);
|
|
287
|
+
return { ...s, alive: found?.alive ?? false };
|
|
288
|
+
}).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
|
289
|
+
: results.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
290
|
+
|
|
291
|
+
res.json({ success: true, sessions: output });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
res.status(500).json({ success: false, error: error.message });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
app.delete('/api/terminal-sessions/:id', async (req, res) => {
|
|
298
|
+
try {
|
|
299
|
+
const id = parseInt(req.params.id, 10);
|
|
300
|
+
if (!Number.isFinite(id)) {
|
|
301
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const session = terminalSessions.get(id);
|
|
305
|
+
if (!session) {
|
|
306
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (session.pid) {
|
|
310
|
+
killTerminalPid(session.pid);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
terminalSessions.delete(id);
|
|
314
|
+
res.json({ success: true, removedId: id });
|
|
315
|
+
} catch (error) {
|
|
316
|
+
res.status(500).json({ success: false, error: error.message });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|