claude-opencode-viewer 2.6.25 → 2.6.27
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 +2 -1
- package/index-pc.html +233 -21
- package/index.html +235 -21
- package/package.json +1 -1
- package/server.js +137 -22
package/index-pc.html
CHANGED
|
@@ -544,6 +544,96 @@
|
|
|
544
544
|
text-align: center;
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
/* 文档面板 */
|
|
548
|
+
#docs-bar {
|
|
549
|
+
display: none;
|
|
550
|
+
position: absolute;
|
|
551
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
552
|
+
background: #0a0a0a;
|
|
553
|
+
z-index: 1000;
|
|
554
|
+
flex-direction: column;
|
|
555
|
+
}
|
|
556
|
+
#docs-bar.visible { display: flex; }
|
|
557
|
+
#docs-header {
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
justify-content: space-between;
|
|
561
|
+
padding: 12px 16px;
|
|
562
|
+
background: #111;
|
|
563
|
+
border-bottom: 1px solid #222;
|
|
564
|
+
flex-shrink: 0;
|
|
565
|
+
}
|
|
566
|
+
#docs-title {
|
|
567
|
+
font-size: 14px;
|
|
568
|
+
color: #ddd;
|
|
569
|
+
font-weight: 600;
|
|
570
|
+
}
|
|
571
|
+
.docs-file-list {
|
|
572
|
+
height: 250px;
|
|
573
|
+
flex-shrink: 0;
|
|
574
|
+
overflow-y: auto;
|
|
575
|
+
border-bottom: 1px solid #2a2a2a;
|
|
576
|
+
}
|
|
577
|
+
.docs-file-item {
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
padding: 8px 12px;
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
color: #ccc;
|
|
583
|
+
font-size: 13px;
|
|
584
|
+
gap: 8px;
|
|
585
|
+
}
|
|
586
|
+
.docs-file-item:hover { background: #1a1a1a; }
|
|
587
|
+
.docs-file-item.active {
|
|
588
|
+
background: rgba(197, 134, 192, 0.12);
|
|
589
|
+
color: #fff;
|
|
590
|
+
}
|
|
591
|
+
.docs-file-name {
|
|
592
|
+
overflow: hidden;
|
|
593
|
+
text-overflow: ellipsis;
|
|
594
|
+
flex: 1;
|
|
595
|
+
font-family: Menlo, Monaco, monospace;
|
|
596
|
+
font-size: 12px;
|
|
597
|
+
}
|
|
598
|
+
.docs-file-time {
|
|
599
|
+
color: #666;
|
|
600
|
+
font-size: 11px;
|
|
601
|
+
flex-shrink: 0;
|
|
602
|
+
}
|
|
603
|
+
.docs-content-area {
|
|
604
|
+
flex: 1;
|
|
605
|
+
display: flex;
|
|
606
|
+
flex-direction: column;
|
|
607
|
+
min-height: 0;
|
|
608
|
+
overflow: auto;
|
|
609
|
+
padding: 16px;
|
|
610
|
+
}
|
|
611
|
+
.docs-content-area pre {
|
|
612
|
+
margin: 0;
|
|
613
|
+
white-space: pre-wrap;
|
|
614
|
+
word-break: break-word;
|
|
615
|
+
font-family: Menlo, Monaco, monospace;
|
|
616
|
+
font-size: 13px;
|
|
617
|
+
line-height: 1.6;
|
|
618
|
+
color: #d4d4d4;
|
|
619
|
+
}
|
|
620
|
+
.docs-placeholder {
|
|
621
|
+
flex: 1;
|
|
622
|
+
display: flex;
|
|
623
|
+
flex-direction: column;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
gap: 12px;
|
|
627
|
+
color: #333;
|
|
628
|
+
font-size: 13px;
|
|
629
|
+
}
|
|
630
|
+
.docs-loading {
|
|
631
|
+
text-align: center;
|
|
632
|
+
padding: 16px;
|
|
633
|
+
color: #888;
|
|
634
|
+
font-size: 12px;
|
|
635
|
+
}
|
|
636
|
+
|
|
547
637
|
/* Unified diff 行 */
|
|
548
638
|
.diff-table {
|
|
549
639
|
width: 100%;
|
|
@@ -638,6 +728,15 @@
|
|
|
638
728
|
</svg>
|
|
639
729
|
<span>Diff</span>
|
|
640
730
|
</button>
|
|
731
|
+
<button class="history-toggle-btn" id="docs-toggle" style="color:#c586c0; border-color:#4a2a5a;">
|
|
732
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
733
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
734
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
735
|
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
736
|
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
737
|
+
</svg>
|
|
738
|
+
<span>文档</span>
|
|
739
|
+
</button>
|
|
641
740
|
</div>
|
|
642
741
|
<div id="mode-switcher">
|
|
643
742
|
<span id="mode-label"></span>
|
|
@@ -720,6 +819,43 @@
|
|
|
720
819
|
</div>
|
|
721
820
|
</div>
|
|
722
821
|
|
|
822
|
+
<!-- 文档栏 -->
|
|
823
|
+
<div id="docs-bar">
|
|
824
|
+
<div id="docs-header">
|
|
825
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
826
|
+
<span id="docs-title">文档</span>
|
|
827
|
+
<span class="diff-file-count" id="docs-count">0</span>
|
|
828
|
+
</div>
|
|
829
|
+
<div style="display: flex; gap: 8px;">
|
|
830
|
+
<button class="history-toggle-btn" id="refresh-docs" style="padding: 4px 8px;">
|
|
831
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
|
832
|
+
<polyline points="23 4 23 10 17 10"></polyline>
|
|
833
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
834
|
+
</svg>
|
|
835
|
+
<span>刷新</span>
|
|
836
|
+
</button>
|
|
837
|
+
<button class="history-toggle-btn" id="close-docs" style="padding: 4px 8px;">
|
|
838
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
|
839
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
840
|
+
</svg>
|
|
841
|
+
<span>返回</span>
|
|
842
|
+
</button>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="docs-file-list" id="docs-file-list">
|
|
846
|
+
<div class="docs-loading">加载中...</div>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="docs-content-area" id="docs-content-area">
|
|
849
|
+
<div class="docs-placeholder">
|
|
850
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">
|
|
851
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
852
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
853
|
+
</svg>
|
|
854
|
+
<span>点击文件查看内容</span>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
|
|
723
859
|
<div id="content">
|
|
724
860
|
<div id="terminal-container">
|
|
725
861
|
<div id="terminal">
|
|
@@ -1239,8 +1375,8 @@
|
|
|
1239
1375
|
}
|
|
1240
1376
|
else if (msg.type === 'exit') {
|
|
1241
1377
|
if (!isCreatingNewSession && !isTransitioning) {
|
|
1242
|
-
throttledWrite('\r\n
|
|
1243
|
-
throttledWrite('
|
|
1378
|
+
throttledWrite('\r\n[进程已退出: ' + msg.exitCode + ']\r\n');
|
|
1379
|
+
throttledWrite('按 Enter 键重新启动 ' + currentMode + '...\r\n');
|
|
1244
1380
|
}
|
|
1245
1381
|
}
|
|
1246
1382
|
else if (msg.type === 'mode') {
|
|
@@ -1261,25 +1397,23 @@
|
|
|
1261
1397
|
}
|
|
1262
1398
|
}
|
|
1263
1399
|
else if (msg.type === 'restored') {
|
|
1264
|
-
//
|
|
1265
|
-
term.
|
|
1266
|
-
term.write('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\r\n');
|
|
1267
|
-
term.write('\r\n');
|
|
1400
|
+
// 会话恢复成功,重置终端清除残留ANSI状态
|
|
1401
|
+
term.reset();
|
|
1268
1402
|
}
|
|
1269
1403
|
else if (msg.type === 'restore-error') {
|
|
1270
1404
|
// 恢复失败
|
|
1271
|
-
term.write('
|
|
1405
|
+
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1272
1406
|
}
|
|
1273
1407
|
else if (msg.type === 'started') {
|
|
1274
1408
|
rebindTouchScroll();
|
|
1275
1409
|
}
|
|
1276
1410
|
else if (msg.type === 'new-session-ok') {
|
|
1277
1411
|
isCreatingNewSession = false;
|
|
1278
|
-
term.
|
|
1412
|
+
term.reset();
|
|
1279
1413
|
}
|
|
1280
1414
|
else if (msg.type === 'new-session-error') {
|
|
1281
1415
|
isCreatingNewSession = false;
|
|
1282
|
-
term.write('
|
|
1416
|
+
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1283
1417
|
}
|
|
1284
1418
|
} catch(err) {}
|
|
1285
1419
|
};
|
|
@@ -1480,25 +1614,17 @@
|
|
|
1480
1614
|
// 关闭历史栏
|
|
1481
1615
|
toggleHistoryBar();
|
|
1482
1616
|
|
|
1483
|
-
//
|
|
1484
|
-
term.
|
|
1485
|
-
|
|
1486
|
-
// 显示正在恢复的提示
|
|
1487
|
-
term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
|
|
1488
|
-
term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
|
|
1489
|
-
term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
|
|
1490
|
-
term.write('\r\n');
|
|
1617
|
+
// 重置终端(清除残留ANSI解析状态)
|
|
1618
|
+
term.reset();
|
|
1491
1619
|
|
|
1492
1620
|
if (ws && ws.readyState === 1) {
|
|
1493
1621
|
if (currentMode !== 'opencode') {
|
|
1494
|
-
term.write('
|
|
1622
|
+
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
1495
1623
|
return;
|
|
1496
1624
|
}
|
|
1497
|
-
term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + session.id + '\x1b[0m\r\n');
|
|
1498
|
-
term.write('\r\n');
|
|
1499
1625
|
ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
|
|
1500
1626
|
} else {
|
|
1501
|
-
term.write('
|
|
1627
|
+
term.write('错误: WebSocket 未连接\r\n');
|
|
1502
1628
|
}
|
|
1503
1629
|
}
|
|
1504
1630
|
|
|
@@ -1818,6 +1944,92 @@
|
|
|
1818
1944
|
'</svg><span>点击文件查看 diff</span></div>';
|
|
1819
1945
|
});
|
|
1820
1946
|
|
|
1947
|
+
// === 文档面板 ===
|
|
1948
|
+
var docsBarVisible = false;
|
|
1949
|
+
var docsSelectedFile = null;
|
|
1950
|
+
|
|
1951
|
+
function toggleDocsBar() {
|
|
1952
|
+
docsBarVisible = !docsBarVisible;
|
|
1953
|
+
var bar = document.getElementById('docs-bar');
|
|
1954
|
+
if (docsBarVisible) {
|
|
1955
|
+
bar.classList.add('visible');
|
|
1956
|
+
loadDocs();
|
|
1957
|
+
} else {
|
|
1958
|
+
bar.classList.remove('visible');
|
|
1959
|
+
docsSelectedFile = null;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function loadDocs() {
|
|
1964
|
+
var fileList = document.getElementById('docs-file-list');
|
|
1965
|
+
fileList.innerHTML = '<div class="docs-loading">加载中...</div>';
|
|
1966
|
+
document.getElementById('docs-count').textContent = '0';
|
|
1967
|
+
|
|
1968
|
+
fetch(basePath + '/api/docs')
|
|
1969
|
+
.then(function(r) { return r.json(); })
|
|
1970
|
+
.then(function(data) {
|
|
1971
|
+
var docs = data.docs || [];
|
|
1972
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
1973
|
+
if (!docs.length) {
|
|
1974
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
var html = '';
|
|
1978
|
+
docs.forEach(function(doc) {
|
|
1979
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
1980
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
1981
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
1982
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
1983
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
1984
|
+
html += '</div>';
|
|
1985
|
+
});
|
|
1986
|
+
fileList.innerHTML = html;
|
|
1987
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
1988
|
+
item.addEventListener('click', function() {
|
|
1989
|
+
var file = this.getAttribute('data-file');
|
|
1990
|
+
docsSelectedFile = file;
|
|
1991
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
1992
|
+
this.classList.add('active');
|
|
1993
|
+
loadDocContent(file);
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
})
|
|
1997
|
+
.catch(function() {
|
|
1998
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function loadDocContent(file) {
|
|
2003
|
+
var area = document.getElementById('docs-content-area');
|
|
2004
|
+
area.innerHTML = '<div class="docs-loading">加载中...</div>';
|
|
2005
|
+
fetch(basePath + '/api/doc-content?file=' + encodeURIComponent(file))
|
|
2006
|
+
.then(function(r) { return r.json(); })
|
|
2007
|
+
.then(function(data) {
|
|
2008
|
+
if (data.error) {
|
|
2009
|
+
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">' + escapeHtml(data.error) + '</div>';
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
area.innerHTML = '<pre>' + escapeHtml(data.content) + '</pre>';
|
|
2013
|
+
})
|
|
2014
|
+
.catch(function(err) {
|
|
2015
|
+
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
document.getElementById('docs-toggle').addEventListener('click', toggleDocsBar);
|
|
2020
|
+
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2021
|
+
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2022
|
+
e.stopPropagation();
|
|
2023
|
+
loadDocs();
|
|
2024
|
+
docsSelectedFile = null;
|
|
2025
|
+
document.getElementById('docs-content-area').innerHTML =
|
|
2026
|
+
'<div class="docs-placeholder">' +
|
|
2027
|
+
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">' +
|
|
2028
|
+
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>' +
|
|
2029
|
+
'<polyline points="14 2 14 8 20 8"></polyline></svg>' +
|
|
2030
|
+
'<span>点击文件查看内容</span></div>';
|
|
2031
|
+
});
|
|
2032
|
+
|
|
1821
2033
|
connect();
|
|
1822
2034
|
setTimeout(resize, 100);
|
|
1823
2035
|
})();
|
package/index.html
CHANGED
|
@@ -612,6 +612,97 @@
|
|
|
612
612
|
text-align: center;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/* 文档面板 */
|
|
616
|
+
#docs-bar {
|
|
617
|
+
display: none;
|
|
618
|
+
position: absolute;
|
|
619
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
620
|
+
background: #0a0a0a;
|
|
621
|
+
z-index: 1000;
|
|
622
|
+
flex-direction: column;
|
|
623
|
+
}
|
|
624
|
+
#docs-bar.visible { display: flex; }
|
|
625
|
+
#docs-header {
|
|
626
|
+
display: flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
justify-content: space-between;
|
|
629
|
+
padding: 12px 16px;
|
|
630
|
+
background: #111;
|
|
631
|
+
border-bottom: 1px solid #222;
|
|
632
|
+
flex-shrink: 0;
|
|
633
|
+
}
|
|
634
|
+
#docs-title {
|
|
635
|
+
font-size: 14px;
|
|
636
|
+
color: #ddd;
|
|
637
|
+
font-weight: 600;
|
|
638
|
+
}
|
|
639
|
+
.docs-file-list {
|
|
640
|
+
height: 250px;
|
|
641
|
+
flex-shrink: 0;
|
|
642
|
+
overflow-y: auto;
|
|
643
|
+
border-bottom: 1px solid #2a2a2a;
|
|
644
|
+
}
|
|
645
|
+
.docs-file-item {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
padding: 8px 12px;
|
|
649
|
+
cursor: pointer;
|
|
650
|
+
color: #ccc;
|
|
651
|
+
font-size: 13px;
|
|
652
|
+
gap: 8px;
|
|
653
|
+
}
|
|
654
|
+
.docs-file-item:hover { background: #1a1a1a; }
|
|
655
|
+
.docs-file-item.active {
|
|
656
|
+
background: rgba(197, 134, 192, 0.12);
|
|
657
|
+
color: #fff;
|
|
658
|
+
}
|
|
659
|
+
.docs-file-name {
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
text-overflow: ellipsis;
|
|
662
|
+
flex: 1;
|
|
663
|
+
font-family: Menlo, Monaco, monospace;
|
|
664
|
+
font-size: 12px;
|
|
665
|
+
}
|
|
666
|
+
.docs-file-time {
|
|
667
|
+
color: #666;
|
|
668
|
+
font-size: 11px;
|
|
669
|
+
flex-shrink: 0;
|
|
670
|
+
}
|
|
671
|
+
.docs-content-area {
|
|
672
|
+
flex: 1;
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-direction: column;
|
|
675
|
+
min-height: 0;
|
|
676
|
+
overflow: auto;
|
|
677
|
+
padding: 16px;
|
|
678
|
+
-webkit-overflow-scrolling: touch;
|
|
679
|
+
}
|
|
680
|
+
.docs-content-area pre {
|
|
681
|
+
margin: 0;
|
|
682
|
+
white-space: pre-wrap;
|
|
683
|
+
word-break: break-word;
|
|
684
|
+
font-family: Menlo, Monaco, monospace;
|
|
685
|
+
font-size: 13px;
|
|
686
|
+
line-height: 1.6;
|
|
687
|
+
color: #d4d4d4;
|
|
688
|
+
}
|
|
689
|
+
.docs-placeholder {
|
|
690
|
+
flex: 1;
|
|
691
|
+
display: flex;
|
|
692
|
+
flex-direction: column;
|
|
693
|
+
align-items: center;
|
|
694
|
+
justify-content: center;
|
|
695
|
+
gap: 12px;
|
|
696
|
+
color: #333;
|
|
697
|
+
font-size: 13px;
|
|
698
|
+
}
|
|
699
|
+
.docs-loading {
|
|
700
|
+
text-align: center;
|
|
701
|
+
padding: 16px;
|
|
702
|
+
color: #888;
|
|
703
|
+
font-size: 12px;
|
|
704
|
+
}
|
|
705
|
+
|
|
615
706
|
/* Unified diff 行 */
|
|
616
707
|
.diff-table {
|
|
617
708
|
width: 100%;
|
|
@@ -706,6 +797,15 @@
|
|
|
706
797
|
</svg>
|
|
707
798
|
<span>Diff</span>
|
|
708
799
|
</button>
|
|
800
|
+
<button class="history-toggle-btn" id="docs-toggle" style="color:#c586c0; border-color:#4a2a5a;">
|
|
801
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
802
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
803
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
804
|
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
805
|
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
806
|
+
</svg>
|
|
807
|
+
<span>文档</span>
|
|
808
|
+
</button>
|
|
709
809
|
<button class="history-toggle-btn" id="msg-toggle" style="color:#c9a0dc; border-color:#5a3a6a;">
|
|
710
810
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
711
811
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
|
@@ -794,6 +894,43 @@
|
|
|
794
894
|
</div>
|
|
795
895
|
</div>
|
|
796
896
|
|
|
897
|
+
<!-- 文档栏 -->
|
|
898
|
+
<div id="docs-bar">
|
|
899
|
+
<div id="docs-header">
|
|
900
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
901
|
+
<span id="docs-title">文档</span>
|
|
902
|
+
<span class="diff-file-count" id="docs-count">0</span>
|
|
903
|
+
</div>
|
|
904
|
+
<div style="display: flex; gap: 8px;">
|
|
905
|
+
<button class="history-toggle-btn" id="refresh-docs" style="padding: 4px 8px;">
|
|
906
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
|
907
|
+
<polyline points="23 4 23 10 17 10"></polyline>
|
|
908
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
909
|
+
</svg>
|
|
910
|
+
<span>刷新</span>
|
|
911
|
+
</button>
|
|
912
|
+
<button class="history-toggle-btn" id="close-docs" style="padding: 4px 8px;">
|
|
913
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
|
914
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
915
|
+
</svg>
|
|
916
|
+
<span>返回</span>
|
|
917
|
+
</button>
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="docs-file-list" id="docs-file-list">
|
|
921
|
+
<div class="docs-loading">加载中...</div>
|
|
922
|
+
</div>
|
|
923
|
+
<div class="docs-content-area" id="docs-content-area">
|
|
924
|
+
<div class="docs-placeholder">
|
|
925
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">
|
|
926
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
927
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
928
|
+
</svg>
|
|
929
|
+
<span>点击文件查看内容</span>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
|
|
797
934
|
<div id="content">
|
|
798
935
|
<div id="terminal-container">
|
|
799
936
|
<div id="terminal">
|
|
@@ -1348,8 +1485,8 @@
|
|
|
1348
1485
|
}
|
|
1349
1486
|
else if (msg.type === 'exit') {
|
|
1350
1487
|
if (!isCreatingNewSession && !isTransitioning) {
|
|
1351
|
-
throttledWrite('\r\n
|
|
1352
|
-
throttledWrite('
|
|
1488
|
+
throttledWrite('\r\n[进程已退出: ' + msg.exitCode + ']\r\n');
|
|
1489
|
+
throttledWrite('按 Enter 键重新启动 ' + currentMode + '...\r\n');
|
|
1353
1490
|
}
|
|
1354
1491
|
}
|
|
1355
1492
|
else if (msg.type === 'mode') {
|
|
@@ -1370,25 +1507,23 @@
|
|
|
1370
1507
|
}
|
|
1371
1508
|
}
|
|
1372
1509
|
else if (msg.type === 'restored') {
|
|
1373
|
-
//
|
|
1374
|
-
term.
|
|
1375
|
-
term.write('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\r\n');
|
|
1376
|
-
term.write('\r\n');
|
|
1510
|
+
// 会话恢复成功,重置终端清除残留ANSI状态
|
|
1511
|
+
term.reset();
|
|
1377
1512
|
}
|
|
1378
1513
|
else if (msg.type === 'restore-error') {
|
|
1379
1514
|
// 恢复失败
|
|
1380
|
-
term.write('
|
|
1515
|
+
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1381
1516
|
}
|
|
1382
1517
|
else if (msg.type === 'started') {
|
|
1383
1518
|
rebindTouchScroll();
|
|
1384
1519
|
}
|
|
1385
1520
|
else if (msg.type === 'new-session-ok') {
|
|
1386
1521
|
isCreatingNewSession = false;
|
|
1387
|
-
term.
|
|
1522
|
+
term.reset();
|
|
1388
1523
|
}
|
|
1389
1524
|
else if (msg.type === 'new-session-error') {
|
|
1390
1525
|
isCreatingNewSession = false;
|
|
1391
|
-
term.write('
|
|
1526
|
+
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1392
1527
|
}
|
|
1393
1528
|
} catch(err) {}
|
|
1394
1529
|
};
|
|
@@ -1706,25 +1841,18 @@
|
|
|
1706
1841
|
// 关闭历史栏
|
|
1707
1842
|
toggleHistoryBar();
|
|
1708
1843
|
|
|
1709
|
-
//
|
|
1710
|
-
term.
|
|
1711
|
-
|
|
1712
|
-
// 显示正在恢复的提示
|
|
1713
|
-
term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
|
|
1714
|
-
term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
|
|
1715
|
-
term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
|
|
1716
|
-
term.write('\r\n');
|
|
1844
|
+
// 重置终端(清除残留ANSI解析状态)
|
|
1845
|
+
term.reset();
|
|
1717
1846
|
|
|
1718
1847
|
if (ws && ws.readyState === 1) {
|
|
1719
1848
|
if (currentMode !== 'opencode') {
|
|
1720
|
-
term.write('
|
|
1849
|
+
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
1721
1850
|
return;
|
|
1722
1851
|
}
|
|
1723
|
-
|
|
1724
|
-
term.write('\r\n');
|
|
1852
|
+
// 静默恢复
|
|
1725
1853
|
ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
|
|
1726
1854
|
} else {
|
|
1727
|
-
term.write('
|
|
1855
|
+
term.write('错误: WebSocket 未连接\r\n');
|
|
1728
1856
|
}
|
|
1729
1857
|
}
|
|
1730
1858
|
|
|
@@ -2086,6 +2214,92 @@
|
|
|
2086
2214
|
'</svg><span>点击文件查看 diff</span></div>';
|
|
2087
2215
|
});
|
|
2088
2216
|
|
|
2217
|
+
// === 文档面板 ===
|
|
2218
|
+
var docsBarVisible = false;
|
|
2219
|
+
var docsSelectedFile = null;
|
|
2220
|
+
|
|
2221
|
+
function toggleDocsBar() {
|
|
2222
|
+
docsBarVisible = !docsBarVisible;
|
|
2223
|
+
var bar = document.getElementById('docs-bar');
|
|
2224
|
+
if (docsBarVisible) {
|
|
2225
|
+
bar.classList.add('visible');
|
|
2226
|
+
loadDocs();
|
|
2227
|
+
} else {
|
|
2228
|
+
bar.classList.remove('visible');
|
|
2229
|
+
docsSelectedFile = null;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function loadDocs() {
|
|
2234
|
+
var fileList = document.getElementById('docs-file-list');
|
|
2235
|
+
fileList.innerHTML = '<div class="docs-loading">加载中...</div>';
|
|
2236
|
+
document.getElementById('docs-count').textContent = '0';
|
|
2237
|
+
|
|
2238
|
+
fetch(basePath + '/api/docs')
|
|
2239
|
+
.then(function(r) { return r.json(); })
|
|
2240
|
+
.then(function(data) {
|
|
2241
|
+
var docs = data.docs || [];
|
|
2242
|
+
document.getElementById('docs-count').textContent = docs.length;
|
|
2243
|
+
if (!docs.length) {
|
|
2244
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
var html = '';
|
|
2248
|
+
docs.forEach(function(doc) {
|
|
2249
|
+
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2250
|
+
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
2251
|
+
html += '<div class="docs-file-item' + activeClass + '" data-file="' + escapeHtml(doc.name) + '">';
|
|
2252
|
+
html += '<span class="docs-file-name">' + escapeHtml(doc.name) + '</span>';
|
|
2253
|
+
html += '<span class="docs-file-time">' + time + '</span>';
|
|
2254
|
+
html += '</div>';
|
|
2255
|
+
});
|
|
2256
|
+
fileList.innerHTML = html;
|
|
2257
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(item) {
|
|
2258
|
+
item.addEventListener('click', function() {
|
|
2259
|
+
var file = this.getAttribute('data-file');
|
|
2260
|
+
docsSelectedFile = file;
|
|
2261
|
+
fileList.querySelectorAll('.docs-file-item').forEach(function(el) { el.classList.remove('active'); });
|
|
2262
|
+
this.classList.add('active');
|
|
2263
|
+
loadDocContent(file);
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
})
|
|
2267
|
+
.catch(function() {
|
|
2268
|
+
fileList.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function loadDocContent(file) {
|
|
2273
|
+
var area = document.getElementById('docs-content-area');
|
|
2274
|
+
area.innerHTML = '<div class="docs-loading">加载中...</div>';
|
|
2275
|
+
fetch(basePath + '/api/doc-content?file=' + encodeURIComponent(file))
|
|
2276
|
+
.then(function(r) { return r.json(); })
|
|
2277
|
+
.then(function(data) {
|
|
2278
|
+
if (data.error) {
|
|
2279
|
+
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">' + escapeHtml(data.error) + '</div>';
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
area.innerHTML = '<pre>' + escapeHtml(data.content) + '</pre>';
|
|
2283
|
+
})
|
|
2284
|
+
.catch(function() {
|
|
2285
|
+
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
document.getElementById('docs-toggle').addEventListener('click', toggleDocsBar);
|
|
2290
|
+
document.getElementById('close-docs').addEventListener('click', toggleDocsBar);
|
|
2291
|
+
document.getElementById('refresh-docs').addEventListener('click', function(e) {
|
|
2292
|
+
e.stopPropagation();
|
|
2293
|
+
loadDocs();
|
|
2294
|
+
docsSelectedFile = null;
|
|
2295
|
+
document.getElementById('docs-content-area').innerHTML =
|
|
2296
|
+
'<div class="docs-placeholder">' +
|
|
2297
|
+
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">' +
|
|
2298
|
+
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>' +
|
|
2299
|
+
'<polyline points="14 2 14 8 20 8"></polyline></svg>' +
|
|
2300
|
+
'<span>点击文件查看内容</span></div>';
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2089
2303
|
// 初始化虚拟按键事件
|
|
2090
2304
|
setupVirtualKeyEvents();
|
|
2091
2305
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { createServer as createHttpsServer } from 'node:https';
|
|
4
|
-
import { existsSync, createReadStream, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, createReadStream, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { networkInterfaces, platform, arch, homedir } from 'node:os';
|
|
@@ -95,6 +95,7 @@ let lastPtyRows = 30;
|
|
|
95
95
|
|
|
96
96
|
let activeWs = null;
|
|
97
97
|
let currentSessionId = null;
|
|
98
|
+
let sessionStartTime = 0; // 新会话启动时间,用于限制查询范围
|
|
98
99
|
let previousSessionId = null; // 用于判断新会话是否已写入 DB
|
|
99
100
|
const clientSizes = new Map();
|
|
100
101
|
const mobileClients = new Set();
|
|
@@ -163,23 +164,38 @@ function findCommand(cmd) {
|
|
|
163
164
|
return cmd;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
function getChildPids(pid) {
|
|
168
|
+
// 递归获取所有子进程 PID
|
|
169
|
+
try {
|
|
170
|
+
const out = execSync(`ps --ppid ${pid} -o pid= 2>/dev/null || pgrep -P ${pid} 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 });
|
|
171
|
+
const children = out.trim().split(/\s+/).filter(Boolean).map(Number);
|
|
172
|
+
let all = [...children];
|
|
173
|
+
for (const child of children) {
|
|
174
|
+
all = all.concat(getChildPids(child));
|
|
175
|
+
}
|
|
176
|
+
return all;
|
|
177
|
+
} catch { return []; }
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
function killProcessTree(proc) {
|
|
167
181
|
if (!proc || !proc.pid) return;
|
|
168
182
|
const pid = proc.pid;
|
|
169
|
-
// 1.
|
|
183
|
+
// 1. 收集所有子进程(在杀父进程之前,否则子进程变孤儿就找不到了)
|
|
184
|
+
const children = getChildPids(pid);
|
|
185
|
+
// 2. 尝试杀进程组
|
|
170
186
|
try { process.kill(-pid, 'SIGTERM'); } catch {}
|
|
171
|
-
//
|
|
187
|
+
// 3. 杀 pty 进程本身
|
|
172
188
|
try { proc.kill(); } catch {}
|
|
173
|
-
//
|
|
189
|
+
// 4. SIGTERM 所有子进程
|
|
190
|
+
for (const cpid of children) {
|
|
191
|
+
try { process.kill(cpid, 'SIGTERM'); } catch {}
|
|
192
|
+
}
|
|
193
|
+
// 5. 1秒后 SIGKILL 兜底
|
|
174
194
|
setTimeout(() => {
|
|
175
|
-
|
|
176
|
-
process.kill(
|
|
177
|
-
|
|
178
|
-
} catch {}
|
|
179
|
-
try {
|
|
180
|
-
process.kill(pid, 0);
|
|
181
|
-
process.kill(pid, 'SIGKILL');
|
|
182
|
-
} catch {}
|
|
195
|
+
for (const cpid of children) {
|
|
196
|
+
try { process.kill(cpid, 'SIGKILL'); } catch {}
|
|
197
|
+
}
|
|
198
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
183
199
|
}, 1000);
|
|
184
200
|
}
|
|
185
201
|
|
|
@@ -284,8 +300,8 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
284
300
|
currentSessionId = sessionId;
|
|
285
301
|
LOG(`[session] 当前会话 ID: ${currentSessionId}`);
|
|
286
302
|
} else {
|
|
287
|
-
// 新建会话:保持 null,前端点击复制时不显示内容
|
|
288
303
|
currentSessionId = null;
|
|
304
|
+
sessionStartTime = Date.now();
|
|
289
305
|
}
|
|
290
306
|
}
|
|
291
307
|
|
|
@@ -550,7 +566,23 @@ const requestHandler = async (req, res) => {
|
|
|
550
566
|
'Content-Type': 'application/json',
|
|
551
567
|
'Access-Control-Allow-Origin': '*',
|
|
552
568
|
});
|
|
553
|
-
|
|
569
|
+
// 实时从数据库查当前有效的会话
|
|
570
|
+
let resolvedSessionId = currentSessionId;
|
|
571
|
+
if (!resolvedSessionId && sessionStartTime && existsSync(OPENCODE_DB_PATH)) {
|
|
572
|
+
try {
|
|
573
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
574
|
+
const row = db.prepare(
|
|
575
|
+
`SELECT s.id FROM session s
|
|
576
|
+
WHERE s.parent_id IS NULL AND s.time_archived IS NULL
|
|
577
|
+
AND s.time_created >= ?
|
|
578
|
+
AND EXISTS (SELECT 1 FROM message m WHERE m.session_id = s.id)
|
|
579
|
+
ORDER BY s.time_created DESC LIMIT 1`
|
|
580
|
+
).get(sessionStartTime);
|
|
581
|
+
db.close();
|
|
582
|
+
if (row) resolvedSessionId = row.id;
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
res.end(JSON.stringify({ sessionId: resolvedSessionId }));
|
|
554
586
|
return;
|
|
555
587
|
}
|
|
556
588
|
|
|
@@ -561,8 +593,9 @@ const requestHandler = async (req, res) => {
|
|
|
561
593
|
'Access-Control-Allow-Origin': '*',
|
|
562
594
|
});
|
|
563
595
|
try {
|
|
596
|
+
const gitCwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
|
|
564
597
|
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
565
|
-
cwd:
|
|
598
|
+
cwd: gitCwd, encoding: 'utf-8', timeout: 5000,
|
|
566
599
|
});
|
|
567
600
|
const changes = stdout.split('\n').filter(Boolean).map(line => ({
|
|
568
601
|
status: line.substring(0, 2).trim(),
|
|
@@ -589,7 +622,7 @@ const requestHandler = async (req, res) => {
|
|
|
589
622
|
}
|
|
590
623
|
const fileList = files.split(',').filter(Boolean);
|
|
591
624
|
const diffs = [];
|
|
592
|
-
const cwd = process.cwd();
|
|
625
|
+
const cwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
|
|
593
626
|
for (const file of fileList) {
|
|
594
627
|
if (file.includes('..') || file.startsWith('/')) continue;
|
|
595
628
|
try {
|
|
@@ -644,6 +677,53 @@ const requestHandler = async (req, res) => {
|
|
|
644
677
|
return;
|
|
645
678
|
}
|
|
646
679
|
|
|
680
|
+
// API: 获取文档列表
|
|
681
|
+
if (req.url === '/api/docs') {
|
|
682
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
683
|
+
try {
|
|
684
|
+
const docsCwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
|
|
685
|
+
const EXCLUDE = new Set(['readme.md', 'changelog.md', 'license.md', 'claude.md', 'agents.md', 'contributing.md', 'security.md', 'context.md']);
|
|
686
|
+
const files = readdirSync(docsCwd)
|
|
687
|
+
.filter(f => f.endsWith('.md') && !EXCLUDE.has(f.toLowerCase()))
|
|
688
|
+
.map(f => {
|
|
689
|
+
try {
|
|
690
|
+
const st = statSync(join(docsCwd, f));
|
|
691
|
+
return { name: f, mtime: st.mtimeMs };
|
|
692
|
+
} catch { return null; }
|
|
693
|
+
})
|
|
694
|
+
.filter(Boolean)
|
|
695
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
696
|
+
res.end(JSON.stringify({ docs: files }));
|
|
697
|
+
} catch (err) {
|
|
698
|
+
res.end(JSON.stringify({ docs: [], error: err.message }));
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// API: 获取文档内容
|
|
704
|
+
if (req.url?.startsWith('/api/doc-content')) {
|
|
705
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
706
|
+
const file = url.searchParams.get('file');
|
|
707
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
708
|
+
if (!file || file.includes('/') || file.includes('..') || !file.endsWith('.md')) {
|
|
709
|
+
res.end(JSON.stringify({ error: '无效文件名' }));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const docsCwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
|
|
714
|
+
const filePath = join(docsCwd, file);
|
|
715
|
+
if (!existsSync(filePath)) {
|
|
716
|
+
res.end(JSON.stringify({ error: '文件不存在' }));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
720
|
+
res.end(JSON.stringify({ name: file, content }));
|
|
721
|
+
} catch (err) {
|
|
722
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
723
|
+
}
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
647
727
|
// API: 软删除会话(设置 time_archived)
|
|
648
728
|
if (req.method === 'DELETE' && req.url?.startsWith('/api/session/')) {
|
|
649
729
|
const sessionId = req.url.split('/').pop();
|
|
@@ -687,6 +767,29 @@ const requestHandler = async (req, res) => {
|
|
|
687
767
|
const httpsOpts = USE_HTTPS ? await getOrCreateCert() : null;
|
|
688
768
|
let server, wss;
|
|
689
769
|
|
|
770
|
+
// 无客户端连接后自动退出,防止进程堆积
|
|
771
|
+
// 首次启动等 3 分钟(给用户时间打开浏览器),连接过之后断开等 30 秒
|
|
772
|
+
let noClientTimer = null;
|
|
773
|
+
let hasEverConnected = false;
|
|
774
|
+
function startNoClientTimer() {
|
|
775
|
+
if (noClientTimer) return;
|
|
776
|
+
if (wss && wss.clients.size > 0) return;
|
|
777
|
+
const timeout = hasEverConnected ? 30000 : 180000;
|
|
778
|
+
noClientTimer = setTimeout(() => {
|
|
779
|
+
if (!wss || wss.clients.size === 0) {
|
|
780
|
+
LOG(`[auto-exit] ${timeout / 1000}秒无客户端连接,自动退出`);
|
|
781
|
+
cleanupAndExit();
|
|
782
|
+
}
|
|
783
|
+
noClientTimer = null;
|
|
784
|
+
}, timeout);
|
|
785
|
+
}
|
|
786
|
+
function cancelNoClientTimer() {
|
|
787
|
+
if (noClientTimer) {
|
|
788
|
+
clearTimeout(noClientTimer);
|
|
789
|
+
noClientTimer = null;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
690
793
|
function createServerAndWss() {
|
|
691
794
|
server = USE_HTTPS
|
|
692
795
|
? createHttpsServer(httpsOpts, requestHandler)
|
|
@@ -708,6 +811,8 @@ function createServerAndWss() {
|
|
|
708
811
|
function setupWss(wssInst) {
|
|
709
812
|
wssInst.on('connection', (ws, req) => {
|
|
710
813
|
LOG('[WS] 客户端连接 from', req.socket.remoteAddress);
|
|
814
|
+
hasEverConnected = true;
|
|
815
|
+
cancelNoClientTimer();
|
|
711
816
|
ws.isAlive = true;
|
|
712
817
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
713
818
|
|
|
@@ -864,6 +969,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
864
969
|
isSwitching = true;
|
|
865
970
|
previousSessionId = currentSessionId; // 记住旧会话,用于判断新会话是否已写入 DB
|
|
866
971
|
currentSessionId = null; // 立即清除旧会话 ID
|
|
972
|
+
sessionStartTime = Date.now(); // 重置启动时间,防止查到旧会话
|
|
867
973
|
|
|
868
974
|
if (mode === 'opencode' && opencodeProcess) {
|
|
869
975
|
try { killProcessTree(opencodeProcess); } catch {}
|
|
@@ -888,18 +994,22 @@ wssInst.on('connection', (ws, req) => {
|
|
|
888
994
|
}
|
|
889
995
|
isSwitching = false;
|
|
890
996
|
|
|
891
|
-
// 延迟检测新会话 ID(等 opencode 写入 DB
|
|
892
|
-
const
|
|
997
|
+
// 延迟检测新会话 ID(等 opencode 写入 DB,必须在 sessionStartTime 之后且有消息)
|
|
998
|
+
const startTs = sessionStartTime;
|
|
893
999
|
const checkNewSession = (attempt) => {
|
|
894
|
-
if (currentSessionId || attempt >
|
|
1000
|
+
if (currentSessionId || attempt > 15) return;
|
|
895
1001
|
try {
|
|
896
1002
|
if (existsSync(OPENCODE_DB_PATH)) {
|
|
897
1003
|
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
898
1004
|
const row = db.prepare(
|
|
899
|
-
`SELECT id FROM session
|
|
900
|
-
|
|
1005
|
+
`SELECT s.id FROM session s
|
|
1006
|
+
WHERE s.parent_id IS NULL AND s.time_archived IS NULL
|
|
1007
|
+
AND s.time_created >= ?
|
|
1008
|
+
AND EXISTS (SELECT 1 FROM message m WHERE m.session_id = s.id)
|
|
1009
|
+
ORDER BY s.time_created DESC LIMIT 1`
|
|
1010
|
+
).get(startTs);
|
|
901
1011
|
db.close();
|
|
902
|
-
if (row
|
|
1012
|
+
if (row) {
|
|
903
1013
|
currentSessionId = row.id;
|
|
904
1014
|
LOG(`[new-session] 检测到新会话 ID: ${currentSessionId}`);
|
|
905
1015
|
return;
|
|
@@ -937,6 +1047,8 @@ wssInst.on('connection', (ws, req) => {
|
|
|
937
1047
|
}
|
|
938
1048
|
}
|
|
939
1049
|
}
|
|
1050
|
+
// 无客户端连接时,30秒后自动退出
|
|
1051
|
+
startNoClientTimer();
|
|
940
1052
|
});
|
|
941
1053
|
});
|
|
942
1054
|
|
|
@@ -998,6 +1110,9 @@ function startServer() {
|
|
|
998
1110
|
} else {
|
|
999
1111
|
await spawnProcess('opencode');
|
|
1000
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
// 启动首次连接超时检测(3分钟无人连接则退出)
|
|
1115
|
+
startNoClientTimer();
|
|
1001
1116
|
});
|
|
1002
1117
|
|
|
1003
1118
|
server.on('error', (err) => {
|