claude-opencode-viewer 2.6.12 → 2.6.14
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 +165 -135
- package/package.json +1 -1
- package/server.js +92 -19
package/index.html
CHANGED
|
@@ -174,13 +174,13 @@
|
|
|
174
174
|
background: none;
|
|
175
175
|
border: 1px solid #333;
|
|
176
176
|
color: #aaa;
|
|
177
|
-
padding:
|
|
178
|
-
font-size:
|
|
177
|
+
padding: 3px 6px;
|
|
178
|
+
font-size: 11px;
|
|
179
179
|
cursor: pointer;
|
|
180
180
|
border-radius: 4px;
|
|
181
181
|
display: flex;
|
|
182
182
|
align-items: center;
|
|
183
|
-
gap:
|
|
183
|
+
gap: 3px;
|
|
184
184
|
flex-shrink: 0;
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -191,8 +191,8 @@
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
.history-toggle-btn svg {
|
|
194
|
-
width:
|
|
195
|
-
height:
|
|
194
|
+
width: 12px;
|
|
195
|
+
height: 12px;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
.session-loading {
|
|
@@ -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
|
/* 复制成功提示 */
|
|
@@ -680,6 +703,12 @@
|
|
|
680
703
|
</svg>
|
|
681
704
|
<span>Diff</span>
|
|
682
705
|
</button>
|
|
706
|
+
<button class="history-toggle-btn" id="msg-toggle" style="color:#c9a0dc; border-color:#5a3a6a;">
|
|
707
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
708
|
+
<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>
|
|
709
|
+
</svg>
|
|
710
|
+
<span>消息</span>
|
|
711
|
+
</button>
|
|
683
712
|
</div>
|
|
684
713
|
<div id="mode-switcher">
|
|
685
714
|
<span id="mode-label"></span>
|
|
@@ -765,11 +794,6 @@
|
|
|
765
794
|
<div id="content">
|
|
766
795
|
<div id="terminal-container">
|
|
767
796
|
<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
797
|
</div>
|
|
774
798
|
<div id="virtual-keybar">
|
|
775
799
|
<div class="virtual-key" data-key="up">↑</div>
|
|
@@ -785,6 +809,16 @@
|
|
|
785
809
|
</div>
|
|
786
810
|
</div>
|
|
787
811
|
|
|
812
|
+
<div id="message-viewer">
|
|
813
|
+
<div id="msg-viewer-header">
|
|
814
|
+
<span>会话消息</span>
|
|
815
|
+
<button id="msg-viewer-close">✕</button>
|
|
816
|
+
</div>
|
|
817
|
+
<div id="msg-viewer-content">
|
|
818
|
+
<div class="msg-empty">加载中...</div>
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
|
|
788
822
|
<div id="copy-toast">已复制</div>
|
|
789
823
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
790
824
|
<script>
|
|
@@ -931,7 +965,7 @@
|
|
|
931
965
|
onVVChange();
|
|
932
966
|
}
|
|
933
967
|
|
|
934
|
-
//
|
|
968
|
+
// 移动端固定尺寸计算:基于 #terminal 元素实际高度,确保终端与按钮栏齐平
|
|
935
969
|
function mobileFixedResize() {
|
|
936
970
|
if (!term) return;
|
|
937
971
|
var cellDims = getCellDims();
|
|
@@ -941,12 +975,9 @@
|
|
|
941
975
|
}
|
|
942
976
|
|
|
943
977
|
var padX = 16;
|
|
944
|
-
var
|
|
945
|
-
var topBarHeight = 40;
|
|
946
|
-
var keybarHeight = 52;
|
|
947
|
-
|
|
978
|
+
var termEl = document.getElementById('terminal');
|
|
948
979
|
var availW = window.innerWidth - padX;
|
|
949
|
-
var availH =
|
|
980
|
+
var availH = termEl ? termEl.clientHeight : 300;
|
|
950
981
|
|
|
951
982
|
var currentFontSize = term.options.fontSize;
|
|
952
983
|
var currentCharWidth = cellDims.width;
|
|
@@ -1092,49 +1123,17 @@
|
|
|
1092
1123
|
}
|
|
1093
1124
|
}
|
|
1094
1125
|
|
|
1095
|
-
// 长按检测
|
|
1096
|
-
var longPressTimer = null;
|
|
1097
|
-
var longPressTriggered = false;
|
|
1098
|
-
var LONG_PRESS_DELAY = 550; // ms
|
|
1099
|
-
|
|
1100
|
-
function clearLongPress() {
|
|
1101
|
-
if (longPressTimer) {
|
|
1102
|
-
clearTimeout(longPressTimer);
|
|
1103
|
-
longPressTimer = null;
|
|
1104
|
-
}
|
|
1105
|
-
// 始终恢复 xterm textarea,防止 disabled 残留
|
|
1106
|
-
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1107
|
-
if (xtermTa) xtermTa.removeAttribute('disabled');
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
1126
|
function handleTouchStart(e) {
|
|
1111
1127
|
stopMomentum();
|
|
1112
1128
|
altScrollAccum = 0;
|
|
1113
|
-
longPressTriggered = false;
|
|
1114
|
-
clearLongPress();
|
|
1115
1129
|
if (e.touches.length !== 1) return;
|
|
1116
1130
|
lastY = e.touches[0].clientY;
|
|
1117
1131
|
lastTime = performance.now();
|
|
1118
1132
|
velocitySamples = [];
|
|
1119
|
-
|
|
1120
|
-
// 启动长按计时器
|
|
1121
|
-
// 在长按检测期间阻止 xterm textarea 获取焦点,防止弹出键盘
|
|
1122
|
-
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1123
|
-
if (xtermTa) {
|
|
1124
|
-
xtermTa.setAttribute('disabled', 'true');
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
longPressTimer = setTimeout(function() {
|
|
1128
|
-
longPressTriggered = true;
|
|
1129
|
-
longPressTimer = null;
|
|
1130
|
-
openSelectMode();
|
|
1131
|
-
}, LONG_PRESS_DELAY);
|
|
1132
1133
|
}
|
|
1133
1134
|
|
|
1134
1135
|
function handleTouchMove(e) {
|
|
1135
1136
|
if (e.touches.length !== 1) return;
|
|
1136
|
-
// 有移动则取消长按
|
|
1137
|
-
clearLongPress();
|
|
1138
1137
|
var y = e.touches[0].clientY;
|
|
1139
1138
|
var now = performance.now();
|
|
1140
1139
|
var dt = now - lastTime;
|
|
@@ -1157,17 +1156,6 @@
|
|
|
1157
1156
|
}
|
|
1158
1157
|
|
|
1159
1158
|
function handleTouchEnd() {
|
|
1160
|
-
console.log('[scroll] touchend');
|
|
1161
|
-
clearLongPress();
|
|
1162
|
-
|
|
1163
|
-
// 长按已触发,不执行滚动惯性
|
|
1164
|
-
if (longPressTriggered) {
|
|
1165
|
-
longPressTriggered = false;
|
|
1166
|
-
pendingDy = 0;
|
|
1167
|
-
pixelAccum = 0;
|
|
1168
|
-
velocitySamples = [];
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
1159
|
|
|
1172
1160
|
if (scrollRaf) {
|
|
1173
1161
|
cancelAnimationFrame(scrollRaf);
|
|
@@ -1818,47 +1806,89 @@
|
|
|
1818
1806
|
}
|
|
1819
1807
|
|
|
1820
1808
|
|
|
1821
|
-
//
|
|
1822
|
-
var
|
|
1823
|
-
var
|
|
1824
|
-
var
|
|
1825
|
-
var inSelectMode = false;
|
|
1809
|
+
// 消息查看器
|
|
1810
|
+
var messageViewer = document.getElementById('message-viewer');
|
|
1811
|
+
var msgViewerContent = document.getElementById('msg-viewer-content');
|
|
1812
|
+
var msgViewerClose = document.getElementById('msg-viewer-close');
|
|
1826
1813
|
|
|
1827
|
-
function
|
|
1828
|
-
if (inSelectMode) return;
|
|
1829
|
-
inSelectMode = true;
|
|
1814
|
+
function openMessageViewer() {
|
|
1830
1815
|
// 收起键盘
|
|
1831
1816
|
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1832
1817
|
if (xtermTa) xtermTa.blur();
|
|
1833
1818
|
document.activeElement && document.activeElement.blur();
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
selectTextLayer.classList.add('visible');
|
|
1838
|
-
selectModeClose.style.display = 'block';
|
|
1819
|
+
|
|
1820
|
+
messageViewer.classList.add('visible');
|
|
1821
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
|
|
1839
1822
|
unbindTouchScroll();
|
|
1823
|
+
|
|
1824
|
+
// 获取当前会话 ID
|
|
1825
|
+
fetch(basePath + '/api/current-session')
|
|
1826
|
+
.then(function(r) { return r.json(); })
|
|
1827
|
+
.then(function(data) {
|
|
1828
|
+
if (!data.sessionId) {
|
|
1829
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无活跃会话</div>';
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
return fetch(basePath + '/api/session/' + data.sessionId)
|
|
1833
|
+
.then(function(r) { return r.json(); })
|
|
1834
|
+
.then(function(messages) {
|
|
1835
|
+
renderMessages(messages);
|
|
1836
|
+
});
|
|
1837
|
+
})
|
|
1838
|
+
.catch(function(e) {
|
|
1839
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">加载失败: ' + e.message + '</div>';
|
|
1840
|
+
});
|
|
1840
1841
|
}
|
|
1841
1842
|
|
|
1842
|
-
function
|
|
1843
|
-
if (!
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1843
|
+
function renderMessages(messages) {
|
|
1844
|
+
if (!messages || messages.length === 0) {
|
|
1845
|
+
msgViewerContent.innerHTML = '<div class="msg-empty">暂无消息</div>';
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
var html = '';
|
|
1849
|
+
messages.forEach(function(msg) {
|
|
1850
|
+
var role = msg.role || 'unknown';
|
|
1851
|
+
var roleLabel = role === 'user' ? '用户' : role === 'assistant' ? '助手' : role;
|
|
1852
|
+
var cls = role === 'user' ? 'msg-user' : role === 'assistant' ? 'msg-assistant' : '';
|
|
1853
|
+
html += '<div class="msg-item ' + cls + '">';
|
|
1854
|
+
html += '<div class="msg-role">' + roleLabel + '</div>';
|
|
1855
|
+
if (msg.text) {
|
|
1856
|
+
html += '<div class="msg-text">' + escapeHtml(msg.text) + '</div>';
|
|
1857
|
+
}
|
|
1858
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
1859
|
+
msg.toolCalls.forEach(function(tc) {
|
|
1860
|
+
html += '<div class="msg-tool">🔧 ' + escapeHtml(tc.name || 'tool') + '</div>';
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
html += '</div>';
|
|
1864
|
+
});
|
|
1865
|
+
msgViewerContent.innerHTML = html;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function escapeHtml(str) {
|
|
1869
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function closeMessageViewer() {
|
|
1873
|
+
messageViewer.classList.remove('visible');
|
|
1849
1874
|
rebindTouchScroll();
|
|
1850
1875
|
}
|
|
1851
1876
|
|
|
1852
|
-
|
|
1877
|
+
msgViewerClose.addEventListener('click', function(e) {
|
|
1853
1878
|
e.preventDefault();
|
|
1854
1879
|
e.stopPropagation();
|
|
1855
|
-
|
|
1880
|
+
closeMessageViewer();
|
|
1856
1881
|
});
|
|
1857
1882
|
|
|
1858
|
-
|
|
1883
|
+
msgViewerClose.addEventListener('touchend', function(e) {
|
|
1859
1884
|
e.preventDefault();
|
|
1860
1885
|
e.stopPropagation();
|
|
1861
|
-
|
|
1886
|
+
closeMessageViewer();
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
// 顶部消息按钮
|
|
1890
|
+
document.getElementById('msg-toggle').addEventListener('click', function() {
|
|
1891
|
+
openMessageViewer();
|
|
1862
1892
|
});
|
|
1863
1893
|
|
|
1864
1894
|
// ======= 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,61 @@ 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
|
+
// 尝试恢复最近的会话,如果没有则新建
|
|
857
|
+
let lastSessionId = null;
|
|
858
|
+
try {
|
|
859
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
860
|
+
const row = db.prepare(
|
|
861
|
+
`SELECT id FROM session WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_updated DESC LIMIT 1`
|
|
862
|
+
).get();
|
|
863
|
+
db.close();
|
|
864
|
+
if (row) lastSessionId = row.id;
|
|
865
|
+
} catch (e) {}
|
|
866
|
+
|
|
867
|
+
if (lastSessionId) {
|
|
868
|
+
console.log(`[startup] 恢复最近会话: ${lastSessionId}`);
|
|
869
|
+
await spawnProcess('opencode', lastSessionId);
|
|
870
|
+
} else {
|
|
871
|
+
await spawnProcess('opencode');
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
server.on('error', (err) => {
|
|
876
|
+
if (err.code === 'EADDRINUSE' && IS_PC && retries < MAX_PORT_RETRIES) {
|
|
877
|
+
// PC 模式端口冲突,顺序查找下一个可用端口
|
|
878
|
+
const oldPort = PORT;
|
|
879
|
+
PORT = PORT >= PC_PORT_MAX ? PC_PORT_MIN : PORT + 1;
|
|
880
|
+
console.error(`[port] ${oldPort} 已占用,尝试 ${PORT} (${retries + 1}/${MAX_PORT_RETRIES})`);
|
|
881
|
+
server.removeAllListeners('error');
|
|
882
|
+
startServer(retries + 1);
|
|
883
|
+
} else {
|
|
884
|
+
console.error(`启动失败: ${err.message}`);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
startServer();
|