claude-opencode-viewer 2.6.12 → 2.6.13
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/index.html +162 -91
- package/package.json +1 -1
- package/server.js +76 -19
package/index.html
CHANGED
|
@@ -351,71 +351,94 @@
|
|
|
351
351
|
color: #fff;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
/*
|
|
355
|
-
#
|
|
356
|
-
visibility: hidden;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
#select-text-layer {
|
|
354
|
+
/* 消息查看器 */
|
|
355
|
+
#message-viewer {
|
|
360
356
|
display: none;
|
|
361
|
-
position:
|
|
357
|
+
position: fixed;
|
|
362
358
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
363
|
-
overflow-y: auto;
|
|
364
|
-
-webkit-overflow-scrolling: touch;
|
|
365
359
|
background: #0a0a0a;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
z-index: 10;
|
|
360
|
+
z-index: 1000;
|
|
361
|
+
flex-direction: column;
|
|
369
362
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
display: block;
|
|
363
|
+
#message-viewer.visible {
|
|
364
|
+
display: flex;
|
|
373
365
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
padding: 6px 0;
|
|
383
|
-
border-bottom: 1px solid #333;
|
|
384
|
-
z-index: 1;
|
|
385
|
-
-webkit-user-select: none;
|
|
386
|
-
user-select: none;
|
|
366
|
+
#msg-viewer-header {
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
justify-content: space-between;
|
|
370
|
+
padding: 10px 14px;
|
|
371
|
+
background: #111;
|
|
372
|
+
border-bottom: 1px solid #222;
|
|
373
|
+
flex-shrink: 0;
|
|
387
374
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
393
|
-
font-size: 11px;
|
|
394
|
-
line-height: 1.4;
|
|
395
|
-
white-space: pre-wrap;
|
|
396
|
-
word-break: break-all;
|
|
397
|
-
-webkit-user-select: text;
|
|
398
|
-
user-select: text;
|
|
375
|
+
#msg-viewer-header span {
|
|
376
|
+
color: #ccc;
|
|
377
|
+
font-size: 15px;
|
|
378
|
+
font-weight: 500;
|
|
399
379
|
}
|
|
400
|
-
|
|
401
|
-
#select-mode-close {
|
|
402
|
-
position: absolute;
|
|
403
|
-
top: 6px;
|
|
404
|
-
right: 6px;
|
|
405
|
-
z-index: 20;
|
|
406
|
-
display: none;
|
|
380
|
+
#msg-viewer-close {
|
|
407
381
|
background: rgba(50,50,50,0.9);
|
|
408
382
|
border: 1px solid #555;
|
|
409
383
|
color: #ccc;
|
|
410
|
-
width:
|
|
411
|
-
height:
|
|
384
|
+
width: 30px;
|
|
385
|
+
height: 30px;
|
|
412
386
|
border-radius: 50%;
|
|
413
|
-
font-size:
|
|
414
|
-
line-height:
|
|
387
|
+
font-size: 15px;
|
|
388
|
+
line-height: 28px;
|
|
415
389
|
text-align: center;
|
|
416
390
|
cursor: pointer;
|
|
417
|
-
|
|
418
|
-
|
|
391
|
+
}
|
|
392
|
+
#msg-viewer-content {
|
|
393
|
+
flex: 1;
|
|
394
|
+
overflow-y: auto;
|
|
395
|
+
-webkit-overflow-scrolling: touch;
|
|
396
|
+
padding: 12px;
|
|
397
|
+
}
|
|
398
|
+
.msg-item {
|
|
399
|
+
margin-bottom: 16px;
|
|
400
|
+
padding: 10px 12px;
|
|
401
|
+
border-radius: 8px;
|
|
402
|
+
border: 1px solid #222;
|
|
403
|
+
}
|
|
404
|
+
.msg-user {
|
|
405
|
+
background: #1a2332;
|
|
406
|
+
border-color: #2a4a7c;
|
|
407
|
+
}
|
|
408
|
+
.msg-assistant {
|
|
409
|
+
background: #1a2e1a;
|
|
410
|
+
border-color: #2a5a3a;
|
|
411
|
+
}
|
|
412
|
+
.msg-role {
|
|
413
|
+
font-size: 11px;
|
|
414
|
+
color: #888;
|
|
415
|
+
font-weight: 600;
|
|
416
|
+
text-transform: uppercase;
|
|
417
|
+
margin-bottom: 6px;
|
|
418
|
+
}
|
|
419
|
+
.msg-text {
|
|
420
|
+
color: #ddd;
|
|
421
|
+
font-size: 13px;
|
|
422
|
+
line-height: 1.6;
|
|
423
|
+
white-space: pre-wrap;
|
|
424
|
+
word-break: break-word;
|
|
425
|
+
-webkit-user-select: text;
|
|
426
|
+
user-select: text;
|
|
427
|
+
}
|
|
428
|
+
.msg-tool {
|
|
429
|
+
margin-top: 6px;
|
|
430
|
+
padding: 6px 8px;
|
|
431
|
+
background: #1a1a0a;
|
|
432
|
+
border: 1px solid #333;
|
|
433
|
+
border-radius: 4px;
|
|
434
|
+
font-size: 11px;
|
|
435
|
+
color: #f0ad4e;
|
|
436
|
+
}
|
|
437
|
+
.msg-empty {
|
|
438
|
+
text-align: center;
|
|
439
|
+
padding: 40px 20px;
|
|
440
|
+
color: #666;
|
|
441
|
+
font-size: 13px;
|
|
419
442
|
}
|
|
420
443
|
|
|
421
444
|
/* 复制成功提示 */
|
|
@@ -765,11 +788,6 @@
|
|
|
765
788
|
<div id="content">
|
|
766
789
|
<div id="terminal-container">
|
|
767
790
|
<div id="terminal">
|
|
768
|
-
<div id="select-text-layer">
|
|
769
|
-
<div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
|
|
770
|
-
<pre id="select-text-pre"></pre>
|
|
771
|
-
</div>
|
|
772
|
-
<button id="select-mode-close">✕</button>
|
|
773
791
|
</div>
|
|
774
792
|
<div id="virtual-keybar">
|
|
775
793
|
<div class="virtual-key" data-key="up">↑</div>
|
|
@@ -785,6 +803,16 @@
|
|
|
785
803
|
</div>
|
|
786
804
|
</div>
|
|
787
805
|
|
|
806
|
+
<div id="message-viewer">
|
|
807
|
+
<div id="msg-viewer-header">
|
|
808
|
+
<span>会话消息</span>
|
|
809
|
+
<button id="msg-viewer-close">✕</button>
|
|
810
|
+
</div>
|
|
811
|
+
<div id="msg-viewer-content">
|
|
812
|
+
<div class="msg-empty">加载中...</div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
788
816
|
<div id="copy-toast">已复制</div>
|
|
789
817
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
790
818
|
<script>
|
|
@@ -931,7 +959,7 @@
|
|
|
931
959
|
onVVChange();
|
|
932
960
|
}
|
|
933
961
|
|
|
934
|
-
//
|
|
962
|
+
// 移动端固定尺寸计算:基于 #terminal 元素实际高度,确保终端与按钮栏齐平
|
|
935
963
|
function mobileFixedResize() {
|
|
936
964
|
if (!term) return;
|
|
937
965
|
var cellDims = getCellDims();
|
|
@@ -941,12 +969,9 @@
|
|
|
941
969
|
}
|
|
942
970
|
|
|
943
971
|
var padX = 16;
|
|
944
|
-
var
|
|
945
|
-
var topBarHeight = 40;
|
|
946
|
-
var keybarHeight = 52;
|
|
947
|
-
|
|
972
|
+
var termEl = document.getElementById('terminal');
|
|
948
973
|
var availW = window.innerWidth - padX;
|
|
949
|
-
var availH =
|
|
974
|
+
var availH = termEl ? termEl.clientHeight : 300;
|
|
950
975
|
|
|
951
976
|
var currentFontSize = term.options.fontSize;
|
|
952
977
|
var currentCharWidth = cellDims.width;
|
|
@@ -1095,7 +1120,10 @@
|
|
|
1095
1120
|
// 长按检测
|
|
1096
1121
|
var longPressTimer = null;
|
|
1097
1122
|
var longPressTriggered = false;
|
|
1098
|
-
var LONG_PRESS_DELAY =
|
|
1123
|
+
var LONG_PRESS_DELAY = 800; // ms
|
|
1124
|
+
var LONG_PRESS_MOVE_THRESHOLD = 10; // px,移动超过此距离取消长按
|
|
1125
|
+
var touchStartX = 0;
|
|
1126
|
+
var touchStartY = 0;
|
|
1099
1127
|
|
|
1100
1128
|
function clearLongPress() {
|
|
1101
1129
|
if (longPressTimer) {
|
|
@@ -1116,6 +1144,8 @@
|
|
|
1116
1144
|
lastY = e.touches[0].clientY;
|
|
1117
1145
|
lastTime = performance.now();
|
|
1118
1146
|
velocitySamples = [];
|
|
1147
|
+
touchStartX = e.touches[0].clientX;
|
|
1148
|
+
touchStartY = e.touches[0].clientY;
|
|
1119
1149
|
|
|
1120
1150
|
// 启动长按计时器
|
|
1121
1151
|
// 在长按检测期间阻止 xterm textarea 获取焦点,防止弹出键盘
|
|
@@ -1127,14 +1157,18 @@
|
|
|
1127
1157
|
longPressTimer = setTimeout(function() {
|
|
1128
1158
|
longPressTriggered = true;
|
|
1129
1159
|
longPressTimer = null;
|
|
1130
|
-
|
|
1160
|
+
openMessageViewer();
|
|
1131
1161
|
}, LONG_PRESS_DELAY);
|
|
1132
1162
|
}
|
|
1133
1163
|
|
|
1134
1164
|
function handleTouchMove(e) {
|
|
1135
1165
|
if (e.touches.length !== 1) return;
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1166
|
+
// 移动超过阈值才取消长按(容忍手指微小抖动)
|
|
1167
|
+
var dx = e.touches[0].clientX - touchStartX;
|
|
1168
|
+
var dy2 = e.touches[0].clientY - touchStartY;
|
|
1169
|
+
if (Math.abs(dx) > LONG_PRESS_MOVE_THRESHOLD || Math.abs(dy2) > LONG_PRESS_MOVE_THRESHOLD) {
|
|
1170
|
+
clearLongPress();
|
|
1171
|
+
}
|
|
1138
1172
|
var y = e.touches[0].clientY;
|
|
1139
1173
|
var now = performance.now();
|
|
1140
1174
|
var dt = now - lastTime;
|
|
@@ -1818,47 +1852,84 @@
|
|
|
1818
1852
|
}
|
|
1819
1853
|
|
|
1820
1854
|
|
|
1821
|
-
//
|
|
1822
|
-
var
|
|
1823
|
-
var
|
|
1824
|
-
var
|
|
1825
|
-
var inSelectMode = false;
|
|
1855
|
+
// 长按打开消息查看器
|
|
1856
|
+
var messageViewer = document.getElementById('message-viewer');
|
|
1857
|
+
var msgViewerContent = document.getElementById('msg-viewer-content');
|
|
1858
|
+
var msgViewerClose = document.getElementById('msg-viewer-close');
|
|
1826
1859
|
|
|
1827
|
-
function
|
|
1828
|
-
if (inSelectMode) return;
|
|
1829
|
-
inSelectMode = true;
|
|
1860
|
+
function openMessageViewer() {
|
|
1830
1861
|
// 收起键盘
|
|
1831
1862
|
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1832
1863
|
if (xtermTa) xtermTa.blur();
|
|
1833
1864
|
document.activeElement && document.activeElement.blur();
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
selectTextLayer.classList.add('visible');
|
|
1838
|
-
selectModeClose.style.display = 'block';
|
|
1865
|
+
|
|
1866
|
+
messageViewer.classList.add('visible');
|
|
1867
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
|
|
1839
1868
|
unbindTouchScroll();
|
|
1869
|
+
|
|
1870
|
+
// 获取当前会话 ID
|
|
1871
|
+
fetch(basePath + '/api/current-session')
|
|
1872
|
+
.then(function(r) { return r.json(); })
|
|
1873
|
+
.then(function(data) {
|
|
1874
|
+
if (!data.sessionId) {
|
|
1875
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
return fetch(basePath + '/api/session/' + data.sessionId)
|
|
1879
|
+
.then(function(r) { return r.json(); })
|
|
1880
|
+
.then(function(messages) {
|
|
1881
|
+
renderMessages(messages);
|
|
1882
|
+
});
|
|
1883
|
+
})
|
|
1884
|
+
.catch(function(e) {
|
|
1885
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function renderMessages(messages) {
|
|
1890
|
+
if (!messages || messages.length === 0) {
|
|
1891
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无消息</div>';
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
var html = '';
|
|
1895
|
+
messages.forEach(function(msg) {
|
|
1896
|
+
var role = msg.role || 'unknown';
|
|
1897
|
+
var roleLabel = role === 'user' ? '用户' : role === 'assistant' ? '助手' : role;
|
|
1898
|
+
var cls = role === 'user' ? 'msg-user' : role === 'assistant' ? 'msg-assistant' : '';
|
|
1899
|
+
html += '<div class="msg-item ' + cls + '">';
|
|
1900
|
+
html += '<div class="msg-role">' + roleLabel + '</div>';
|
|
1901
|
+
if (msg.text) {
|
|
1902
|
+
html += '<div class="msg-text">' + escapeHtml(msg.text) + '</div>';
|
|
1903
|
+
}
|
|
1904
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
1905
|
+
msg.toolCalls.forEach(function(tc) {
|
|
1906
|
+
html += '<div class="msg-tool">🔧 ' + escapeHtml(tc.name || 'tool') + '</div>';
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
html += '</div>';
|
|
1910
|
+
});
|
|
1911
|
+
msgViewerContent.innerHTML = html;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function escapeHtml(str) {
|
|
1915
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1840
1916
|
}
|
|
1841
1917
|
|
|
1842
|
-
function
|
|
1843
|
-
|
|
1844
|
-
inSelectMode = false;
|
|
1845
|
-
terminalEl.classList.remove('select-mode');
|
|
1846
|
-
selectTextLayer.classList.remove('visible');
|
|
1847
|
-
selectModeClose.style.display = 'none';
|
|
1848
|
-
window.getSelection().removeAllRanges();
|
|
1918
|
+
function closeMessageViewer() {
|
|
1919
|
+
messageViewer.classList.remove('visible');
|
|
1849
1920
|
rebindTouchScroll();
|
|
1850
1921
|
}
|
|
1851
1922
|
|
|
1852
|
-
|
|
1923
|
+
msgViewerClose.addEventListener('click', function(e) {
|
|
1853
1924
|
e.preventDefault();
|
|
1854
1925
|
e.stopPropagation();
|
|
1855
|
-
|
|
1926
|
+
closeMessageViewer();
|
|
1856
1927
|
});
|
|
1857
1928
|
|
|
1858
|
-
|
|
1929
|
+
msgViewerClose.addEventListener('touchend', function(e) {
|
|
1859
1930
|
e.preventDefault();
|
|
1860
1931
|
e.stopPropagation();
|
|
1861
|
-
|
|
1932
|
+
closeMessageViewer();
|
|
1862
1933
|
});
|
|
1863
1934
|
|
|
1864
1935
|
// ======= Git Diff 功能 =======
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import Database from 'better-sqlite3';
|
|
|
15
15
|
process.title = 'claude-opencode-viewer';
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
|
|
18
|
+
let PORT = parseInt(process.argv[2]) || 7008;
|
|
19
19
|
const IS_PC = process.argv.includes('--pc');
|
|
20
20
|
const USE_HTTPS = process.argv.includes('--https');
|
|
21
21
|
const JSON_OUTPUT = process.argv.includes('--json');
|
|
@@ -72,6 +72,7 @@ let lastPtyCols = 120;
|
|
|
72
72
|
let lastPtyRows = 30;
|
|
73
73
|
|
|
74
74
|
let activeWs = null;
|
|
75
|
+
let currentSessionId = null;
|
|
75
76
|
const clientSizes = new Map();
|
|
76
77
|
const mobileClients = new Set();
|
|
77
78
|
let currentMode = 'opencode';
|
|
@@ -229,6 +230,30 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
229
230
|
} catch {}
|
|
230
231
|
}
|
|
231
232
|
opencodeProcess = proc;
|
|
233
|
+
|
|
234
|
+
// 追踪当前会话 ID
|
|
235
|
+
if (sessionId) {
|
|
236
|
+
currentSessionId = sessionId;
|
|
237
|
+
console.log(`[session] 当前会话 ID: ${currentSessionId}`);
|
|
238
|
+
} else {
|
|
239
|
+
// 新建会话:延迟查数据库获取最新 session ID
|
|
240
|
+
currentSessionId = null;
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
try {
|
|
243
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
244
|
+
const row = db.prepare(
|
|
245
|
+
`SELECT id FROM session WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_created DESC LIMIT 1`
|
|
246
|
+
).get();
|
|
247
|
+
db.close();
|
|
248
|
+
if (row) {
|
|
249
|
+
currentSessionId = row.id;
|
|
250
|
+
console.log(`[session] 检测到新会话 ID: ${currentSessionId}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.log('[session] 查询新会话 ID 失败:', e.message);
|
|
254
|
+
}
|
|
255
|
+
}, 3000);
|
|
256
|
+
}
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
currentProcess = proc;
|
|
@@ -479,6 +504,16 @@ const requestHandler = async (req, res) => {
|
|
|
479
504
|
return;
|
|
480
505
|
}
|
|
481
506
|
|
|
507
|
+
// API: 获取当前会话 ID
|
|
508
|
+
if (req.url === '/api/current-session') {
|
|
509
|
+
res.writeHead(200, {
|
|
510
|
+
'Content-Type': 'application/json',
|
|
511
|
+
'Access-Control-Allow-Origin': '*',
|
|
512
|
+
});
|
|
513
|
+
res.end(JSON.stringify({ sessionId: currentSessionId }));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
482
517
|
// API: 获取 git status
|
|
483
518
|
if (req.url === '/api/git-status') {
|
|
484
519
|
res.writeHead(200, {
|
|
@@ -795,23 +830,45 @@ wss.on('connection', (ws, req) => {
|
|
|
795
830
|
process.on('SIGINT', () => process.exit(0));
|
|
796
831
|
process.on('SIGTERM', () => process.exit(0));
|
|
797
832
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
833
|
+
// PC 模式端口范围,用于端口冲突重试
|
|
834
|
+
const PC_PORT_MIN = 19200;
|
|
835
|
+
const PC_PORT_MAX = 19220;
|
|
836
|
+
const MAX_PORT_RETRIES = 10;
|
|
801
837
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
console.log('\n' + '='.repeat(50));
|
|
807
|
-
console.log('✅ Claude OpenCode Viewer 已启动');
|
|
808
|
-
console.log('='.repeat(50));
|
|
809
|
-
console.log(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
|
|
810
|
-
console.log(`📱 手机访问:${proto}://${ip}:${PORT}`);
|
|
811
|
-
if (USE_HTTPS) console.log('🔐 HTTPS 模式(首次访问需信任自签名证书)');
|
|
812
|
-
console.log('='.repeat(50));
|
|
813
|
-
console.log('\n按 Ctrl+C 停止服务\n');
|
|
814
|
-
}
|
|
838
|
+
function startServer(retries = 0) {
|
|
839
|
+
server.listen(PORT, '0.0.0.0', async () => {
|
|
840
|
+
const ip = getLocalIp();
|
|
841
|
+
const proto = USE_HTTPS ? 'https' : 'http';
|
|
815
842
|
|
|
816
|
-
|
|
817
|
-
});
|
|
843
|
+
if (JSON_OUTPUT) {
|
|
844
|
+
console.log(JSON.stringify({ port: PORT, url: `${proto}://127.0.0.1:${PORT}`, ip, proto, pid: process.pid }));
|
|
845
|
+
} else {
|
|
846
|
+
console.log('\n' + '='.repeat(50));
|
|
847
|
+
console.log('✅ Claude OpenCode Viewer 已启动');
|
|
848
|
+
console.log('='.repeat(50));
|
|
849
|
+
console.log(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
|
|
850
|
+
console.log(`📱 手机访问:${proto}://${ip}:${PORT}`);
|
|
851
|
+
if (USE_HTTPS) console.log('🔐 HTTPS 模式(首次访问需信任自签名证书)');
|
|
852
|
+
console.log('='.repeat(50));
|
|
853
|
+
console.log('\n按 Ctrl+C 停止服务\n');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
await spawnProcess('opencode');
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
server.on('error', (err) => {
|
|
860
|
+
if (err.code === 'EADDRINUSE' && IS_PC && retries < MAX_PORT_RETRIES) {
|
|
861
|
+
// PC 模式端口冲突,顺序查找下一个可用端口
|
|
862
|
+
const oldPort = PORT;
|
|
863
|
+
PORT = PORT >= PC_PORT_MAX ? PC_PORT_MIN : PORT + 1;
|
|
864
|
+
console.error(`[port] ${oldPort} 已占用,尝试 ${PORT} (${retries + 1}/${MAX_PORT_RETRIES})`);
|
|
865
|
+
server.removeAllListeners('error');
|
|
866
|
+
startServer(retries + 1);
|
|
867
|
+
} else {
|
|
868
|
+
console.error(`启动失败: ${err.message}`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
startServer();
|