claude-opencode-viewer 2.6.36 → 2.6.38
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 +158 -2
- package/index.html +1 -1
- package/package.json +1 -1
- package/server.js +106 -55
package/index-pc.html
CHANGED
|
@@ -761,6 +761,58 @@
|
|
|
761
761
|
padding: 1px 6px;
|
|
762
762
|
border-radius: 8px;
|
|
763
763
|
}
|
|
764
|
+
/* 启动对话框 */
|
|
765
|
+
#startup-overlay {
|
|
766
|
+
display: none;
|
|
767
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
768
|
+
background: rgba(0, 0, 0, 0.75);
|
|
769
|
+
z-index: 99999;
|
|
770
|
+
align-items: center; justify-content: center;
|
|
771
|
+
}
|
|
772
|
+
#startup-overlay.visible { display: flex; }
|
|
773
|
+
#startup-card {
|
|
774
|
+
background: #1e1e2e; border-radius: 12px; padding: 28px;
|
|
775
|
+
max-width: 500px; width: 90%; color: #ccc;
|
|
776
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
777
|
+
}
|
|
778
|
+
#startup-card h3 {
|
|
779
|
+
margin: 0 0 16px 0; color: #e0e0e0; font-size: 16px; font-weight: 600;
|
|
780
|
+
}
|
|
781
|
+
.startup-session-info {
|
|
782
|
+
background: #252535; border-radius: 8px; padding: 14px; margin-bottom: 16px;
|
|
783
|
+
border-left: 3px solid #4a9eff;
|
|
784
|
+
}
|
|
785
|
+
.startup-session-info.claude-session { border-left-color: #4a9eff; }
|
|
786
|
+
.startup-session-mode {
|
|
787
|
+
font-size: 12px; font-weight: 600; margin-bottom: 6px; color: #aaa;
|
|
788
|
+
}
|
|
789
|
+
.startup-session-mode .mode-tag {
|
|
790
|
+
display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px;
|
|
791
|
+
}
|
|
792
|
+
.startup-session-mode .mode-tag.opencode { background: #1a3a2a; color: #4ec9b0; }
|
|
793
|
+
.startup-session-mode .mode-tag.claude { background: #1a2a3a; color: #4a9eff; }
|
|
794
|
+
.startup-session-preview {
|
|
795
|
+
font-size: 13px; color: #ddd; margin-bottom: 6px;
|
|
796
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
797
|
+
}
|
|
798
|
+
.startup-session-meta {
|
|
799
|
+
font-size: 11px; color: #888;
|
|
800
|
+
}
|
|
801
|
+
.startup-buttons {
|
|
802
|
+
display: flex; gap: 10px; margin-top: 8px;
|
|
803
|
+
}
|
|
804
|
+
.startup-buttons button {
|
|
805
|
+
flex: 1; padding: 10px 16px; border: none; border-radius: 8px;
|
|
806
|
+
font-size: 14px; cursor: pointer; font-weight: 500; transition: background 0.2s;
|
|
807
|
+
}
|
|
808
|
+
.startup-btn-restore {
|
|
809
|
+
background: #4a9eff; color: #fff;
|
|
810
|
+
}
|
|
811
|
+
.startup-btn-restore:hover { background: #3a8eef; }
|
|
812
|
+
.startup-btn-new {
|
|
813
|
+
background: #333; color: #ccc;
|
|
814
|
+
}
|
|
815
|
+
.startup-btn-new:hover { background: #444; }
|
|
764
816
|
</style>
|
|
765
817
|
</head>
|
|
766
818
|
<body>
|
|
@@ -964,6 +1016,7 @@
|
|
|
964
1016
|
var currentMode = 'claude';
|
|
965
1017
|
var isTransitioning = false;
|
|
966
1018
|
var isBufferReplay = true; // 初始缓冲区回放中,不弹 toast
|
|
1019
|
+
var startupDialogShown = false;
|
|
967
1020
|
|
|
968
1021
|
var term = new Terminal({
|
|
969
1022
|
cursorBlink: !isMobile,
|
|
@@ -1432,6 +1485,92 @@
|
|
|
1432
1485
|
}, 50);
|
|
1433
1486
|
});
|
|
1434
1487
|
|
|
1488
|
+
// === 启动对话框 ===
|
|
1489
|
+
function showStartupDialog() {
|
|
1490
|
+
fetch(basePath + '/api/last-sessions')
|
|
1491
|
+
.then(function(r) { return r.json(); })
|
|
1492
|
+
.then(function(data) {
|
|
1493
|
+
var oc = data.opencode;
|
|
1494
|
+
var cl = data.claude;
|
|
1495
|
+
// 无任何历史 → 直接新建 claude
|
|
1496
|
+
if (!oc && !cl) {
|
|
1497
|
+
sendInit('claude', null);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
// 确定最近的会话
|
|
1501
|
+
var latest = null, latestMode = 'claude';
|
|
1502
|
+
if (oc && cl) {
|
|
1503
|
+
if (oc.mtime >= cl.mtime) { latest = oc; latestMode = 'opencode'; }
|
|
1504
|
+
else { latest = cl; latestMode = 'claude'; }
|
|
1505
|
+
} else if (oc) { latest = oc; latestMode = 'opencode'; }
|
|
1506
|
+
else { latest = cl; latestMode = 'claude'; }
|
|
1507
|
+
|
|
1508
|
+
// 渲染会话信息
|
|
1509
|
+
var infoDiv = document.getElementById('startup-session-info');
|
|
1510
|
+
var modeLabel = latestMode === 'claude' ? 'Claude Code' : 'OpenCode';
|
|
1511
|
+
var modeClass = latestMode === 'claude' ? 'claude' : 'opencode';
|
|
1512
|
+
var timeAgo = getTimeAgo(latest.mtime);
|
|
1513
|
+
var dir = latest.directory || '';
|
|
1514
|
+
if (dir.startsWith('/Users/')) dir = '~' + dir.substring(dir.indexOf('/', 1));
|
|
1515
|
+
var preview = latest.preview || '(无预览)';
|
|
1516
|
+
if (preview.length > 100) preview = preview.substring(0, 100) + '...';
|
|
1517
|
+
|
|
1518
|
+
infoDiv.innerHTML =
|
|
1519
|
+
'<div class="startup-session-info ' + (latestMode === 'claude' ? 'claude-session' : '') + '">' +
|
|
1520
|
+
'<div class="startup-session-mode"><span class="mode-tag ' + modeClass + '">' + modeLabel + '</span> · ' + timeAgo + '</div>' +
|
|
1521
|
+
'<div class="startup-session-preview">' + escapeHtml(preview) + '</div>' +
|
|
1522
|
+
(dir ? '<div class="startup-session-meta">' + escapeHtml(dir) + '</div>' : '') +
|
|
1523
|
+
'</div>';
|
|
1524
|
+
|
|
1525
|
+
// 恢复按钮样式
|
|
1526
|
+
var restoreBtn = document.getElementById('startup-btn-restore');
|
|
1527
|
+
restoreBtn.className = 'startup-btn-restore';
|
|
1528
|
+
restoreBtn.textContent = '恢复会话';
|
|
1529
|
+
|
|
1530
|
+
// 绑定事件
|
|
1531
|
+
restoreBtn.onclick = function() {
|
|
1532
|
+
hideStartupDialog();
|
|
1533
|
+
sendInit(latestMode, latest.id);
|
|
1534
|
+
};
|
|
1535
|
+
document.getElementById('startup-btn-new').onclick = function() {
|
|
1536
|
+
hideStartupDialog();
|
|
1537
|
+
sendInit('claude', null);
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
// 显示对话框
|
|
1541
|
+
document.getElementById('startup-overlay').classList.add('visible');
|
|
1542
|
+
})
|
|
1543
|
+
.catch(function() {
|
|
1544
|
+
// API 失败,直接新建
|
|
1545
|
+
sendInit('claude', null);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function hideStartupDialog() {
|
|
1550
|
+
document.getElementById('startup-overlay').classList.remove('visible');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function sendInit(mode, sessionId) {
|
|
1554
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1555
|
+
currentMode = mode;
|
|
1556
|
+
modeSelect.value = mode;
|
|
1557
|
+
document.getElementById('mode-label').textContent = '';
|
|
1558
|
+
var msg = { type: 'init', mode: mode };
|
|
1559
|
+
if (sessionId) msg.sessionId = sessionId;
|
|
1560
|
+
ws.send(JSON.stringify(msg));
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function getTimeAgo(ts) {
|
|
1564
|
+
var diff = Date.now() - ts;
|
|
1565
|
+
var min = Math.floor(diff / 60000);
|
|
1566
|
+
if (min < 1) return '刚刚';
|
|
1567
|
+
if (min < 60) return min + ' 分钟前';
|
|
1568
|
+
var hr = Math.floor(min / 60);
|
|
1569
|
+
if (hr < 24) return hr + ' 小时前';
|
|
1570
|
+
var day = Math.floor(hr / 24);
|
|
1571
|
+
return day + ' 天前';
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1435
1574
|
function connect() {
|
|
1436
1575
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1437
1576
|
ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
|
|
@@ -1441,6 +1580,7 @@
|
|
|
1441
1580
|
resize();
|
|
1442
1581
|
rebindTouchScroll();
|
|
1443
1582
|
setTimeout(function() { isBufferReplay = false; }, 500);
|
|
1583
|
+
// 不在这里初始化,等 state 消息判断是否需要弹对话框
|
|
1444
1584
|
};
|
|
1445
1585
|
|
|
1446
1586
|
ws.onclose = function() {
|
|
@@ -1477,7 +1617,12 @@
|
|
|
1477
1617
|
if (msg.mode) {
|
|
1478
1618
|
currentMode = msg.mode;
|
|
1479
1619
|
modeSelect.value = msg.mode;
|
|
1480
|
-
|
|
1620
|
+
document.getElementById('mode-label').textContent = '';
|
|
1621
|
+
}
|
|
1622
|
+
// 服务端无运行进程,显示启动对话框
|
|
1623
|
+
if (!msg.running && !startupDialogShown) {
|
|
1624
|
+
startupDialogShown = true;
|
|
1625
|
+
showStartupDialog();
|
|
1481
1626
|
}
|
|
1482
1627
|
}
|
|
1483
1628
|
else if (msg.type === 'restored') {
|
|
@@ -2241,7 +2386,7 @@
|
|
|
2241
2386
|
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2242
2387
|
return;
|
|
2243
2388
|
}
|
|
2244
|
-
var html = '';
|
|
2389
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2245
2390
|
docs.forEach(function(doc) {
|
|
2246
2391
|
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2247
2392
|
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
@@ -2301,5 +2446,16 @@
|
|
|
2301
2446
|
setTimeout(resize, 100);
|
|
2302
2447
|
})();
|
|
2303
2448
|
</script>
|
|
2449
|
+
<!-- 启动对话框 -->
|
|
2450
|
+
<div id="startup-overlay">
|
|
2451
|
+
<div id="startup-card">
|
|
2452
|
+
<h3>选择会话</h3>
|
|
2453
|
+
<div id="startup-session-info"></div>
|
|
2454
|
+
<div class="startup-buttons">
|
|
2455
|
+
<button class="startup-btn-restore" id="startup-btn-restore">恢复会话</button>
|
|
2456
|
+
<button class="startup-btn-new" id="startup-btn-new">新建会话</button>
|
|
2457
|
+
</div>
|
|
2458
|
+
</div>
|
|
2459
|
+
</div>
|
|
2304
2460
|
</body>
|
|
2305
2461
|
</html>
|
package/index.html
CHANGED
|
@@ -2505,7 +2505,7 @@
|
|
|
2505
2505
|
fileList.innerHTML = '<div class="docs-loading" style="color:#666;">无文档</div>';
|
|
2506
2506
|
return;
|
|
2507
2507
|
}
|
|
2508
|
-
var html = '';
|
|
2508
|
+
var html = '<div style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #2a2a2a;">📁 ' + escapeHtml(data.cwd || 'PROJECT_DIR') + '</div>';
|
|
2509
2509
|
docs.forEach(function(doc) {
|
|
2510
2510
|
var activeClass = docsSelectedFile === doc.name ? ' active' : '';
|
|
2511
2511
|
var time = new Date(doc.mtime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -644,15 +644,15 @@ const requestHandler = async (req, res) => {
|
|
|
644
644
|
try {
|
|
645
645
|
const gitCwd = process.env.PROJECT_DIR || process.cwd();
|
|
646
646
|
const { stdout } = await execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain'], {
|
|
647
|
-
cwd: gitCwd, encoding: 'utf-8', timeout:
|
|
647
|
+
cwd: gitCwd, encoding: 'utf-8', timeout: 60000,
|
|
648
648
|
});
|
|
649
649
|
const changes = stdout.split('\n').filter(Boolean).map(line => ({
|
|
650
650
|
status: line.substring(0, 2).trim(),
|
|
651
651
|
file: line.substring(3),
|
|
652
652
|
})).filter(c => !/^core-/.test(c.file));
|
|
653
653
|
res.end(JSON.stringify({ changes, cwd: gitCwd }));
|
|
654
|
-
} catch {
|
|
655
|
-
res.end(JSON.stringify({ changes: [], cwd: process.env.PROJECT_DIR || process.cwd() }));
|
|
654
|
+
} catch (err) {
|
|
655
|
+
res.end(JSON.stringify({ changes: [], cwd: process.env.PROJECT_DIR || process.cwd(), error: err.message }));
|
|
656
656
|
}
|
|
657
657
|
return;
|
|
658
658
|
}
|
|
@@ -675,7 +675,7 @@ const requestHandler = async (req, res) => {
|
|
|
675
675
|
for (const file of fileList) {
|
|
676
676
|
if (file.includes('..') || file.startsWith('/')) continue;
|
|
677
677
|
try {
|
|
678
|
-
const { stdout: statusOut } = await execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain', '--', file], { cwd, encoding: 'utf-8', timeout:
|
|
678
|
+
const { stdout: statusOut } = await execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain', '--', file], { cwd, encoding: 'utf-8', timeout: 30000 });
|
|
679
679
|
if (!statusOut.trim()) continue;
|
|
680
680
|
const status = statusOut.substring(0, 2).trim();
|
|
681
681
|
const is_new = status === 'A' || status === '??';
|
|
@@ -683,7 +683,7 @@ const requestHandler = async (req, res) => {
|
|
|
683
683
|
let is_binary = false;
|
|
684
684
|
if (!is_deleted) {
|
|
685
685
|
try {
|
|
686
|
-
const { stdout: dc } = await execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--numstat', 'HEAD', '--', file], { cwd, encoding: 'utf-8', timeout:
|
|
686
|
+
const { stdout: dc } = await execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--numstat', 'HEAD', '--', file], { cwd, encoding: 'utf-8', timeout: 30000 });
|
|
687
687
|
if (dc.includes('-\t-\t')) is_binary = true;
|
|
688
688
|
} catch {}
|
|
689
689
|
}
|
|
@@ -691,7 +691,7 @@ const requestHandler = async (req, res) => {
|
|
|
691
691
|
if (!is_binary) {
|
|
692
692
|
if (!is_new) {
|
|
693
693
|
try {
|
|
694
|
-
const { stdout } = await execFileAsync('git', ['-c', 'safe.directory=*', 'show', `HEAD:${file}`], { cwd, encoding: 'utf-8', timeout:
|
|
694
|
+
const { stdout } = await execFileAsync('git', ['-c', 'safe.directory=*', 'show', `HEAD:${file}`], { cwd, encoding: 'utf-8', timeout: 60000, maxBuffer: 5 * 1024 * 1024 });
|
|
695
695
|
old_content = stdout;
|
|
696
696
|
} catch {}
|
|
697
697
|
}
|
|
@@ -713,7 +713,7 @@ const requestHandler = async (req, res) => {
|
|
|
713
713
|
const diffArgs = is_new
|
|
714
714
|
? ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', '--no-index', '/dev/null', file]
|
|
715
715
|
: ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', 'HEAD', '--', file];
|
|
716
|
-
const { stdout } = await execFileAsync('git', diffArgs, { cwd, encoding: 'utf-8', timeout:
|
|
716
|
+
const { stdout } = await execFileAsync('git', diffArgs, { cwd, encoding: 'utf-8', timeout: 60000, maxBuffer: 5 * 1024 * 1024 });
|
|
717
717
|
unified_diff = stdout;
|
|
718
718
|
} catch (e) {
|
|
719
719
|
if (e.stdout) unified_diff = e.stdout;
|
|
@@ -752,7 +752,7 @@ const requestHandler = async (req, res) => {
|
|
|
752
752
|
};
|
|
753
753
|
scanDir(docsRoot, '', 0);
|
|
754
754
|
files.sort((a, b) => b.mtime - a.mtime);
|
|
755
|
-
res.end(JSON.stringify({ docs: files }));
|
|
755
|
+
res.end(JSON.stringify({ docs: files, cwd: docsRoot }));
|
|
756
756
|
} catch (err) {
|
|
757
757
|
res.end(JSON.stringify({ docs: [], error: err.message }));
|
|
758
758
|
}
|
|
@@ -782,6 +782,87 @@ const requestHandler = async (req, res) => {
|
|
|
782
782
|
return;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
// API: 获取最近的 OpenCode 和 Claude 会话(用于启动对话框)
|
|
786
|
+
if (req.url === '/api/last-sessions') {
|
|
787
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
788
|
+
let opencode = null, claude = null;
|
|
789
|
+
// OpenCode: 从 SQLite 查最近会话
|
|
790
|
+
try {
|
|
791
|
+
const db = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
792
|
+
const row = db.prepare(
|
|
793
|
+
`SELECT id, directory, time_updated FROM session WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_updated DESC LIMIT 1`
|
|
794
|
+
).get();
|
|
795
|
+
db.close();
|
|
796
|
+
if (row) {
|
|
797
|
+
opencode = { id: row.id, mtime: new Date(row.time_updated).getTime(), directory: row.directory || '', preview: '' };
|
|
798
|
+
// 尝试读最近用户消息作为预览(文本在 part 表中)
|
|
799
|
+
try {
|
|
800
|
+
const db2 = new Database(OPENCODE_DB_PATH, { readonly: true });
|
|
801
|
+
const partRow = db2.prepare(
|
|
802
|
+
`SELECT p.data FROM part p JOIN message m ON p.message_id = m.id
|
|
803
|
+
WHERE m.session_id = ? AND json_extract(m.data, '$.role') = 'user'
|
|
804
|
+
ORDER BY m.time_created DESC LIMIT 1`
|
|
805
|
+
).get(row.id);
|
|
806
|
+
db2.close();
|
|
807
|
+
if (partRow) {
|
|
808
|
+
try {
|
|
809
|
+
const pd = JSON.parse(partRow.data);
|
|
810
|
+
if (pd.text) opencode.preview = pd.text.slice(0, 200);
|
|
811
|
+
} catch {}
|
|
812
|
+
}
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
} catch {}
|
|
816
|
+
// Claude: 扫描 JSONL 找最近文件
|
|
817
|
+
try {
|
|
818
|
+
const projectsDir = join(CLAUDE_HOME, 'projects');
|
|
819
|
+
if (existsSync(projectsDir)) {
|
|
820
|
+
let newest = 0, newestFile = null, newestProj = null;
|
|
821
|
+
for (const projDir of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
822
|
+
if (!projDir.isDirectory()) continue;
|
|
823
|
+
const projPath = join(projectsDir, projDir.name);
|
|
824
|
+
for (const f of readdirSync(projPath)) {
|
|
825
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
826
|
+
try {
|
|
827
|
+
const st = statSync(join(projPath, f));
|
|
828
|
+
if (st.mtimeMs > newest) {
|
|
829
|
+
newest = st.mtimeMs;
|
|
830
|
+
newestFile = f.replace('.jsonl', '');
|
|
831
|
+
newestProj = projDir.name;
|
|
832
|
+
}
|
|
833
|
+
} catch {}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (newestFile && newestProj) {
|
|
837
|
+
claude = { id: newestFile, mtime: newest, project: newestProj, directory: '', preview: '' };
|
|
838
|
+
try {
|
|
839
|
+
const content = readFileSync(join(projectsDir, newestProj, newestFile + '.jsonl'), 'utf-8');
|
|
840
|
+
const lines = content.split('\n').slice(0, 30);
|
|
841
|
+
for (const line of lines) {
|
|
842
|
+
if (!line.trim()) continue;
|
|
843
|
+
try {
|
|
844
|
+
const d = JSON.parse(line);
|
|
845
|
+
if (d.cwd && !claude.directory) claude.directory = d.cwd;
|
|
846
|
+
if (d.type === 'user' && d.message && !claude.preview) {
|
|
847
|
+
const c = d.message.content;
|
|
848
|
+
if (typeof c === 'string') claude.preview = c.slice(0, 200);
|
|
849
|
+
else if (Array.isArray(c)) {
|
|
850
|
+
for (const item of c) {
|
|
851
|
+
if (item.type === 'text' && item.text) { claude.preview = item.text.slice(0, 200); break; }
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
} catch {}
|
|
857
|
+
}
|
|
858
|
+
} catch {}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
} catch {}
|
|
862
|
+
res.end(JSON.stringify({ opencode, claude }));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
785
866
|
// API: Claude Code 会话列表(扫描所有项目)
|
|
786
867
|
if (req.url === '/api/claude-sessions') {
|
|
787
868
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -1149,6 +1230,20 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1149
1230
|
}
|
|
1150
1231
|
isSwitching = false;
|
|
1151
1232
|
}
|
|
1233
|
+
} else if (msg.type === 'init') {
|
|
1234
|
+
// 首次启动:客户端选择模式和会话后发送
|
|
1235
|
+
const mode = msg.mode || 'claude';
|
|
1236
|
+
currentMode = mode;
|
|
1237
|
+
LOG(`[init] 启动 ${mode}, sessionId=${msg.sessionId || '(新会话)'}`);
|
|
1238
|
+
outputBuffer = '';
|
|
1239
|
+
try {
|
|
1240
|
+
await spawnProcess(mode, msg.sessionId || null);
|
|
1241
|
+
ws.send(JSON.stringify({ type: 'mode', mode: currentMode }));
|
|
1242
|
+
ws.send(JSON.stringify({ type: 'started', sessionId: msg.sessionId || null }));
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
LOG('[init] 启动失败:', e.message);
|
|
1245
|
+
ws.send(JSON.stringify({ type: 'start-error', error: e.message }));
|
|
1246
|
+
}
|
|
1152
1247
|
} else if (msg.type === 'start') {
|
|
1153
1248
|
// 前端启动指令:可选带 sessionId 恢复会话
|
|
1154
1249
|
const mode = currentMode;
|
|
@@ -1297,54 +1392,10 @@ function startServer() {
|
|
|
1297
1392
|
// 清理上次 cov 崩溃残留的孤儿进程
|
|
1298
1393
|
cleanupOrphanProcesses();
|
|
1299
1394
|
|
|
1300
|
-
// 尝试恢复最近的会话,如果没有则新建
|
|
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) {}
|
|
1311
1395
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
} else {
|
|
1316
|
-
await spawnProcess('opencode');
|
|
1317
|
-
}
|
|
1318
|
-
} else {
|
|
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
|
-
}
|
|
1347
|
-
}
|
|
1396
|
+
// 延迟启动:等待 PC 客户端发送 init 消息后再 spawn 进程
|
|
1397
|
+
// 移动端客户端连接时自动启动 claude(见 WS 连接处理)
|
|
1398
|
+
LOG('[startup] 等待客户端连接并选择会话...');
|
|
1348
1399
|
|
|
1349
1400
|
// 启动首次连接超时检测(3分钟无人连接则退出)
|
|
1350
1401
|
startNoClientTimer();
|