@adversity/coding-tool-x 2.4.2 → 2.5.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/CHANGELOG.md +23 -0
- package/dist/web/assets/{icons-BkBtk3H1.js → icons-BALJo7bE.js} +1 -1
- package/dist/web/assets/{index-Cgq2DyzS.css → index-CvHZsWbE.css} +3 -3
- package/dist/web/assets/index-DZjidyED.js +14 -0
- package/dist/web/assets/{naive-ui-D-gb0WfN.js → naive-ui-sh0u_0bf.js} +1 -1
- package/dist/web/assets/{vendors-Bd5vxA1-.js → vendors-CzcvkTIS.js} +1 -1
- package/dist/web/assets/{vue-vendor-hRp8vsrL.js → vue-vendor-CEeI-Azr.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +2 -1
- package/src/commands/security.js +36 -0
- package/src/index.js +19 -3
- package/src/server/api/config-export.js +122 -32
- package/src/server/api/security.js +53 -0
- package/src/server/api/terminal.js +5 -1
- package/src/server/index.js +1 -0
- package/src/server/services/config-export-service.js +611 -38
- package/src/server/services/pty-manager.js +250 -28
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/terminal-config.js +81 -1
- package/src/server/services/terminal-detector.js +76 -1
- package/src/server/services/ui-config.js +2 -0
- package/src/server/websocket-server.js +14 -3
- package/dist/web/assets/index-DOsR4qc6.js +0 -14
|
@@ -10,6 +10,8 @@ const fs = require('fs');
|
|
|
10
10
|
// 尝试加载 node-pty,如果失败则提示
|
|
11
11
|
let pty = null;
|
|
12
12
|
let ptyError = null;
|
|
13
|
+
let HeadlessTerminal = null;
|
|
14
|
+
let headlessError = null;
|
|
13
15
|
|
|
14
16
|
try {
|
|
15
17
|
// 优先使用 @lydell/node-pty (支持更新的 Node.js 版本)
|
|
@@ -24,6 +26,14 @@ try {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
try {
|
|
30
|
+
const headless = require('@xterm/headless');
|
|
31
|
+
HeadlessTerminal = headless.Terminal || headless.default?.Terminal || headless.default || null;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
headlessError = err.message;
|
|
34
|
+
console.warn('Warning: @xterm/headless failed to load:', err.message);
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
class PtyManager {
|
|
28
38
|
constructor() {
|
|
29
39
|
// 终端进程池: terminalId -> { pty, ws, metadata }
|
|
@@ -32,6 +42,89 @@ class PtyManager {
|
|
|
32
42
|
|
|
33
43
|
// 清理已退出的进程
|
|
34
44
|
this.cleanupInterval = setInterval(() => this.cleanupDeadTerminals(), 30000);
|
|
45
|
+
// 避免在未启用 Web 终端时阻止进程退出
|
|
46
|
+
if (typeof this.cleanupInterval.unref === 'function') {
|
|
47
|
+
this.cleanupInterval.unref();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createScreen(cols, rows) {
|
|
52
|
+
if (!HeadlessTerminal) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return new HeadlessTerminal({
|
|
57
|
+
cols: Math.max(cols, 80),
|
|
58
|
+
rows: Math.max(rows, 24),
|
|
59
|
+
scrollback: 10000,
|
|
60
|
+
convertEol: true
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn('Failed to create headless terminal:', err.message);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
buildScreenSnapshot(terminal) {
|
|
69
|
+
if (!terminal?.screen || !terminal.screen.buffer?.active) {
|
|
70
|
+
return { data: '' };
|
|
71
|
+
}
|
|
72
|
+
const screen = terminal.screen;
|
|
73
|
+
const buffer = screen.buffer.active;
|
|
74
|
+
const end = buffer.length;
|
|
75
|
+
const cols = screen.cols || terminal.metadata?.cols || 80;
|
|
76
|
+
const rows = screen.rows || terminal.metadata?.rows || 24;
|
|
77
|
+
const cursorLineIndex = Math.max(0, (buffer.baseY || 0) + (buffer.cursorY || 0));
|
|
78
|
+
const cursorCol = Math.max(0, buffer.cursorX || 0);
|
|
79
|
+
const lastLineIndex = end > 0 ? end - 1 : -1;
|
|
80
|
+
let output = '';
|
|
81
|
+
let hasOutput = false;
|
|
82
|
+
for (let i = 0; i <= lastLineIndex; i++) {
|
|
83
|
+
const line = buffer.getLine(i);
|
|
84
|
+
if (!line) {
|
|
85
|
+
if (hasOutput) {
|
|
86
|
+
output += '\r\n';
|
|
87
|
+
} else {
|
|
88
|
+
output = '';
|
|
89
|
+
hasOutput = true;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let text = '';
|
|
94
|
+
try {
|
|
95
|
+
text = line.translateToString(true);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
try {
|
|
98
|
+
text = line.translateToString(false);
|
|
99
|
+
} catch (err2) {
|
|
100
|
+
text = '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const isWrapped = Boolean(line.isWrapped);
|
|
104
|
+
if (!hasOutput) {
|
|
105
|
+
output = text;
|
|
106
|
+
hasOutput = true;
|
|
107
|
+
} else if (isWrapped) {
|
|
108
|
+
output += text;
|
|
109
|
+
} else {
|
|
110
|
+
output += '\r\n' + text;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const data = hasOutput ? output : '';
|
|
114
|
+
const topIndex = Math.max(0, lastLineIndex - rows + 1);
|
|
115
|
+
let cursorRow = cursorLineIndex - topIndex + 1;
|
|
116
|
+
if (!Number.isFinite(cursorRow)) {
|
|
117
|
+
cursorRow = rows;
|
|
118
|
+
}
|
|
119
|
+
cursorRow = Math.min(Math.max(cursorRow, 1), rows);
|
|
120
|
+
const cursorColSafe = Math.min(Math.max(cursorCol + 1, 1), cols);
|
|
121
|
+
return {
|
|
122
|
+
data,
|
|
123
|
+
cursorRow,
|
|
124
|
+
cursorCol: cursorColSafe,
|
|
125
|
+
rows,
|
|
126
|
+
cols
|
|
127
|
+
};
|
|
35
128
|
}
|
|
36
129
|
|
|
37
130
|
/**
|
|
@@ -164,9 +257,14 @@ class PtyManager {
|
|
|
164
257
|
pty: ptyProcess,
|
|
165
258
|
ws: null,
|
|
166
259
|
buffer: [], // 缓存未发送的输出(用于 WebSocket 断开期间)
|
|
167
|
-
history: [], //
|
|
260
|
+
history: [], // 断线期间的输出缓冲(仅用于补发)
|
|
168
261
|
historySize: 0, // 历史记录字节大小
|
|
169
262
|
maxHistorySize: 100 * 1024, // 最大历史记录大小 100KB
|
|
263
|
+
screen: this.createScreen(cols, rows),
|
|
264
|
+
pendingOutput: [],
|
|
265
|
+
snapshotInProgress: false,
|
|
266
|
+
outputSeq: 0,
|
|
267
|
+
snapshotSeq: 0,
|
|
170
268
|
metadata: {
|
|
171
269
|
cwd,
|
|
172
270
|
shell,
|
|
@@ -183,23 +281,36 @@ class PtyManager {
|
|
|
183
281
|
|
|
184
282
|
// 监听 PTY 输出
|
|
185
283
|
ptyProcess.onData((data) => {
|
|
186
|
-
|
|
187
|
-
terminal.
|
|
188
|
-
terminal.
|
|
189
|
-
|
|
190
|
-
// 如果历史记录超出限制,裁剪前面的内容
|
|
191
|
-
while (terminal.historySize > terminal.maxHistorySize && terminal.history.length > 1) {
|
|
192
|
-
const removed = terminal.history.shift();
|
|
193
|
-
terminal.historySize -= removed.length;
|
|
284
|
+
terminal.outputSeq += 1;
|
|
285
|
+
const outputEntry = { seq: terminal.outputSeq, data };
|
|
286
|
+
if (terminal.screen) {
|
|
287
|
+
terminal.screen.write(data);
|
|
194
288
|
}
|
|
195
289
|
|
|
196
290
|
if (terminal.ws && terminal.ws.readyState === 1) { // WebSocket.OPEN
|
|
197
|
-
terminal.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
291
|
+
if (terminal.snapshotInProgress) {
|
|
292
|
+
terminal.pendingOutput.push(outputEntry);
|
|
293
|
+
} else {
|
|
294
|
+
terminal.ws.send(JSON.stringify({
|
|
295
|
+
type: 'terminal:output',
|
|
296
|
+
terminalId,
|
|
297
|
+
data
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!terminal.screen) {
|
|
304
|
+
// 仅在断开期间缓存输出,避免重连时重复回放
|
|
305
|
+
terminal.history.push(data);
|
|
306
|
+
terminal.historySize += data.length;
|
|
307
|
+
|
|
308
|
+
// 如果历史记录超出限制,裁剪前面的内容
|
|
309
|
+
while (terminal.historySize > terminal.maxHistorySize && terminal.history.length > 1) {
|
|
310
|
+
const removed = terminal.history.shift();
|
|
311
|
+
terminal.historySize -= removed.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
203
314
|
// 缓存输出,等待 WebSocket 连接
|
|
204
315
|
terminal.buffer.push(data);
|
|
205
316
|
// 限制缓存大小
|
|
@@ -225,6 +336,7 @@ class PtyManager {
|
|
|
225
336
|
// 标记为已退出,稍后清理
|
|
226
337
|
terminal.exited = true;
|
|
227
338
|
terminal.exitCode = exitCode;
|
|
339
|
+
terminal.exitedAt = Date.now();
|
|
228
340
|
});
|
|
229
341
|
|
|
230
342
|
this.terminals.set(terminalId, terminal);
|
|
@@ -253,7 +365,7 @@ class PtyManager {
|
|
|
253
365
|
* @param {WebSocket} ws - WebSocket 连接
|
|
254
366
|
* @returns {boolean} 是否成功
|
|
255
367
|
*/
|
|
256
|
-
attachWebSocket(terminalId, ws) {
|
|
368
|
+
attachWebSocket(terminalId, ws, options = {}) {
|
|
257
369
|
const terminal = this.terminals.get(terminalId);
|
|
258
370
|
if (!terminal) {
|
|
259
371
|
console.warn(`Terminal ${terminalId} not found`);
|
|
@@ -261,20 +373,114 @@ class PtyManager {
|
|
|
261
373
|
}
|
|
262
374
|
|
|
263
375
|
terminal.ws = ws;
|
|
376
|
+
terminal.snapshotInProgress = false;
|
|
377
|
+
terminal.pendingOutput = [];
|
|
378
|
+
terminal.snapshotSeq = 0;
|
|
264
379
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
380
|
+
const { includeHistory = true, trimLastLine = false } = options;
|
|
381
|
+
|
|
382
|
+
const trimHistoryLastLine = (data) => {
|
|
383
|
+
if (!data) return '';
|
|
384
|
+
if (data.endsWith('\r\n')) {
|
|
385
|
+
return data.slice(0, -2);
|
|
386
|
+
}
|
|
387
|
+
if (data.endsWith('\n')) {
|
|
388
|
+
return data.slice(0, -1);
|
|
389
|
+
}
|
|
390
|
+
return data;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (terminal.screen && includeHistory) {
|
|
394
|
+
terminal.snapshotInProgress = true;
|
|
395
|
+
terminal.pendingOutput = [];
|
|
396
|
+
terminal.snapshotSeq = terminal.outputSeq;
|
|
397
|
+
terminal.buffer = [];
|
|
398
|
+
terminal.history = [];
|
|
399
|
+
terminal.historySize = 0;
|
|
400
|
+
|
|
401
|
+
const sendSnapshot = () => {
|
|
402
|
+
try {
|
|
403
|
+
const snapshot = this.buildScreenSnapshot(terminal);
|
|
404
|
+
if (snapshot?.data && ws.readyState === 1) {
|
|
405
|
+
ws.send(JSON.stringify({
|
|
406
|
+
type: 'terminal:output',
|
|
407
|
+
terminalId,
|
|
408
|
+
data: snapshot.data
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
if (snapshot?.cursorRow && snapshot?.cursorCol && ws.readyState === 1) {
|
|
412
|
+
ws.send(JSON.stringify({
|
|
413
|
+
type: 'terminal:output',
|
|
414
|
+
terminalId,
|
|
415
|
+
data: `\x1b[${snapshot.cursorRow};${snapshot.cursorCol}H`
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
const pendingTail = terminal.pendingOutput
|
|
419
|
+
.filter(item => item && item.seq > terminal.snapshotSeq)
|
|
420
|
+
.map(item => item.data);
|
|
421
|
+
if (pendingTail.length > 0 && ws.readyState === 1) {
|
|
422
|
+
ws.send(JSON.stringify({
|
|
423
|
+
type: 'terminal:output',
|
|
424
|
+
terminalId,
|
|
425
|
+
data: pendingTail.join('')
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.warn(`Failed to send terminal snapshot ${terminalId}:`, err.message);
|
|
430
|
+
} finally {
|
|
431
|
+
terminal.pendingOutput = [];
|
|
432
|
+
terminal.snapshotInProgress = false;
|
|
433
|
+
terminal.snapshotSeq = 0;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
275
436
|
|
|
276
|
-
|
|
277
|
-
|
|
437
|
+
if (typeof terminal.screen.write === 'function') {
|
|
438
|
+
terminal.screen.write('', sendSnapshot);
|
|
439
|
+
} else {
|
|
440
|
+
sendSnapshot();
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
// 发送历史记录(用于重连时恢复之前的输出)
|
|
444
|
+
let sentHistory = false;
|
|
445
|
+
if (includeHistory) {
|
|
446
|
+
if (terminal.history.length > 0) {
|
|
447
|
+
let historyData = terminal.history.join('');
|
|
448
|
+
if (trimLastLine) {
|
|
449
|
+
historyData = trimHistoryLastLine(historyData);
|
|
450
|
+
}
|
|
451
|
+
if (historyData) {
|
|
452
|
+
ws.send(JSON.stringify({
|
|
453
|
+
type: 'terminal:output',
|
|
454
|
+
terminalId,
|
|
455
|
+
data: historyData
|
|
456
|
+
}));
|
|
457
|
+
console.log(`Sent ${terminal.history.length} history chunks (${terminal.historySize} bytes) to terminal ${terminalId}`);
|
|
458
|
+
sentHistory = true;
|
|
459
|
+
}
|
|
460
|
+
} else if (terminal.buffer.length > 0) {
|
|
461
|
+
let bufferData = terminal.buffer.join('');
|
|
462
|
+
if (trimLastLine) {
|
|
463
|
+
bufferData = trimHistoryLastLine(bufferData);
|
|
464
|
+
}
|
|
465
|
+
if (bufferData) {
|
|
466
|
+
ws.send(JSON.stringify({
|
|
467
|
+
type: 'terminal:output',
|
|
468
|
+
terminalId,
|
|
469
|
+
data: bufferData
|
|
470
|
+
}));
|
|
471
|
+
console.log(`Sent ${terminal.buffer.length} buffered chunks to terminal ${terminalId}`);
|
|
472
|
+
sentHistory = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 清空临时缓冲区(已经包含在历史记录中了)
|
|
478
|
+
terminal.buffer = [];
|
|
479
|
+
if (sentHistory) {
|
|
480
|
+
terminal.history = [];
|
|
481
|
+
terminal.historySize = 0;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
278
484
|
|
|
279
485
|
// 如果终端已退出,通知客户端
|
|
280
486
|
if (terminal.exited) {
|
|
@@ -297,6 +503,9 @@ class PtyManager {
|
|
|
297
503
|
const terminal = this.terminals.get(terminalId);
|
|
298
504
|
if (terminal) {
|
|
299
505
|
terminal.ws = null;
|
|
506
|
+
terminal.snapshotInProgress = false;
|
|
507
|
+
terminal.pendingOutput = [];
|
|
508
|
+
terminal.snapshotSeq = 0;
|
|
300
509
|
}
|
|
301
510
|
}
|
|
302
511
|
|
|
@@ -331,10 +540,16 @@ class PtyManager {
|
|
|
331
540
|
|
|
332
541
|
const newCols = Math.max(cols, 80);
|
|
333
542
|
const newRows = Math.max(rows, 24);
|
|
543
|
+
if (terminal.metadata.cols === newCols && terminal.metadata.rows === newRows) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
334
546
|
|
|
335
547
|
terminal.pty.resize(newCols, newRows);
|
|
336
548
|
terminal.metadata.cols = newCols;
|
|
337
549
|
terminal.metadata.rows = newRows;
|
|
550
|
+
if (terminal.screen && typeof terminal.screen.resize === 'function') {
|
|
551
|
+
terminal.screen.resize(newCols, newRows);
|
|
552
|
+
}
|
|
338
553
|
|
|
339
554
|
return true;
|
|
340
555
|
}
|
|
@@ -369,6 +584,10 @@ class PtyManager {
|
|
|
369
584
|
}
|
|
370
585
|
}
|
|
371
586
|
|
|
587
|
+
if (terminal.screen && typeof terminal.screen.dispose === 'function') {
|
|
588
|
+
terminal.screen.dispose();
|
|
589
|
+
}
|
|
590
|
+
|
|
372
591
|
this.terminals.delete(terminalId);
|
|
373
592
|
console.log(`Destroyed terminal ${terminalId}`);
|
|
374
593
|
|
|
@@ -423,6 +642,9 @@ class PtyManager {
|
|
|
423
642
|
if (terminal.exited && !terminal.ws) {
|
|
424
643
|
const exitedTime = terminal.exitedAt || now;
|
|
425
644
|
if (now - exitedTime > 5 * 60 * 1000) {
|
|
645
|
+
if (terminal.screen && typeof terminal.screen.dispose === 'function') {
|
|
646
|
+
terminal.screen.dispose();
|
|
647
|
+
}
|
|
426
648
|
this.terminals.delete(id);
|
|
427
649
|
console.log(`Cleaned up dead terminal ${id}`);
|
|
428
650
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const SECURITY_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
7
|
+
const SECURITY_FILE = path.join(SECURITY_DIR, 'security.json');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SECURITY_CONFIG = {
|
|
10
|
+
passwordHash: '',
|
|
11
|
+
salt: '',
|
|
12
|
+
updatedAt: null
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PBKDF2_ITERATIONS = 120000;
|
|
16
|
+
const PBKDF2_KEYLEN = 64;
|
|
17
|
+
const PBKDF2_DIGEST = 'sha512';
|
|
18
|
+
|
|
19
|
+
function ensureSecurityDir() {
|
|
20
|
+
if (!fs.existsSync(SECURITY_DIR)) {
|
|
21
|
+
fs.mkdirSync(SECURITY_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readSecurityConfig() {
|
|
26
|
+
ensureSecurityDir();
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(SECURITY_FILE)) {
|
|
29
|
+
return { ...DEFAULT_SECURITY_CONFIG };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(SECURITY_FILE, 'utf8');
|
|
34
|
+
const data = JSON.parse(content);
|
|
35
|
+
return {
|
|
36
|
+
...DEFAULT_SECURITY_CONFIG,
|
|
37
|
+
...data
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error loading security config:', error);
|
|
41
|
+
return { ...DEFAULT_SECURITY_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeSecurityConfig(config) {
|
|
46
|
+
ensureSecurityDir();
|
|
47
|
+
fs.writeFileSync(SECURITY_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
48
|
+
try {
|
|
49
|
+
fs.chmodSync(SECURITY_FILE, 0o600);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn('Failed to set security config permissions:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasPassword(config) {
|
|
56
|
+
return Boolean(config.passwordHash && config.salt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hashPassword(password, salt) {
|
|
60
|
+
return crypto
|
|
61
|
+
.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, PBKDF2_KEYLEN, PBKDF2_DIGEST)
|
|
62
|
+
.toString('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeEqualHash(a, b) {
|
|
66
|
+
try {
|
|
67
|
+
const bufferA = Buffer.from(a, 'hex');
|
|
68
|
+
const bufferB = Buffer.from(b, 'hex');
|
|
69
|
+
if (bufferA.length !== bufferB.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return crypto.timingSafeEqual(bufferA, bufferB);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSecurityStatus() {
|
|
79
|
+
const config = readSecurityConfig();
|
|
80
|
+
return { hasPassword: hasPassword(config) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function verifySecurityPassword(password) {
|
|
84
|
+
const config = readSecurityConfig();
|
|
85
|
+
if (!hasPassword(config)) {
|
|
86
|
+
return { ok: false, reason: 'not_set' };
|
|
87
|
+
}
|
|
88
|
+
const hash = hashPassword(password, config.salt);
|
|
89
|
+
return { ok: safeEqualHash(hash, config.passwordHash) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setSecurityPassword({ currentPassword, newPassword }) {
|
|
93
|
+
if (typeof newPassword !== 'string' || newPassword.length < 4) {
|
|
94
|
+
const error = new Error('新密码至少 4 位');
|
|
95
|
+
error.code = 'WEAK_PASSWORD';
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const config = readSecurityConfig();
|
|
100
|
+
if (hasPassword(config)) {
|
|
101
|
+
if (typeof currentPassword !== 'string' || !currentPassword) {
|
|
102
|
+
const error = new Error('请输入当前密码');
|
|
103
|
+
error.code = 'CURRENT_REQUIRED';
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const verifyResult = verifySecurityPassword(currentPassword);
|
|
107
|
+
if (!verifyResult.ok) {
|
|
108
|
+
const error = new Error('当前密码错误');
|
|
109
|
+
error.code = 'INVALID_PASSWORD';
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
115
|
+
const passwordHash = hashPassword(newPassword, salt);
|
|
116
|
+
const nextConfig = {
|
|
117
|
+
...config,
|
|
118
|
+
passwordHash,
|
|
119
|
+
salt,
|
|
120
|
+
updatedAt: new Date().toISOString()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
writeSecurityConfig(nextConfig);
|
|
124
|
+
return { hasPassword: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
getSecurityStatus,
|
|
129
|
+
verifySecurityPassword,
|
|
130
|
+
setSecurityPassword
|
|
131
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
-
const { detectAvailableTerminals, getDefaultTerminal } = require('./terminal-detector');
|
|
4
|
+
const { detectAvailableTerminals, getDefaultTerminal, getSystemShell } = require('./terminal-detector');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 获取配置文件路径
|
|
@@ -77,6 +77,78 @@ function getSelectedTerminal() {
|
|
|
77
77
|
return selectedTerminal || getDefaultTerminal();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function getSystemRoot() {
|
|
81
|
+
return process.env.SystemRoot || process.env.windir || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveWindowsShellPath(selectedTerminalId) {
|
|
85
|
+
const systemRoot = getSystemRoot();
|
|
86
|
+
|
|
87
|
+
if (selectedTerminalId === 'cmd') {
|
|
88
|
+
const candidates = [];
|
|
89
|
+
if (process.env.COMSPEC) {
|
|
90
|
+
candidates.push(process.env.COMSPEC);
|
|
91
|
+
}
|
|
92
|
+
if (systemRoot) {
|
|
93
|
+
candidates.push(path.join(systemRoot, 'System32', 'cmd.exe'));
|
|
94
|
+
}
|
|
95
|
+
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (selectedTerminalId === 'powershell') {
|
|
99
|
+
const candidates = [];
|
|
100
|
+
if (systemRoot) {
|
|
101
|
+
candidates.push(path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'));
|
|
102
|
+
}
|
|
103
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
104
|
+
candidates.push(path.join(programFiles, 'PowerShell', '7', 'pwsh.exe'));
|
|
105
|
+
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selectedTerminalId === 'git-bash') {
|
|
109
|
+
const terminals = detectAvailableTerminals();
|
|
110
|
+
const gitBash = terminals.find(t => t.id === 'git-bash');
|
|
111
|
+
if (gitBash && gitBash.executablePath && fs.existsSync(gitBash.executablePath)) {
|
|
112
|
+
return gitBash.executablePath;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getWebTerminalShellConfig() {
|
|
120
|
+
const config = loadTerminalConfig();
|
|
121
|
+
const selectedTerminalId = config.selectedTerminal;
|
|
122
|
+
|
|
123
|
+
if (!selectedTerminalId) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (selectedTerminalId === 'system-shell') {
|
|
128
|
+
const shell = getSystemShell();
|
|
129
|
+
if (shell) {
|
|
130
|
+
return { shell };
|
|
131
|
+
}
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (process.platform !== 'win32') {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const shell = resolveWindowsShellPath(selectedTerminalId);
|
|
140
|
+
if (!shell) {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const args = [];
|
|
145
|
+
if (selectedTerminalId === 'git-bash') {
|
|
146
|
+
args.push('--login', '-i');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { shell, args };
|
|
150
|
+
}
|
|
151
|
+
|
|
80
152
|
/**
|
|
81
153
|
* 获取终端启动命令(填充参数后)
|
|
82
154
|
* @param {string} cwd - 工作目录
|
|
@@ -91,7 +163,14 @@ function getTerminalLaunchCommand(cwd, sessionId, toolType, customCliCommand) {
|
|
|
91
163
|
throw new Error('No terminal available');
|
|
92
164
|
}
|
|
93
165
|
|
|
166
|
+
if (terminal.supportsLocalLaunch === false || terminal.id === 'system-shell') {
|
|
167
|
+
throw new Error('系统 Shell 仅用于 Web 终端,请使用 Web 终端启动会话');
|
|
168
|
+
}
|
|
169
|
+
|
|
94
170
|
let command = terminal.command;
|
|
171
|
+
if (!command) {
|
|
172
|
+
throw new Error('未配置终端启动命令');
|
|
173
|
+
}
|
|
95
174
|
|
|
96
175
|
// 根据工具类型构建 CLI 命令
|
|
97
176
|
let cliCommand;
|
|
@@ -136,5 +215,6 @@ module.exports = {
|
|
|
136
215
|
loadTerminalConfig,
|
|
137
216
|
saveTerminalConfig,
|
|
138
217
|
getSelectedTerminal,
|
|
218
|
+
getWebTerminalShellConfig,
|
|
139
219
|
getTerminalLaunchCommand
|
|
140
220
|
};
|