claude-opencode-viewer 2.6.15 → 2.6.17
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/.claude/settings.local.json +5 -1
- package/index-pc.html +4 -3
- package/index.html +13 -10
- package/package.json +1 -1
- package/server.js +125 -33
package/index-pc.html
CHANGED
|
@@ -1217,6 +1217,7 @@
|
|
|
1217
1217
|
|
|
1218
1218
|
ws.onclose = function() {
|
|
1219
1219
|
ws = null;
|
|
1220
|
+
term.reset(); // 清除终端状态,避免 buffer 回放叠加导致状态混乱
|
|
1220
1221
|
setTimeout(connect, 2000);
|
|
1221
1222
|
};
|
|
1222
1223
|
|
|
@@ -1229,7 +1230,7 @@
|
|
|
1229
1230
|
}
|
|
1230
1231
|
}
|
|
1231
1232
|
else if (msg.type === 'exit') {
|
|
1232
|
-
if (!isCreatingNewSession) {
|
|
1233
|
+
if (!isCreatingNewSession && !isTransitioning) {
|
|
1233
1234
|
throttledWrite('\r\n\x1b[33m[进程已退出: ' + msg.exitCode + ']\x1b[0m\r\n');
|
|
1234
1235
|
throttledWrite('\x1b[90m按 Enter 键重新启动 ' + currentMode + '...\x1b[0m\r\n');
|
|
1235
1236
|
}
|
|
@@ -1240,8 +1241,8 @@
|
|
|
1240
1241
|
rebindTouchScroll();
|
|
1241
1242
|
}
|
|
1242
1243
|
else if (msg.type === 'switching') {
|
|
1243
|
-
//
|
|
1244
|
-
term.
|
|
1244
|
+
// 服务端开始切换,完全重置终端(清除残留输出和状态)
|
|
1245
|
+
term.reset();
|
|
1245
1246
|
writeBuffer = '';
|
|
1246
1247
|
}
|
|
1247
1248
|
else if (msg.type === 'state') {
|
package/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
/* 参考 cc-viewer 的 App.jsx 行 1320-1335: 顶部工具栏 */
|
|
23
23
|
#header {
|
|
24
|
-
padding: 10px
|
|
24
|
+
padding: 10px 8px;
|
|
25
25
|
background: #111;
|
|
26
26
|
border-bottom: 1px solid #222;
|
|
27
27
|
display: flex;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
justify-content: space-between;
|
|
30
30
|
flex-shrink: 0;
|
|
31
31
|
height: 40px;
|
|
32
|
-
gap:
|
|
32
|
+
gap: 6px;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
#session-history-bar {
|
|
@@ -679,7 +679,7 @@
|
|
|
679
679
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
680
680
|
<div id="layout">
|
|
681
681
|
<div id="header">
|
|
682
|
-
<div style="display: flex; gap:
|
|
682
|
+
<div style="display: flex; gap: 4px; align-items: center; overflow-x: auto; flex: 1; min-width: 0;">
|
|
683
683
|
<button class="history-toggle-btn" id="new-session-btn" style="color:#73c991; border-color:#2a5a3a;">
|
|
684
684
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
685
685
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
@@ -801,10 +801,10 @@
|
|
|
801
801
|
<div class="virtual-key" data-key="left">←</div>
|
|
802
802
|
<div class="virtual-key" data-key="right">→</div>
|
|
803
803
|
<div class="virtual-key" data-key="enter">Enter</div>
|
|
804
|
+
<div class="virtual-key" data-key="paste">Ctrl+V</div>
|
|
804
805
|
<div class="virtual-key" data-key="tab">Tab</div>
|
|
805
806
|
<div class="virtual-key" data-key="esc">Esc</div>
|
|
806
807
|
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
807
|
-
<div class="virtual-key" data-key="paste">Ctrl+V</div>
|
|
808
808
|
</div>
|
|
809
809
|
</div>
|
|
810
810
|
</div>
|
|
@@ -1044,6 +1044,7 @@
|
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
1046
|
// 触摸滚动实现 - 混合模式:alternate buffer 发鼠标滚轮,normal buffer 用 scrollLines
|
|
1047
|
+
var SCROLL_SENSITIVITY = 0.7; // 滚动灵敏度,越小每次滑动行数越少
|
|
1047
1048
|
var touchScreen = null;
|
|
1048
1049
|
var touchEventsBound = false;
|
|
1049
1050
|
|
|
@@ -1112,7 +1113,7 @@
|
|
|
1112
1113
|
scrollRaf = null;
|
|
1113
1114
|
if (pendingDy === 0) return;
|
|
1114
1115
|
|
|
1115
|
-
pixelAccum += pendingDy;
|
|
1116
|
+
pixelAccum += pendingDy * SCROLL_SENSITIVITY;
|
|
1116
1117
|
pendingDy = 0;
|
|
1117
1118
|
|
|
1118
1119
|
var lh = getLineHeight();
|
|
@@ -1164,7 +1165,7 @@
|
|
|
1164
1165
|
}
|
|
1165
1166
|
|
|
1166
1167
|
if (pendingDy !== 0) {
|
|
1167
|
-
pixelAccum += pendingDy;
|
|
1168
|
+
pixelAccum += pendingDy * SCROLL_SENSITIVITY;
|
|
1168
1169
|
pendingDy = 0;
|
|
1169
1170
|
var lh = getLineHeight();
|
|
1170
1171
|
var lines = Math.trunc(pixelAccum / lh);
|
|
@@ -1189,8 +1190,9 @@
|
|
|
1189
1190
|
}
|
|
1190
1191
|
velocitySamples = [];
|
|
1191
1192
|
|
|
1193
|
+
velocity *= SCROLL_SENSITIVITY;
|
|
1192
1194
|
console.log('[scroll] velocity:', velocity);
|
|
1193
|
-
if (Math.abs(velocity) < 0.
|
|
1195
|
+
if (Math.abs(velocity) < 0.3) return;
|
|
1194
1196
|
|
|
1195
1197
|
var friction = 0.95;
|
|
1196
1198
|
var mAccum = 0;
|
|
@@ -1329,6 +1331,7 @@
|
|
|
1329
1331
|
|
|
1330
1332
|
ws.onclose = function() {
|
|
1331
1333
|
ws = null;
|
|
1334
|
+
term.reset(); // 清除终端状态,避免 buffer 回放叠加导致状态混乱
|
|
1332
1335
|
setTimeout(connect, 2000);
|
|
1333
1336
|
};
|
|
1334
1337
|
|
|
@@ -1341,7 +1344,7 @@
|
|
|
1341
1344
|
}
|
|
1342
1345
|
}
|
|
1343
1346
|
else if (msg.type === 'exit') {
|
|
1344
|
-
if (!isCreatingNewSession) {
|
|
1347
|
+
if (!isCreatingNewSession && !isTransitioning) {
|
|
1345
1348
|
throttledWrite('\r\n\x1b[33m[进程已退出: ' + msg.exitCode + ']\x1b[0m\r\n');
|
|
1346
1349
|
throttledWrite('\x1b[90m按 Enter 键重新启动 ' + currentMode + '...\x1b[0m\r\n');
|
|
1347
1350
|
}
|
|
@@ -1352,8 +1355,8 @@
|
|
|
1352
1355
|
rebindTouchScroll();
|
|
1353
1356
|
}
|
|
1354
1357
|
else if (msg.type === 'switching') {
|
|
1355
|
-
//
|
|
1356
|
-
term.
|
|
1358
|
+
// 服务端开始切换,完全重置终端(清除残留输出和状态)
|
|
1359
|
+
term.reset();
|
|
1357
1360
|
writeBuffer = '';
|
|
1358
1361
|
}
|
|
1359
1362
|
else if (msg.type === 'state') {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -59,6 +59,27 @@ const OPENCODE_DB_PATH = process.env.OPENCODE_DB_PATH || join(
|
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
const MAX_BUFFER = 1024 * 1024; // 1MB
|
|
62
|
+
|
|
63
|
+
// 安全截断:避免从 ANSI 转义序列中间开始导致终端状态紊乱
|
|
64
|
+
function findSafeSliceStart(buf, rawStart) {
|
|
65
|
+
const scanLimit = Math.min(rawStart + 64, buf.length);
|
|
66
|
+
let i = rawStart;
|
|
67
|
+
while (i < scanLimit) {
|
|
68
|
+
const ch = buf.charCodeAt(i);
|
|
69
|
+
if (ch === 0x1b) {
|
|
70
|
+
let j = i + 1;
|
|
71
|
+
while (j < scanLimit && !((buf.charCodeAt(j) >= 0x40 && buf.charCodeAt(j) <= 0x7e) && j > i + 1)) {
|
|
72
|
+
j++;
|
|
73
|
+
}
|
|
74
|
+
if (j < scanLimit) return j + 1;
|
|
75
|
+
i = j;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch >= 0x20 && ch <= 0x3f) { i++; continue; }
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return i < buf.length ? i : rawStart;
|
|
82
|
+
}
|
|
62
83
|
const execFileAsync = promisify(execFile);
|
|
63
84
|
|
|
64
85
|
let ptyModule = null;
|
|
@@ -73,9 +94,11 @@ let lastPtyRows = 30;
|
|
|
73
94
|
|
|
74
95
|
let activeWs = null;
|
|
75
96
|
let currentSessionId = null;
|
|
97
|
+
let previousSessionId = null; // 用于判断新会话是否已写入 DB
|
|
76
98
|
const clientSizes = new Map();
|
|
77
99
|
const mobileClients = new Set();
|
|
78
100
|
let currentMode = 'opencode';
|
|
101
|
+
let isSwitching = false; // 模式切换/恢复会话中,忽略旧进程退出事件
|
|
79
102
|
|
|
80
103
|
function getMobileSize() {
|
|
81
104
|
for (const mws of mobileClients) {
|
|
@@ -194,7 +217,9 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
194
217
|
proc.onData((data) => {
|
|
195
218
|
outputBuffer += data;
|
|
196
219
|
if (outputBuffer.length > MAX_BUFFER) {
|
|
197
|
-
|
|
220
|
+
const rawStart = outputBuffer.length - MAX_BUFFER;
|
|
221
|
+
const safeStart = findSafeSliceStart(outputBuffer, rawStart);
|
|
222
|
+
outputBuffer = outputBuffer.slice(safeStart);
|
|
198
223
|
}
|
|
199
224
|
dataListeners.forEach(cb => cb(data));
|
|
200
225
|
});
|
|
@@ -210,6 +235,8 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
210
235
|
if (opencodeProcess === proc) {
|
|
211
236
|
opencodeProcess = null;
|
|
212
237
|
}
|
|
238
|
+
// 已被替换的旧进程或切换中的进程,不通知前端
|
|
239
|
+
if (isSwitching || currentProcess !== null) return;
|
|
213
240
|
exitListeners.forEach(cb => cb(exitCode || 0));
|
|
214
241
|
});
|
|
215
242
|
|
|
@@ -236,23 +263,8 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
236
263
|
currentSessionId = sessionId;
|
|
237
264
|
console.log(`[session] 当前会话 ID: ${currentSessionId}`);
|
|
238
265
|
} else {
|
|
239
|
-
//
|
|
266
|
+
// 新建会话:保持 null,前端点击复制时不显示内容
|
|
240
267
|
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
268
|
}
|
|
257
269
|
}
|
|
258
270
|
|
|
@@ -266,6 +278,8 @@ async function switchMode(newMode) {
|
|
|
266
278
|
|
|
267
279
|
console.log(`[switchMode] 从 ${currentMode} 切换到 ${newMode}`);
|
|
268
280
|
|
|
281
|
+
isSwitching = true;
|
|
282
|
+
|
|
269
283
|
// 清空所有历史输出
|
|
270
284
|
outputBuffer = '';
|
|
271
285
|
|
|
@@ -296,10 +310,15 @@ async function switchMode(newMode) {
|
|
|
296
310
|
currentMode = newMode;
|
|
297
311
|
try {
|
|
298
312
|
await spawnProcess(newMode);
|
|
313
|
+
// 强制 resize 确保新进程使用正确的终端尺寸
|
|
314
|
+
if (currentProcess && lastPtyCols && lastPtyRows) {
|
|
315
|
+
try { currentProcess.resize(lastPtyCols, lastPtyRows); } catch {}
|
|
316
|
+
}
|
|
299
317
|
console.log(`[switchMode] 切换到 ${newMode} 成功`);
|
|
300
318
|
} catch (e) {
|
|
301
319
|
console.error('[switchMode] 启动新进程失败:', e.message);
|
|
302
320
|
}
|
|
321
|
+
isSwitching = false;
|
|
303
322
|
}
|
|
304
323
|
|
|
305
324
|
function writeToPty(data) {
|
|
@@ -644,13 +663,29 @@ const requestHandler = async (req, res) => {
|
|
|
644
663
|
res.end('Not Found');
|
|
645
664
|
};
|
|
646
665
|
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
666
|
+
const httpsOpts = USE_HTTPS ? await getOrCreateCert() : null;
|
|
667
|
+
let server, wss;
|
|
668
|
+
|
|
669
|
+
function createServerAndWss() {
|
|
670
|
+
server = USE_HTTPS
|
|
671
|
+
? createHttpsServer(httpsOpts, requestHandler)
|
|
672
|
+
: createServer(requestHandler);
|
|
673
|
+
wss = new WebSocketServer({ noServer: true });
|
|
674
|
+
setupWss(wss);
|
|
650
675
|
|
|
651
|
-
|
|
676
|
+
server.on('upgrade', (req, socket, head) => {
|
|
677
|
+
if (req.url === '/ws') {
|
|
678
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
679
|
+
wss.emit('connection', ws, req);
|
|
680
|
+
});
|
|
681
|
+
} else {
|
|
682
|
+
socket.destroy();
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
652
686
|
|
|
653
|
-
|
|
687
|
+
function setupWss(wssInst) {
|
|
688
|
+
wssInst.on('connection', (ws, req) => {
|
|
654
689
|
console.log('[WS] 客户端连接 from', req.socket.remoteAddress);
|
|
655
690
|
|
|
656
691
|
ws.send(JSON.stringify({
|
|
@@ -660,12 +695,34 @@ wss.on('connection', (ws, req) => {
|
|
|
660
695
|
mode: currentMode,
|
|
661
696
|
}));
|
|
662
697
|
|
|
663
|
-
|
|
698
|
+
// 重连时:如果有当前 opencode 会话且不在切换中,重启加载完整会话
|
|
699
|
+
if (!isSwitching && currentMode === 'opencode' && currentProcess && currentSessionId) {
|
|
700
|
+
isSwitching = true;
|
|
701
|
+
const sid = currentSessionId;
|
|
702
|
+
try {
|
|
703
|
+
if (opencodeProcess) {
|
|
704
|
+
opencodeProcess.kill();
|
|
705
|
+
opencodeProcess = null;
|
|
706
|
+
currentProcess = null;
|
|
707
|
+
}
|
|
708
|
+
} catch {}
|
|
709
|
+
outputBuffer = '';
|
|
710
|
+
setTimeout(async () => {
|
|
711
|
+
try {
|
|
712
|
+
await spawnProcess('opencode', sid);
|
|
713
|
+
ws.send(JSON.stringify({ type: 'started', sessionId: sid }));
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error('[reconnect] 重启失败:', e.message);
|
|
716
|
+
}
|
|
717
|
+
isSwitching = false;
|
|
718
|
+
}, 200);
|
|
719
|
+
} else if (outputBuffer) {
|
|
664
720
|
ws.send(JSON.stringify({ type: 'data', data: outputBuffer }));
|
|
665
721
|
}
|
|
666
722
|
|
|
723
|
+
|
|
667
724
|
const listener = (data) => {
|
|
668
|
-
if (ws.readyState === 1) {
|
|
725
|
+
if (ws.readyState === 1 && !isSwitching) {
|
|
669
726
|
ws.send(JSON.stringify({ type: 'data', data }));
|
|
670
727
|
}
|
|
671
728
|
};
|
|
@@ -685,7 +742,8 @@ wss.on('connection', (ws, req) => {
|
|
|
685
742
|
|
|
686
743
|
if (msg.type === 'input') {
|
|
687
744
|
// 进程已退出时,自动重新启动(参考 cc-viewer 逻辑)
|
|
688
|
-
|
|
745
|
+
// 模式切换/新建会话期间不触发 respawn
|
|
746
|
+
if (!currentProcess && !isSwitching) {
|
|
689
747
|
try {
|
|
690
748
|
console.log(`[respawn] 进程已退出,自动重新启动 ${currentMode}`);
|
|
691
749
|
outputBuffer = '';
|
|
@@ -719,6 +777,8 @@ wss.on('connection', (ws, req) => {
|
|
|
719
777
|
if (msg.mode !== currentMode) {
|
|
720
778
|
ws.send(JSON.stringify({ type: 'switching', mode: msg.mode }));
|
|
721
779
|
await switchMode(msg.mode);
|
|
780
|
+
// 切换完成后:先发 reset 清除残留,再发新进程的 buffer
|
|
781
|
+
ws.send(JSON.stringify({ type: 'data', data: '\x1bc' }));
|
|
722
782
|
ws.send(JSON.stringify({ type: 'mode', mode: currentMode }));
|
|
723
783
|
setTimeout(() => {
|
|
724
784
|
if (outputBuffer) {
|
|
@@ -731,6 +791,8 @@ wss.on('connection', (ws, req) => {
|
|
|
731
791
|
if (msg.sessionId && currentMode === 'opencode') {
|
|
732
792
|
console.log(`[restore] 恢复会话: ${msg.sessionId}`);
|
|
733
793
|
|
|
794
|
+
isSwitching = true;
|
|
795
|
+
|
|
734
796
|
// 杀死当前 opencode 进程
|
|
735
797
|
if (opencodeProcess) {
|
|
736
798
|
try {
|
|
@@ -757,6 +819,7 @@ wss.on('connection', (ws, req) => {
|
|
|
757
819
|
console.error('[restore] 启动进程失败:', e.message);
|
|
758
820
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
759
821
|
}
|
|
822
|
+
isSwitching = false;
|
|
760
823
|
}
|
|
761
824
|
} else if (msg.type === 'start') {
|
|
762
825
|
// 前端启动指令:可选带 sessionId 恢复会话
|
|
@@ -775,6 +838,10 @@ wss.on('connection', (ws, req) => {
|
|
|
775
838
|
const mode = currentMode;
|
|
776
839
|
console.log(`[new-session] 开启新会话, mode=${mode}`);
|
|
777
840
|
|
|
841
|
+
isSwitching = true;
|
|
842
|
+
previousSessionId = currentSessionId; // 记住旧会话,用于判断新会话是否已写入 DB
|
|
843
|
+
currentSessionId = null; // 立即清除旧会话 ID
|
|
844
|
+
|
|
778
845
|
if (mode === 'opencode' && opencodeProcess) {
|
|
779
846
|
try { opencodeProcess.kill(); } catch {}
|
|
780
847
|
opencodeProcess = null;
|
|
@@ -796,6 +863,29 @@ wss.on('connection', (ws, req) => {
|
|
|
796
863
|
console.error('[new-session] 启动失败:', e.message);
|
|
797
864
|
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
798
865
|
}
|
|
866
|
+
isSwitching = false;
|
|
867
|
+
|
|
868
|
+
// 延迟检测新会话 ID(等 opencode 写入 DB)
|
|
869
|
+
const prevSid = previousSessionId;
|
|
870
|
+
const checkNewSession = (attempt) => {
|
|
871
|
+
if (currentSessionId || attempt > 10) return;
|
|
872
|
+
try {
|
|
873
|
+
if (existsSync(OPENCODE_DB_PATH)) {
|
|
874
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
875
|
+
const row = db.prepare(
|
|
876
|
+
`SELECT id FROM session WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_created DESC LIMIT 1`
|
|
877
|
+
).get();
|
|
878
|
+
db.close();
|
|
879
|
+
if (row && row.id !== prevSid) {
|
|
880
|
+
currentSessionId = row.id;
|
|
881
|
+
console.log(`[new-session] 检测到新会话 ID: ${currentSessionId}`);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} catch {}
|
|
886
|
+
setTimeout(() => checkNewSession(attempt + 1), 2000);
|
|
887
|
+
};
|
|
888
|
+
setTimeout(() => checkNewSession(0), 3000);
|
|
799
889
|
}
|
|
800
890
|
} catch (err) {
|
|
801
891
|
console.error('[WS] Error:', err.message);
|
|
@@ -826,14 +916,17 @@ wss.on('connection', (ws, req) => {
|
|
|
826
916
|
}
|
|
827
917
|
});
|
|
828
918
|
});
|
|
919
|
+
} // end setupWss
|
|
920
|
+
|
|
921
|
+
createServerAndWss();
|
|
829
922
|
|
|
830
923
|
process.on('SIGINT', () => process.exit(0));
|
|
831
924
|
process.on('SIGTERM', () => process.exit(0));
|
|
832
925
|
|
|
833
|
-
//
|
|
834
|
-
const
|
|
835
|
-
const
|
|
836
|
-
const MAX_PORT_RETRIES =
|
|
926
|
+
// 端口范围,用于端口冲突重试
|
|
927
|
+
const PORT_MIN = IS_PC ? 19200 : 7008;
|
|
928
|
+
const PORT_MAX = IS_PC ? 19220 : 7028;
|
|
929
|
+
const MAX_PORT_RETRIES = 20;
|
|
837
930
|
|
|
838
931
|
function startServer(retries = 0) {
|
|
839
932
|
server.listen(PORT, '0.0.0.0', async () => {
|
|
@@ -873,12 +966,11 @@ function startServer(retries = 0) {
|
|
|
873
966
|
});
|
|
874
967
|
|
|
875
968
|
server.on('error', (err) => {
|
|
876
|
-
if (err.code === 'EADDRINUSE' &&
|
|
877
|
-
// PC 模式端口冲突,顺序查找下一个可用端口
|
|
969
|
+
if (err.code === 'EADDRINUSE' && retries < MAX_PORT_RETRIES) {
|
|
878
970
|
const oldPort = PORT;
|
|
879
|
-
PORT = PORT >=
|
|
971
|
+
PORT = PORT >= PORT_MAX ? PORT_MIN : PORT + 1;
|
|
880
972
|
console.error(`[port] ${oldPort} 已占用,尝试 ${PORT} (${retries + 1}/${MAX_PORT_RETRIES})`);
|
|
881
|
-
|
|
973
|
+
createServerAndWss();
|
|
882
974
|
startServer(retries + 1);
|
|
883
975
|
} else {
|
|
884
976
|
console.error(`启动失败: ${err.message}`);
|