claude-opencode-viewer 2.6.31 → 2.6.33
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-pc.html +272 -5
- package/index.html +266 -5
- package/package.json +1 -1
- package/server.js +245 -40
package/index-pc.html
CHANGED
|
@@ -254,6 +254,69 @@
|
|
|
254
254
|
border-radius: 3px;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/* Claude 会话详情视图 */
|
|
258
|
+
#claude-detail-view {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
flex: 1;
|
|
262
|
+
min-height: 0;
|
|
263
|
+
}
|
|
264
|
+
#claude-detail-view.hidden { display: none; }
|
|
265
|
+
|
|
266
|
+
#claude-detail-header {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
padding: 10px 16px;
|
|
270
|
+
background: #111;
|
|
271
|
+
border-bottom: 1px solid #222;
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
gap: 8px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#claude-messages-container {
|
|
277
|
+
flex: 1;
|
|
278
|
+
overflow-y: auto;
|
|
279
|
+
padding: 12px 16px;
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 12px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.claude-msg {
|
|
286
|
+
padding: 10px 14px;
|
|
287
|
+
border-radius: 6px;
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
line-height: 1.6;
|
|
290
|
+
white-space: pre-wrap;
|
|
291
|
+
word-break: break-word;
|
|
292
|
+
max-width: 100%;
|
|
293
|
+
}
|
|
294
|
+
.claude-msg-user {
|
|
295
|
+
background: #1a2a3a;
|
|
296
|
+
border-left: 3px solid #4a9eff;
|
|
297
|
+
}
|
|
298
|
+
.claude-msg-assistant {
|
|
299
|
+
background: #1a1a2a;
|
|
300
|
+
border-left: 3px solid #c586c0;
|
|
301
|
+
}
|
|
302
|
+
.claude-msg-role {
|
|
303
|
+
font-size: 11px;
|
|
304
|
+
color: #888;
|
|
305
|
+
margin-bottom: 4px;
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
}
|
|
308
|
+
.claude-msg-user .claude-msg-role { color: #4a9eff; }
|
|
309
|
+
.claude-msg-assistant .claude-msg-role { color: #c586c0; }
|
|
310
|
+
.claude-msg-text {
|
|
311
|
+
color: #d4d4d4;
|
|
312
|
+
}
|
|
313
|
+
.claude-msg-time {
|
|
314
|
+
font-size: 10px;
|
|
315
|
+
color: #666;
|
|
316
|
+
margin-top: 4px;
|
|
317
|
+
text-align: right;
|
|
318
|
+
}
|
|
319
|
+
|
|
257
320
|
#mode-switcher {
|
|
258
321
|
display: flex;
|
|
259
322
|
gap: 4px;
|
|
@@ -777,6 +840,27 @@
|
|
|
777
840
|
</div>
|
|
778
841
|
</div>
|
|
779
842
|
|
|
843
|
+
<!-- Claude 会话详情视图 -->
|
|
844
|
+
<div id="claude-detail-view" class="hidden">
|
|
845
|
+
<div id="claude-detail-header">
|
|
846
|
+
<button class="history-toggle-btn" id="claude-detail-back">
|
|
847
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
848
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
849
|
+
</svg>
|
|
850
|
+
<span>返回列表</span>
|
|
851
|
+
</button>
|
|
852
|
+
<div id="claude-detail-title" style="flex:1; text-align:center; color:#ddd; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"></div>
|
|
853
|
+
<button class="history-toggle-btn" id="claude-detail-close">
|
|
854
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
855
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
856
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
857
|
+
</svg>
|
|
858
|
+
<span>关闭</span>
|
|
859
|
+
</button>
|
|
860
|
+
</div>
|
|
861
|
+
<div id="claude-messages-container"></div>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
780
864
|
</div>
|
|
781
865
|
|
|
782
866
|
<!-- Git Diff 面板 -->
|
|
@@ -877,7 +961,7 @@
|
|
|
877
961
|
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
878
962
|
var MOBILE_COLS = 60;
|
|
879
963
|
var fontSize = isMobile ? 11 : 13;
|
|
880
|
-
var currentMode = '
|
|
964
|
+
var currentMode = 'claude';
|
|
881
965
|
var isTransitioning = false;
|
|
882
966
|
var isBufferReplay = true; // 初始缓冲区回放中,不弹 toast
|
|
883
967
|
|
|
@@ -1640,18 +1724,197 @@
|
|
|
1640
1724
|
|
|
1641
1725
|
if (historyBarVisible) {
|
|
1642
1726
|
historyBar.classList.add('visible');
|
|
1727
|
+
// 重置视图:显示列表,隐藏详情
|
|
1728
|
+
document.getElementById('session-list-view').classList.remove('hidden');
|
|
1729
|
+
document.getElementById('claude-detail-view').classList.add('hidden');
|
|
1643
1730
|
if (currentMode === 'opencode') {
|
|
1731
|
+
document.getElementById('session-history-title').textContent = '历史会话';
|
|
1644
1732
|
loadSessions();
|
|
1645
1733
|
} else {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
sessionList.innerHTML = '<div class="session-empty">Claude Code 暂不支持历史会话</div>';
|
|
1734
|
+
document.getElementById('session-history-title').textContent = 'Claude 历史会话';
|
|
1735
|
+
loadClaudeSessions();
|
|
1649
1736
|
}
|
|
1650
1737
|
} else {
|
|
1651
1738
|
historyBar.classList.remove('visible');
|
|
1652
1739
|
}
|
|
1653
1740
|
}
|
|
1654
1741
|
|
|
1742
|
+
// Claude 会话功能
|
|
1743
|
+
function loadClaudeSessions() {
|
|
1744
|
+
var sessionList = document.getElementById('session-list');
|
|
1745
|
+
sessionList.innerHTML = '<div class="session-loading">加载 Claude 历史会话...</div>';
|
|
1746
|
+
|
|
1747
|
+
fetch(basePath + '/api/claude-sessions')
|
|
1748
|
+
.then(function(r) { return r.json(); })
|
|
1749
|
+
.then(function(data) {
|
|
1750
|
+
var sessions = data.sessions || [];
|
|
1751
|
+
if (sessions.length === 0) {
|
|
1752
|
+
sessionList.innerHTML = '<div class="session-empty">暂无 Claude 历史会话</div>';
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
sessionList.innerHTML = '';
|
|
1756
|
+
sessions.forEach(function(s) {
|
|
1757
|
+
var item = document.createElement('div');
|
|
1758
|
+
item.className = 'session-item';
|
|
1759
|
+
|
|
1760
|
+
var icon = document.createElement('div');
|
|
1761
|
+
icon.className = 'session-icon';
|
|
1762
|
+
icon.style.background = '#c586c0';
|
|
1763
|
+
|
|
1764
|
+
var info = document.createElement('div');
|
|
1765
|
+
info.className = 'session-info';
|
|
1766
|
+
|
|
1767
|
+
var title = document.createElement('div');
|
|
1768
|
+
title.className = 'session-title';
|
|
1769
|
+
title.textContent = s.preview || '(空会话)';
|
|
1770
|
+
|
|
1771
|
+
var meta = document.createElement('div');
|
|
1772
|
+
meta.className = 'session-meta';
|
|
1773
|
+
var time = document.createElement('span');
|
|
1774
|
+
time.className = 'session-time';
|
|
1775
|
+
time.textContent = formatTime(s.mtime);
|
|
1776
|
+
meta.appendChild(time);
|
|
1777
|
+
if (s.directory) {
|
|
1778
|
+
var dir = document.createElement('span');
|
|
1779
|
+
dir.className = 'session-dir';
|
|
1780
|
+
dir.textContent = s.directory.replace(/^\/Users\/[^\/]+/, '~');
|
|
1781
|
+
meta.appendChild(dir);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
info.appendChild(title);
|
|
1785
|
+
info.appendChild(meta);
|
|
1786
|
+
item.appendChild(icon);
|
|
1787
|
+
item.appendChild(info);
|
|
1788
|
+
|
|
1789
|
+
var deleteBtn = document.createElement('button');
|
|
1790
|
+
deleteBtn.className = 'session-delete-btn';
|
|
1791
|
+
deleteBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
1792
|
+
deleteBtn.title = '删除会话';
|
|
1793
|
+
deleteBtn.addEventListener('click', function(e) {
|
|
1794
|
+
e.stopPropagation();
|
|
1795
|
+
deleteClaudeSession(s.id, s.project, item);
|
|
1796
|
+
});
|
|
1797
|
+
item.appendChild(deleteBtn);
|
|
1798
|
+
|
|
1799
|
+
item.addEventListener('click', function() {
|
|
1800
|
+
restoreClaudeSession(s.id);
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
sessionList.appendChild(item);
|
|
1804
|
+
});
|
|
1805
|
+
})
|
|
1806
|
+
.catch(function(err) {
|
|
1807
|
+
sessionList.innerHTML = '<div class="session-empty">加载失败: ' + err.message + '</div>';
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function loadClaudeSessionDetail(sessionId, preview) {
|
|
1812
|
+
// 切换到详情视图
|
|
1813
|
+
document.getElementById('session-list-view').classList.add('hidden');
|
|
1814
|
+
var detailView = document.getElementById('claude-detail-view');
|
|
1815
|
+
detailView.classList.remove('hidden');
|
|
1816
|
+
document.getElementById('claude-detail-title').textContent = preview || sessionId;
|
|
1817
|
+
|
|
1818
|
+
var container = document.getElementById('claude-messages-container');
|
|
1819
|
+
container.innerHTML = '<div class="session-loading">加载对话内容...</div>';
|
|
1820
|
+
|
|
1821
|
+
fetch(basePath + '/api/claude-session-messages?id=' + encodeURIComponent(sessionId))
|
|
1822
|
+
.then(function(r) { return r.json(); })
|
|
1823
|
+
.then(function(data) {
|
|
1824
|
+
if (data.error) {
|
|
1825
|
+
container.innerHTML = '<div class="session-empty">' + escapeHtml(data.error) + '</div>';
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
var messages = data.messages || [];
|
|
1829
|
+
if (messages.length === 0) {
|
|
1830
|
+
container.innerHTML = '<div class="session-empty">无对话内容</div>';
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
container.innerHTML = '';
|
|
1834
|
+
messages.forEach(function(msg) {
|
|
1835
|
+
var div = document.createElement('div');
|
|
1836
|
+
div.className = 'claude-msg ' + (msg.role === 'user' ? 'claude-msg-user' : 'claude-msg-assistant');
|
|
1837
|
+
|
|
1838
|
+
var role = document.createElement('div');
|
|
1839
|
+
role.className = 'claude-msg-role';
|
|
1840
|
+
role.textContent = msg.role === 'user' ? 'User' : 'Assistant';
|
|
1841
|
+
|
|
1842
|
+
var text = document.createElement('div');
|
|
1843
|
+
text.className = 'claude-msg-text';
|
|
1844
|
+
text.textContent = msg.text;
|
|
1845
|
+
|
|
1846
|
+
div.appendChild(role);
|
|
1847
|
+
div.appendChild(text);
|
|
1848
|
+
|
|
1849
|
+
if (msg.timestamp) {
|
|
1850
|
+
var time = document.createElement('div');
|
|
1851
|
+
time.className = 'claude-msg-time';
|
|
1852
|
+
time.textContent = new Date(msg.timestamp).toLocaleString('zh-CN');
|
|
1853
|
+
div.appendChild(time);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
container.appendChild(div);
|
|
1857
|
+
});
|
|
1858
|
+
})
|
|
1859
|
+
.catch(function(err) {
|
|
1860
|
+
container.innerHTML = '<div class="session-empty">加载失败: ' + err.message + '</div>';
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function deleteClaudeSession(sessionId, project, itemEl) {
|
|
1865
|
+
if (!confirm('确定要删除这个会话吗?')) return;
|
|
1866
|
+
itemEl.style.opacity = '0.4';
|
|
1867
|
+
itemEl.style.pointerEvents = 'none';
|
|
1868
|
+
fetch(basePath + '/api/claude-session/' + encodeURIComponent(sessionId) + '?project=' + encodeURIComponent(project), { method: 'DELETE' })
|
|
1869
|
+
.then(function(r) { return r.json(); })
|
|
1870
|
+
.then(function(data) {
|
|
1871
|
+
if (data.ok) {
|
|
1872
|
+
itemEl.style.transition = 'all 0.2s';
|
|
1873
|
+
itemEl.style.maxHeight = '0';
|
|
1874
|
+
itemEl.style.overflow = 'hidden';
|
|
1875
|
+
itemEl.style.padding = '0 12px';
|
|
1876
|
+
itemEl.style.margin = '0';
|
|
1877
|
+
itemEl.style.opacity = '0';
|
|
1878
|
+
setTimeout(function() { itemEl.remove(); }, 200);
|
|
1879
|
+
} else {
|
|
1880
|
+
itemEl.style.opacity = '';
|
|
1881
|
+
itemEl.style.pointerEvents = '';
|
|
1882
|
+
alert('删除失败: ' + (data.error || '未知错误'));
|
|
1883
|
+
}
|
|
1884
|
+
})
|
|
1885
|
+
.catch(function(err) {
|
|
1886
|
+
itemEl.style.opacity = '';
|
|
1887
|
+
itemEl.style.pointerEvents = '';
|
|
1888
|
+
alert('删除失败: ' + err.message);
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function restoreClaudeSession(sessionId) {
|
|
1893
|
+
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
1894
|
+
toggleHistoryBar();
|
|
1895
|
+
term.reset();
|
|
1896
|
+
if (ws && ws.readyState === 1) {
|
|
1897
|
+
if (currentMode !== 'claude') {
|
|
1898
|
+
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
ws.send(JSON.stringify({ type: 'restore', sessionId: sessionId }));
|
|
1902
|
+
} else {
|
|
1903
|
+
term.write('错误: WebSocket 未连接\r\n');
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Claude 详情返回按钮
|
|
1908
|
+
document.getElementById('claude-detail-back').addEventListener('click', function() {
|
|
1909
|
+
document.getElementById('claude-detail-view').classList.add('hidden');
|
|
1910
|
+
document.getElementById('session-list-view').classList.remove('hidden');
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// Claude 详情关闭按钮
|
|
1914
|
+
document.getElementById('claude-detail-close').addEventListener('click', function() {
|
|
1915
|
+
toggleHistoryBar();
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1655
1918
|
// 新会话按钮
|
|
1656
1919
|
var isCreatingNewSession = false;
|
|
1657
1920
|
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
@@ -1669,7 +1932,11 @@
|
|
|
1669
1932
|
// 刷新会话列表
|
|
1670
1933
|
document.getElementById('refresh-sessions').addEventListener('click', function(e) {
|
|
1671
1934
|
e.stopPropagation();
|
|
1672
|
-
|
|
1935
|
+
if (currentMode === 'claude') {
|
|
1936
|
+
loadClaudeSessions();
|
|
1937
|
+
} else {
|
|
1938
|
+
loadSessions();
|
|
1939
|
+
}
|
|
1673
1940
|
});
|
|
1674
1941
|
|
|
1675
1942
|
// 关闭历史栏
|
package/index.html
CHANGED
|
@@ -257,6 +257,67 @@
|
|
|
257
257
|
border-radius: 3px;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/* Claude 会话详情视图 */
|
|
261
|
+
#claude-detail-view {
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
flex: 1;
|
|
265
|
+
min-height: 0;
|
|
266
|
+
}
|
|
267
|
+
#claude-detail-view.hidden { display: none; }
|
|
268
|
+
|
|
269
|
+
#claude-detail-header {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
padding: 10px 12px;
|
|
273
|
+
background: #111;
|
|
274
|
+
border-bottom: 1px solid #222;
|
|
275
|
+
flex-shrink: 0;
|
|
276
|
+
gap: 8px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#claude-messages-container {
|
|
280
|
+
flex: 1;
|
|
281
|
+
overflow-y: auto;
|
|
282
|
+
padding: 10px 12px;
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
gap: 10px;
|
|
286
|
+
-webkit-overflow-scrolling: touch;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.claude-msg {
|
|
290
|
+
padding: 8px 12px;
|
|
291
|
+
border-radius: 6px;
|
|
292
|
+
font-size: 13px;
|
|
293
|
+
line-height: 1.6;
|
|
294
|
+
white-space: pre-wrap;
|
|
295
|
+
word-break: break-word;
|
|
296
|
+
}
|
|
297
|
+
.claude-msg-user {
|
|
298
|
+
background: #1a2a3a;
|
|
299
|
+
border-left: 3px solid #4a9eff;
|
|
300
|
+
}
|
|
301
|
+
.claude-msg-assistant {
|
|
302
|
+
background: #1a1a2a;
|
|
303
|
+
border-left: 3px solid #c586c0;
|
|
304
|
+
}
|
|
305
|
+
.claude-msg-role {
|
|
306
|
+
font-size: 11px;
|
|
307
|
+
color: #888;
|
|
308
|
+
margin-bottom: 4px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
}
|
|
311
|
+
.claude-msg-user .claude-msg-role { color: #4a9eff; }
|
|
312
|
+
.claude-msg-assistant .claude-msg-role { color: #c586c0; }
|
|
313
|
+
.claude-msg-text { color: #d4d4d4; }
|
|
314
|
+
.claude-msg-time {
|
|
315
|
+
font-size: 10px;
|
|
316
|
+
color: #666;
|
|
317
|
+
margin-top: 4px;
|
|
318
|
+
text-align: right;
|
|
319
|
+
}
|
|
320
|
+
|
|
260
321
|
#mode-switcher {
|
|
261
322
|
display: flex;
|
|
262
323
|
gap: 4px;
|
|
@@ -852,6 +913,27 @@
|
|
|
852
913
|
</div>
|
|
853
914
|
</div>
|
|
854
915
|
|
|
916
|
+
<!-- Claude 会话详情视图 -->
|
|
917
|
+
<div id="claude-detail-view" class="hidden">
|
|
918
|
+
<div id="claude-detail-header">
|
|
919
|
+
<button class="history-toggle-btn" id="claude-detail-back">
|
|
920
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
921
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
922
|
+
</svg>
|
|
923
|
+
<span>返回</span>
|
|
924
|
+
</button>
|
|
925
|
+
<div id="claude-detail-title" style="flex:1; text-align:center; color:#ddd; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"></div>
|
|
926
|
+
<button class="history-toggle-btn" id="claude-detail-close">
|
|
927
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
928
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
929
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
930
|
+
</svg>
|
|
931
|
+
<span>关闭</span>
|
|
932
|
+
</button>
|
|
933
|
+
</div>
|
|
934
|
+
<div id="claude-messages-container"></div>
|
|
935
|
+
</div>
|
|
936
|
+
|
|
855
937
|
</div>
|
|
856
938
|
|
|
857
939
|
<!-- Git Diff 面板 -->
|
|
@@ -968,7 +1050,7 @@
|
|
|
968
1050
|
var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
969
1051
|
var MOBILE_COLS = 60;
|
|
970
1052
|
var fontSize = isMobile ? 11 : 13;
|
|
971
|
-
var currentMode = '
|
|
1053
|
+
var currentMode = 'claude';
|
|
972
1054
|
var isTransitioning = false;
|
|
973
1055
|
|
|
974
1056
|
var term = new Terminal({
|
|
@@ -1868,18 +1950,193 @@
|
|
|
1868
1950
|
|
|
1869
1951
|
if (historyBarVisible) {
|
|
1870
1952
|
historyBar.classList.add('visible');
|
|
1953
|
+
document.getElementById('session-list-view').classList.remove('hidden');
|
|
1954
|
+
document.getElementById('claude-detail-view').classList.add('hidden');
|
|
1871
1955
|
if (currentMode === 'opencode') {
|
|
1956
|
+
document.getElementById('session-history-title').textContent = '历史会话';
|
|
1872
1957
|
loadSessions();
|
|
1873
1958
|
} else {
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
sessionList.innerHTML = '<div class="session-empty">Claude Code 暂不支持历史会话</div>';
|
|
1959
|
+
document.getElementById('session-history-title').textContent = 'Claude 历史会话';
|
|
1960
|
+
loadClaudeSessions();
|
|
1877
1961
|
}
|
|
1878
1962
|
} else {
|
|
1879
1963
|
historyBar.classList.remove('visible');
|
|
1880
1964
|
}
|
|
1881
1965
|
}
|
|
1882
1966
|
|
|
1967
|
+
// Claude 会话功能
|
|
1968
|
+
function loadClaudeSessions() {
|
|
1969
|
+
var sessionList = document.getElementById('session-list');
|
|
1970
|
+
sessionList.innerHTML = '<div class="session-loading">加载 Claude 历史会话...</div>';
|
|
1971
|
+
|
|
1972
|
+
fetch(basePath + '/api/claude-sessions')
|
|
1973
|
+
.then(function(r) { return r.json(); })
|
|
1974
|
+
.then(function(data) {
|
|
1975
|
+
var sessions = data.sessions || [];
|
|
1976
|
+
if (sessions.length === 0) {
|
|
1977
|
+
sessionList.innerHTML = '<div class="session-empty">暂无 Claude 历史会话</div>';
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
sessionList.innerHTML = '';
|
|
1981
|
+
sessions.forEach(function(s) {
|
|
1982
|
+
var item = document.createElement('div');
|
|
1983
|
+
item.className = 'session-item';
|
|
1984
|
+
|
|
1985
|
+
var icon = document.createElement('div');
|
|
1986
|
+
icon.className = 'session-icon';
|
|
1987
|
+
icon.style.background = '#c586c0';
|
|
1988
|
+
|
|
1989
|
+
var info = document.createElement('div');
|
|
1990
|
+
info.className = 'session-info';
|
|
1991
|
+
|
|
1992
|
+
var title = document.createElement('div');
|
|
1993
|
+
title.className = 'session-title';
|
|
1994
|
+
title.textContent = s.preview || '(空会话)';
|
|
1995
|
+
|
|
1996
|
+
var meta = document.createElement('div');
|
|
1997
|
+
meta.className = 'session-meta';
|
|
1998
|
+
var time = document.createElement('span');
|
|
1999
|
+
time.className = 'session-time';
|
|
2000
|
+
time.textContent = formatTime(s.mtime);
|
|
2001
|
+
meta.appendChild(time);
|
|
2002
|
+
if (s.directory) {
|
|
2003
|
+
var dir = document.createElement('span');
|
|
2004
|
+
dir.className = 'session-dir';
|
|
2005
|
+
dir.textContent = s.directory.replace(/^\/Users\/[^\/]+/, '~');
|
|
2006
|
+
meta.appendChild(dir);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
info.appendChild(title);
|
|
2010
|
+
info.appendChild(meta);
|
|
2011
|
+
item.appendChild(icon);
|
|
2012
|
+
item.appendChild(info);
|
|
2013
|
+
|
|
2014
|
+
var deleteBtn = document.createElement('button');
|
|
2015
|
+
deleteBtn.className = 'session-delete-btn';
|
|
2016
|
+
deleteBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
2017
|
+
deleteBtn.title = '删除会话';
|
|
2018
|
+
deleteBtn.addEventListener('click', function(e) {
|
|
2019
|
+
e.stopPropagation();
|
|
2020
|
+
deleteClaudeSession(s.id, s.project, item);
|
|
2021
|
+
});
|
|
2022
|
+
item.appendChild(deleteBtn);
|
|
2023
|
+
|
|
2024
|
+
item.addEventListener('click', function() {
|
|
2025
|
+
restoreClaudeSession(s.id);
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
sessionList.appendChild(item);
|
|
2029
|
+
});
|
|
2030
|
+
})
|
|
2031
|
+
.catch(function(err) {
|
|
2032
|
+
sessionList.innerHTML = '<div class="session-empty">加载失败: ' + err.message + '</div>';
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function loadClaudeSessionDetail(sessionId, preview) {
|
|
2037
|
+
document.getElementById('session-list-view').classList.add('hidden');
|
|
2038
|
+
var detailView = document.getElementById('claude-detail-view');
|
|
2039
|
+
detailView.classList.remove('hidden');
|
|
2040
|
+
document.getElementById('claude-detail-title').textContent = preview || sessionId;
|
|
2041
|
+
|
|
2042
|
+
var container = document.getElementById('claude-messages-container');
|
|
2043
|
+
container.innerHTML = '<div class="session-loading">加载对话内容...</div>';
|
|
2044
|
+
|
|
2045
|
+
fetch(basePath + '/api/claude-session-messages?id=' + encodeURIComponent(sessionId))
|
|
2046
|
+
.then(function(r) { return r.json(); })
|
|
2047
|
+
.then(function(data) {
|
|
2048
|
+
if (data.error) {
|
|
2049
|
+
container.innerHTML = '<div class="session-empty">' + escapeHtml(data.error) + '</div>';
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
var messages = data.messages || [];
|
|
2053
|
+
if (messages.length === 0) {
|
|
2054
|
+
container.innerHTML = '<div class="session-empty">无对话内容</div>';
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
container.innerHTML = '';
|
|
2058
|
+
messages.forEach(function(msg) {
|
|
2059
|
+
var div = document.createElement('div');
|
|
2060
|
+
div.className = 'claude-msg ' + (msg.role === 'user' ? 'claude-msg-user' : 'claude-msg-assistant');
|
|
2061
|
+
|
|
2062
|
+
var role = document.createElement('div');
|
|
2063
|
+
role.className = 'claude-msg-role';
|
|
2064
|
+
role.textContent = msg.role === 'user' ? 'User' : 'Assistant';
|
|
2065
|
+
|
|
2066
|
+
var text = document.createElement('div');
|
|
2067
|
+
text.className = 'claude-msg-text';
|
|
2068
|
+
text.textContent = msg.text;
|
|
2069
|
+
|
|
2070
|
+
div.appendChild(role);
|
|
2071
|
+
div.appendChild(text);
|
|
2072
|
+
|
|
2073
|
+
if (msg.timestamp) {
|
|
2074
|
+
var time = document.createElement('div');
|
|
2075
|
+
time.className = 'claude-msg-time';
|
|
2076
|
+
time.textContent = new Date(msg.timestamp).toLocaleString('zh-CN');
|
|
2077
|
+
div.appendChild(time);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
container.appendChild(div);
|
|
2081
|
+
});
|
|
2082
|
+
})
|
|
2083
|
+
.catch(function(err) {
|
|
2084
|
+
container.innerHTML = '<div class="session-empty">加载失败: ' + err.message + '</div>';
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function deleteClaudeSession(sessionId, project, itemEl) {
|
|
2089
|
+
if (!confirm('确定要删除这个会话吗?')) return;
|
|
2090
|
+
itemEl.style.opacity = '0.4';
|
|
2091
|
+
itemEl.style.pointerEvents = 'none';
|
|
2092
|
+
fetch(basePath + '/api/claude-session/' + encodeURIComponent(sessionId) + '?project=' + encodeURIComponent(project), { method: 'DELETE' })
|
|
2093
|
+
.then(function(r) { return r.json(); })
|
|
2094
|
+
.then(function(data) {
|
|
2095
|
+
if (data.ok) {
|
|
2096
|
+
itemEl.style.transition = 'all 0.2s';
|
|
2097
|
+
itemEl.style.maxHeight = '0';
|
|
2098
|
+
itemEl.style.overflow = 'hidden';
|
|
2099
|
+
itemEl.style.padding = '0 12px';
|
|
2100
|
+
itemEl.style.margin = '0';
|
|
2101
|
+
itemEl.style.opacity = '0';
|
|
2102
|
+
setTimeout(function() { itemEl.remove(); }, 200);
|
|
2103
|
+
} else {
|
|
2104
|
+
itemEl.style.opacity = '';
|
|
2105
|
+
itemEl.style.pointerEvents = '';
|
|
2106
|
+
alert('删除失败: ' + (data.error || '未知错误'));
|
|
2107
|
+
}
|
|
2108
|
+
})
|
|
2109
|
+
.catch(function(err) {
|
|
2110
|
+
itemEl.style.opacity = '';
|
|
2111
|
+
itemEl.style.pointerEvents = '';
|
|
2112
|
+
alert('删除失败: ' + err.message);
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function restoreClaudeSession(sessionId) {
|
|
2117
|
+
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
2118
|
+
toggleHistoryBar();
|
|
2119
|
+
term.reset();
|
|
2120
|
+
if (ws && ws.readyState === 1) {
|
|
2121
|
+
if (currentMode !== 'claude') {
|
|
2122
|
+
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
ws.send(JSON.stringify({ type: 'restore', sessionId: sessionId }));
|
|
2126
|
+
} else {
|
|
2127
|
+
term.write('错误: WebSocket 未连接\r\n');
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
document.getElementById('claude-detail-back').addEventListener('click', function() {
|
|
2132
|
+
document.getElementById('claude-detail-view').classList.add('hidden');
|
|
2133
|
+
document.getElementById('session-list-view').classList.remove('hidden');
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
document.getElementById('claude-detail-close').addEventListener('click', function() {
|
|
2137
|
+
toggleHistoryBar();
|
|
2138
|
+
});
|
|
2139
|
+
|
|
1883
2140
|
// 新会话按钮
|
|
1884
2141
|
var isCreatingNewSession = false;
|
|
1885
2142
|
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
@@ -1897,7 +2154,11 @@
|
|
|
1897
2154
|
// 刷新会话列表
|
|
1898
2155
|
document.getElementById('refresh-sessions').addEventListener('click', function(e) {
|
|
1899
2156
|
e.stopPropagation();
|
|
1900
|
-
|
|
2157
|
+
if (currentMode === 'claude') {
|
|
2158
|
+
loadClaudeSessions();
|
|
2159
|
+
} else {
|
|
2160
|
+
loadSessions();
|
|
2161
|
+
}
|
|
1901
2162
|
});
|
|
1902
2163
|
|
|
1903
2164
|
// 关闭历史栏
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -53,6 +53,9 @@ async function getOrCreateCert() {
|
|
|
53
53
|
return { key: pems.private, cert: pems.cert };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Claude Code 数据目录(Sandbox 下优先读 NAS 上的 /halo/.claude,本地回退 ~/.claude)
|
|
57
|
+
const CLAUDE_HOME = existsSync('/halo/.claude') ? '/halo/.claude' : join(homedir(), '.claude');
|
|
58
|
+
|
|
56
59
|
// OpenCode 数据库路径(优先读 OPENCODE_DB 环境变量,兼容本地盘 DB 方案)
|
|
57
60
|
const OPENCODE_DB_PATH = process.env.OPENCODE_DB || process.env.OPENCODE_DB_PATH || join(
|
|
58
61
|
process.env.XDG_DATA_HOME || join(homedir(), '.local/share'),
|
|
@@ -99,7 +102,7 @@ let sessionStartTime = 0; // 新会话启动时间,用于限制查询范围
|
|
|
99
102
|
let previousSessionId = null; // 用于判断新会话是否已写入 DB
|
|
100
103
|
const clientSizes = new Map();
|
|
101
104
|
const mobileClients = new Set();
|
|
102
|
-
let currentMode = '
|
|
105
|
+
let currentMode = 'claude';
|
|
103
106
|
let isSwitching = false; // 模式切换/恢复会话中,忽略旧进程退出事件
|
|
104
107
|
|
|
105
108
|
function getMobileSize() {
|
|
@@ -142,24 +145,21 @@ function getLocalIp() {
|
|
|
142
145
|
|
|
143
146
|
function findCommand(cmd) {
|
|
144
147
|
const paths = [
|
|
145
|
-
cmd,
|
|
146
148
|
'/opt/homebrew/bin/' + cmd,
|
|
147
149
|
'/usr/local/bin/' + cmd,
|
|
148
150
|
'/usr/bin/' + cmd,
|
|
149
151
|
join(process.env.HOME, '.local/bin/' + cmd),
|
|
150
|
-
join(process.env.HOME, '.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js'),
|
|
151
152
|
];
|
|
153
|
+
if (cmd === 'claude') {
|
|
154
|
+
paths.push(join(process.env.HOME, '.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js'));
|
|
155
|
+
}
|
|
152
156
|
for (const p of paths) {
|
|
153
|
-
if (
|
|
154
|
-
if (p === 'claude' && existsSync(p)) return p;
|
|
155
|
-
if (p === 'claude' && p.includes('@anthropic-ai')) {
|
|
156
|
-
try {
|
|
157
|
-
return execSync(`which claude`, { encoding: 'utf8' }).trim();
|
|
158
|
-
} catch {}
|
|
159
|
-
}
|
|
157
|
+
if (existsSync(p)) return p;
|
|
160
158
|
}
|
|
159
|
+
// fallback: which (non-interactive shell, no aliases)
|
|
161
160
|
try {
|
|
162
|
-
|
|
161
|
+
const result = execSync(`command -v ${cmd} 2>/dev/null || which ${cmd} 2>/dev/null`, { encoding: 'utf8', shell: '/bin/sh' }).trim();
|
|
162
|
+
if (result && !result.includes('alias') && existsSync(result)) return result;
|
|
163
163
|
} catch {}
|
|
164
164
|
return cmd;
|
|
165
165
|
}
|
|
@@ -202,10 +202,11 @@ function killProcessTree(proc) {
|
|
|
202
202
|
// 清理孤儿进程(PPID=1 的 opencode/claude)
|
|
203
203
|
function cleanupOrphanProcesses() {
|
|
204
204
|
try {
|
|
205
|
+
const myPid = process.pid;
|
|
205
206
|
const orphans = execSync(
|
|
206
207
|
"ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && (/opencode/||/claude/) {print $1}'",
|
|
207
208
|
{ encoding: 'utf-8', timeout: 5000 }
|
|
208
|
-
).trim().split(/\s+/).filter(Boolean).map(Number);
|
|
209
|
+
).trim().split(/\s+/).filter(Boolean).map(Number).filter(pid => pid !== myPid);
|
|
209
210
|
for (const pid of orphans) {
|
|
210
211
|
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
211
212
|
}
|
|
@@ -226,6 +227,11 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
226
227
|
} else {
|
|
227
228
|
command = claudePath;
|
|
228
229
|
}
|
|
230
|
+
// 如果提供了 sessionId,添加 --resume 参数
|
|
231
|
+
if (sessionId) {
|
|
232
|
+
args.push('--resume', sessionId);
|
|
233
|
+
LOG(`[claude] 恢复会话: ${sessionId}`);
|
|
234
|
+
}
|
|
229
235
|
} else {
|
|
230
236
|
command = findCommand('opencode');
|
|
231
237
|
// 如果提供了 sessionId,添加 --session 参数
|
|
@@ -239,7 +245,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
239
245
|
|
|
240
246
|
// 恢复会话时,使用会话记录的工作目录
|
|
241
247
|
let spawnCwd = process.cwd();
|
|
242
|
-
if (sessionId) {
|
|
248
|
+
if (sessionId && mode === 'opencode') {
|
|
243
249
|
try {
|
|
244
250
|
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
245
251
|
const session = db.prepare('SELECT directory FROM session WHERE id = ?').get(sessionId);
|
|
@@ -251,6 +257,33 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
251
257
|
} catch (e) {
|
|
252
258
|
LOG('[opencode] 查询会话目录失败:', e.message);
|
|
253
259
|
}
|
|
260
|
+
} else if (sessionId && mode === 'claude') {
|
|
261
|
+
// 从 JSONL 读取 cwd
|
|
262
|
+
try {
|
|
263
|
+
const projectsDir = join(CLAUDE_HOME, 'projects');
|
|
264
|
+
// 在所有项目目录中查找该会话文件
|
|
265
|
+
for (const projDir of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
266
|
+
if (!projDir.isDirectory()) continue;
|
|
267
|
+
const filePath = join(projectsDir, projDir.name, sessionId + '.jsonl');
|
|
268
|
+
if (existsSync(filePath)) {
|
|
269
|
+
const lines = readFileSync(filePath, 'utf-8').split('\n').slice(0, 30);
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
if (!line.trim()) continue;
|
|
272
|
+
try {
|
|
273
|
+
const d = JSON.parse(line);
|
|
274
|
+
if (d.cwd && existsSync(d.cwd)) {
|
|
275
|
+
spawnCwd = d.cwd;
|
|
276
|
+
LOG(`[claude] 使用会话目录: ${spawnCwd}`);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {
|
|
285
|
+
LOG('[claude] 查询会话目录失败:', e.message);
|
|
286
|
+
}
|
|
254
287
|
}
|
|
255
288
|
|
|
256
289
|
const proc = pty.spawn(command, args, {
|
|
@@ -609,7 +642,7 @@ const requestHandler = async (req, res) => {
|
|
|
609
642
|
'Access-Control-Allow-Origin': '*',
|
|
610
643
|
});
|
|
611
644
|
try {
|
|
612
|
-
const gitCwd =
|
|
645
|
+
const gitCwd = process.env.PROJECT_DIR || process.cwd();
|
|
613
646
|
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
614
647
|
cwd: gitCwd, encoding: 'utf-8', timeout: 5000,
|
|
615
648
|
});
|
|
@@ -618,8 +651,8 @@ const requestHandler = async (req, res) => {
|
|
|
618
651
|
file: line.substring(3),
|
|
619
652
|
})).filter(c => !/^core-/.test(c.file));
|
|
620
653
|
res.end(JSON.stringify({ changes }));
|
|
621
|
-
} catch
|
|
622
|
-
res.end(JSON.stringify({ changes: []
|
|
654
|
+
} catch {
|
|
655
|
+
res.end(JSON.stringify({ changes: [] }));
|
|
623
656
|
}
|
|
624
657
|
return;
|
|
625
658
|
}
|
|
@@ -638,7 +671,7 @@ const requestHandler = async (req, res) => {
|
|
|
638
671
|
}
|
|
639
672
|
const fileList = files.split(',').filter(Boolean);
|
|
640
673
|
const diffs = [];
|
|
641
|
-
const cwd =
|
|
674
|
+
const cwd = process.env.PROJECT_DIR || process.cwd();
|
|
642
675
|
for (const file of fileList) {
|
|
643
676
|
if (file.includes('..') || file.startsWith('/')) continue;
|
|
644
677
|
try {
|
|
@@ -749,6 +782,117 @@ const requestHandler = async (req, res) => {
|
|
|
749
782
|
return;
|
|
750
783
|
}
|
|
751
784
|
|
|
785
|
+
// API: Claude Code 会话列表(扫描所有项目)
|
|
786
|
+
if (req.url === '/api/claude-sessions') {
|
|
787
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
788
|
+
try {
|
|
789
|
+
const projectsDir = join(CLAUDE_HOME, 'projects');
|
|
790
|
+
if (!existsSync(projectsDir)) {
|
|
791
|
+
res.end(JSON.stringify({ sessions: [] }));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const allSessions = [];
|
|
795
|
+
// 遍历所有项目目录
|
|
796
|
+
for (const projDir of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
797
|
+
if (!projDir.isDirectory()) continue;
|
|
798
|
+
const projPath = join(projectsDir, projDir.name);
|
|
799
|
+
// 解码项目路径: -Users-jason-xxx → /Users/jason/xxx
|
|
800
|
+
const decodedPath = projDir.name.replace(/^-/, '/').replace(/-/g, '/');
|
|
801
|
+
for (const f of readdirSync(projPath)) {
|
|
802
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
803
|
+
try {
|
|
804
|
+
const fullPath = join(projPath, f);
|
|
805
|
+
const st = statSync(fullPath);
|
|
806
|
+
const id = f.replace('.jsonl', '');
|
|
807
|
+
let preview = '', cwd = '';
|
|
808
|
+
// 读取前 30 行提取第一条用户消息和 cwd
|
|
809
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
810
|
+
const lines = content.split('\n').slice(0, 30);
|
|
811
|
+
for (const line of lines) {
|
|
812
|
+
if (!line.trim()) continue;
|
|
813
|
+
try {
|
|
814
|
+
const d = JSON.parse(line);
|
|
815
|
+
if (d.type === 'user' && d.message) {
|
|
816
|
+
if (!cwd && d.cwd) cwd = d.cwd;
|
|
817
|
+
if (!preview) {
|
|
818
|
+
const c = d.message.content;
|
|
819
|
+
if (typeof c === 'string') {
|
|
820
|
+
preview = c.slice(0, 200);
|
|
821
|
+
} else if (Array.isArray(c)) {
|
|
822
|
+
for (const item of c) {
|
|
823
|
+
if (item.type === 'text' && item.text) {
|
|
824
|
+
preview = item.text.slice(0, 200);
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
} catch {}
|
|
833
|
+
}
|
|
834
|
+
allSessions.push({ id, preview, mtime: st.mtimeMs, project: projDir.name, directory: cwd || decodedPath });
|
|
835
|
+
} catch {}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
allSessions.sort((a, b) => b.mtime - a.mtime);
|
|
839
|
+
res.end(JSON.stringify({ sessions: allSessions.slice(0, 50) }));
|
|
840
|
+
} catch (err) {
|
|
841
|
+
res.end(JSON.stringify({ sessions: [], error: err.message }));
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// API: Claude Code 会话消息详情
|
|
847
|
+
if (req.url?.startsWith('/api/claude-session-messages')) {
|
|
848
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
849
|
+
const id = url.searchParams.get('id');
|
|
850
|
+
const project = url.searchParams.get('project');
|
|
851
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
852
|
+
if (!id || /[\/\\]|\.\./.test(id) || !project || /[\/\\]|\.\./.test(project)) {
|
|
853
|
+
res.end(JSON.stringify({ error: '无效参数' }));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
const filePath = join(CLAUDE_HOME, 'projects', project, id + '.jsonl');
|
|
858
|
+
if (!existsSync(filePath)) {
|
|
859
|
+
res.end(JSON.stringify({ error: '会话不存在' }));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
863
|
+
const messages = [];
|
|
864
|
+
for (const line of content.split('\n')) {
|
|
865
|
+
if (!line.trim()) continue;
|
|
866
|
+
try {
|
|
867
|
+
const d = JSON.parse(line);
|
|
868
|
+
if (d.type === 'user' && d.message) {
|
|
869
|
+
let text = '';
|
|
870
|
+
const c = d.message.content;
|
|
871
|
+
if (typeof c === 'string') {
|
|
872
|
+
text = c;
|
|
873
|
+
} else if (Array.isArray(c)) {
|
|
874
|
+
text = c.filter(i => i.type === 'text').map(i => i.text).join('\n');
|
|
875
|
+
}
|
|
876
|
+
if (text) messages.push({ role: 'user', text, timestamp: d.timestamp });
|
|
877
|
+
} else if ((d.type === 'assistant' || d.message?.role === 'assistant') && d.message?.content) {
|
|
878
|
+
const c = d.message.content;
|
|
879
|
+
let text = '';
|
|
880
|
+
if (typeof c === 'string') {
|
|
881
|
+
text = c;
|
|
882
|
+
} else if (Array.isArray(c)) {
|
|
883
|
+
text = c.filter(i => i.type === 'text').map(i => i.text).join('\n');
|
|
884
|
+
}
|
|
885
|
+
if (text) messages.push({ role: 'assistant', text, timestamp: d.timestamp });
|
|
886
|
+
}
|
|
887
|
+
} catch {}
|
|
888
|
+
}
|
|
889
|
+
res.end(JSON.stringify({ messages }));
|
|
890
|
+
} catch (err) {
|
|
891
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
752
896
|
// API: 软删除会话(设置 time_archived)
|
|
753
897
|
if (req.method === 'DELETE' && req.url?.startsWith('/api/session/')) {
|
|
754
898
|
const sessionId = req.url.split('/').pop();
|
|
@@ -773,6 +917,31 @@ const requestHandler = async (req, res) => {
|
|
|
773
917
|
return;
|
|
774
918
|
}
|
|
775
919
|
|
|
920
|
+
// API: 删除 Claude 会话(删除 JSONL 文件)
|
|
921
|
+
if (req.method === 'DELETE' && req.url?.startsWith('/api/claude-session/')) {
|
|
922
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
923
|
+
const id = url.pathname.split('/').pop();
|
|
924
|
+
const project = url.searchParams.get('project');
|
|
925
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
926
|
+
if (!id || /[\/\\]|\.\./.test(id) || !project || /[\/\\]|\.\./.test(project)) {
|
|
927
|
+
res.end(JSON.stringify({ ok: false, error: '无效参数' }));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const { unlinkSync } = await import('node:fs');
|
|
932
|
+
const filePath = join(CLAUDE_HOME, 'projects', project, id + '.jsonl');
|
|
933
|
+
if (!existsSync(filePath)) {
|
|
934
|
+
res.end(JSON.stringify({ ok: false, error: '文件不存在' }));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
unlinkSync(filePath);
|
|
938
|
+
res.end(JSON.stringify({ ok: true }));
|
|
939
|
+
} catch (err) {
|
|
940
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
776
945
|
// API: 获取会话消息
|
|
777
946
|
if (req.url?.startsWith('/api/session/')) {
|
|
778
947
|
const sessionId = req.url.split('/').pop();
|
|
@@ -940,21 +1109,26 @@ wssInst.on('connection', (ws, req) => {
|
|
|
940
1109
|
}, 100);
|
|
941
1110
|
}
|
|
942
1111
|
} else if (msg.type === 'restore') {
|
|
943
|
-
//
|
|
944
|
-
if (msg.sessionId
|
|
945
|
-
LOG(`[restore]
|
|
1112
|
+
// 恢复会话(支持 opencode 和 claude)
|
|
1113
|
+
if (msg.sessionId) {
|
|
1114
|
+
LOG(`[restore] 恢复 ${currentMode} 会话: ${msg.sessionId}`);
|
|
946
1115
|
|
|
947
1116
|
isSwitching = true;
|
|
948
1117
|
|
|
949
|
-
//
|
|
950
|
-
|
|
1118
|
+
// 杀死当前进程
|
|
1119
|
+
const proc = currentMode === 'opencode' ? opencodeProcess : claudeProcess;
|
|
1120
|
+
if (proc) {
|
|
951
1121
|
try {
|
|
952
|
-
LOG(`[restore] 杀死当前进程 PID: ${
|
|
953
|
-
killProcessTree(
|
|
1122
|
+
LOG(`[restore] 杀死当前进程 PID: ${proc.pid}`);
|
|
1123
|
+
killProcessTree(proc);
|
|
954
1124
|
} catch (e) {
|
|
955
1125
|
LOG('[restore] 杀死进程失败:', e.message);
|
|
956
1126
|
}
|
|
957
|
-
|
|
1127
|
+
if (currentMode === 'opencode') {
|
|
1128
|
+
opencodeProcess = null;
|
|
1129
|
+
} else {
|
|
1130
|
+
claudeProcess = null;
|
|
1131
|
+
}
|
|
958
1132
|
currentProcess = null;
|
|
959
1133
|
}
|
|
960
1134
|
|
|
@@ -965,9 +1139,9 @@ wssInst.on('connection', (ws, req) => {
|
|
|
965
1139
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
966
1140
|
cleanupOrphanProcesses();
|
|
967
1141
|
|
|
968
|
-
//
|
|
1142
|
+
// 启动进程,传入 session ID
|
|
969
1143
|
try {
|
|
970
|
-
await spawnProcess(
|
|
1144
|
+
await spawnProcess(currentMode, msg.sessionId);
|
|
971
1145
|
ws.send(JSON.stringify({ type: 'restored', sessionId: msg.sessionId }));
|
|
972
1146
|
} catch (e) {
|
|
973
1147
|
LOG('[restore] 启动进程失败:', e.message);
|
|
@@ -1124,21 +1298,52 @@ function startServer() {
|
|
|
1124
1298
|
cleanupOrphanProcesses();
|
|
1125
1299
|
|
|
1126
1300
|
// 尝试恢复最近的会话,如果没有则新建
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1301
|
+
if (currentMode === 'opencode') {
|
|
1302
|
+
let lastSessionId = null;
|
|
1303
|
+
try {
|
|
1304
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
1305
|
+
const row = db.prepare(
|
|
1306
|
+
`SELECT id FROM session WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_updated DESC LIMIT 1`
|
|
1307
|
+
).get();
|
|
1308
|
+
db.close();
|
|
1309
|
+
if (row) lastSessionId = row.id;
|
|
1310
|
+
} catch (e) {}
|
|
1136
1311
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1312
|
+
if (lastSessionId) {
|
|
1313
|
+
LOG(`[startup] 恢复最近会话: ${lastSessionId}`);
|
|
1314
|
+
await spawnProcess('opencode', lastSessionId);
|
|
1315
|
+
} else {
|
|
1316
|
+
await spawnProcess('opencode');
|
|
1317
|
+
}
|
|
1140
1318
|
} else {
|
|
1141
|
-
|
|
1319
|
+
// Claude 模式:找最近的会话恢复
|
|
1320
|
+
let lastClaudeSession = null;
|
|
1321
|
+
try {
|
|
1322
|
+
const projectsDir = join(CLAUDE_HOME, 'projects');
|
|
1323
|
+
if (existsSync(projectsDir)) {
|
|
1324
|
+
let newest = 0;
|
|
1325
|
+
for (const projDir of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
1326
|
+
if (!projDir.isDirectory()) continue;
|
|
1327
|
+
const projPath = join(projectsDir, projDir.name);
|
|
1328
|
+
for (const f of readdirSync(projPath)) {
|
|
1329
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
1330
|
+
try {
|
|
1331
|
+
const st = statSync(join(projPath, f));
|
|
1332
|
+
if (st.mtimeMs > newest) {
|
|
1333
|
+
newest = st.mtimeMs;
|
|
1334
|
+
lastClaudeSession = f.replace('.jsonl', '');
|
|
1335
|
+
}
|
|
1336
|
+
} catch {}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
} catch {}
|
|
1341
|
+
if (lastClaudeSession) {
|
|
1342
|
+
LOG(`[startup] 恢复最近Claude会话: ${lastClaudeSession}`);
|
|
1343
|
+
await spawnProcess('claude', lastClaudeSession);
|
|
1344
|
+
} else {
|
|
1345
|
+
await spawnProcess('claude');
|
|
1346
|
+
}
|
|
1142
1347
|
}
|
|
1143
1348
|
|
|
1144
1349
|
// 启动首次连接超时检测(3分钟无人连接则退出)
|